更新
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 97fdf9e2a23cdfa40a8486c04fa81663
|
||||
@@ -5,6 +5,7 @@ using Continentis.MainGame.Card;
|
||||
using Continentis.MainGame.Combat;
|
||||
using Lean.Pool;
|
||||
using NaughtyAttributes;
|
||||
using SLSFramework.General;
|
||||
using SLSFramework.UModAssistance;
|
||||
using UnityEngine;
|
||||
using Object = UnityEngine.Object;
|
||||
@@ -135,7 +136,23 @@ namespace Continentis.MainGame.Character
|
||||
|
||||
public virtual void Die()
|
||||
{
|
||||
CombatMainManager.Instance.characterController.RemoveCharacter(this);
|
||||
// TODO: 1.1c — 死亡动画命令(入队 Cmd_PlayAnimation + VFX),待动画系统完善后替换
|
||||
Debug.Log($"[Combat] {data.displayName} 死亡");
|
||||
|
||||
CharacterBase self = this;
|
||||
|
||||
// 触发 onDeath 事件,供 Buff / 技能系统在角色移除前响应
|
||||
CommandQueueManager.Instance.AddCommand(Cmd.Do(() =>
|
||||
{
|
||||
self.eventSubmodule.onDeath.Invoke();
|
||||
CombatMainManager.Instance.eventCollection.onCharacterDeath.Invoke(self);
|
||||
}));
|
||||
|
||||
// 角色移除:从战场数据结构中清理,并触发胜负检查
|
||||
CommandQueueManager.Instance.AddCommand(Cmd.Do(() =>
|
||||
{
|
||||
CombatMainManager.Instance.characterController.RemoveCharacter(self);
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Continentis.MainGame.Card;
|
||||
using Continentis.MainGame.Combat;
|
||||
using Continentis.MainGame.UI;
|
||||
using SLSFramework.General;
|
||||
using UnityEngine;
|
||||
@@ -100,6 +101,9 @@ namespace Continentis.MainGame.Character
|
||||
(attachedCharacter.characterView.hudContainer.enablingHUDs["CharacterBuffCollection"] as HUD_CharacterBuffCollection)
|
||||
?.AddBuffIcon(this);
|
||||
}
|
||||
|
||||
// 1.2c — 记录 Buff 首次施加日志
|
||||
CombatLogs.Instance?.LogBuffApply(this, attachedCharacter);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -114,6 +118,9 @@ namespace Continentis.MainGame.Character
|
||||
|
||||
public override void Remove()
|
||||
{
|
||||
// 1.2c — 记录 Buff 移除日志
|
||||
CombatLogs.Instance?.LogBuffRemove(this);
|
||||
|
||||
OnBuffRemove();
|
||||
|
||||
if (iconSubmodule != null)
|
||||
@@ -189,5 +196,15 @@ namespace Continentis.MainGame.Character
|
||||
generalAttributeSubmodule?.RefreshAllModifiedAttributes();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将一条命令加入 Reaction Lane,确保 Buff 触发的动画/视觉效果
|
||||
/// 在当前 Main Lane 命令完成后、下一条 Main Lane 命令执行前播放。
|
||||
/// Buff 中需要动画的被动响应(反击、护盾反伤等)应使用此方法入队。
|
||||
/// </summary>
|
||||
protected void EnqueueReaction(CommandBase cmd)
|
||||
{
|
||||
CommandQueueManager.Instance.AddCommand(cmd, CommandLane.Reaction);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -149,5 +149,8 @@
|
||||
|
||||
/// <summary> 来自魅力的增减益 </summary>
|
||||
public const string OffsetFromCharisma = "OffsetFromCharisma";
|
||||
|
||||
/// <summary> 通用增减益 </summary>
|
||||
public const string UniversalOffset = "UniversalOffset";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d62ac580deaf21a428e43e567b08f790
|
||||
@@ -0,0 +1,77 @@
|
||||
using SLSFramework.General;
|
||||
|
||||
namespace Continentis.MainGame.Character
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色生命周期事件的统一分发入口。
|
||||
/// 每个 Dispatch 方法按固定顺序触发:角色 → 装备 → Buff → 卡牌,
|
||||
/// 消除 CombatMainManager 中手动遍历各层级的冗余代码。
|
||||
/// </summary>
|
||||
public partial class CharacterBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 战斗开始时分发:角色 → 装备 → Buff → 卡牌。
|
||||
/// </summary>
|
||||
public void DispatchCombatStart()
|
||||
{
|
||||
eventSubmodule.onCombatStart.Invoke();
|
||||
equipmentSubmodule.currentEquipments.ForEach(e => e.eventSubmodule.onCombatStart.Invoke());
|
||||
combatBuffSubmodule.CombatStart();
|
||||
deckSubmodule.GetAllCards().ForEach(c => c.eventSubmodule.onCombatStart.Invoke());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 战斗结束时分发:角色 → 装备 → Buff。
|
||||
/// </summary>
|
||||
public void DispatchCombatEnd()
|
||||
{
|
||||
eventSubmodule.onCombatEnd.Invoke();
|
||||
equipmentSubmodule.currentEquipments.ForEach(e => e.eventSubmodule.onCombatEnd.Invoke());
|
||||
combatBuffSubmodule.CombatEnd();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 回合开始时分发:角色 → 装备 → Buff(含回合计数更新) → 卡牌。
|
||||
/// </summary>
|
||||
public void DispatchRoundStart()
|
||||
{
|
||||
eventSubmodule.onRoundStart.Invoke();
|
||||
equipmentSubmodule.currentEquipments.ForEach(e => e.eventSubmodule.onRoundStart.Invoke());
|
||||
combatBuffSubmodule.RoundStart();
|
||||
deckSubmodule.GetAllCards().ForEach(c => c.eventSubmodule.onRoundStart.Invoke());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 回合结束时分发:角色 → 装备 → Buff → 卡牌。
|
||||
/// </summary>
|
||||
public void DispatchRoundEnd()
|
||||
{
|
||||
eventSubmodule.onRoundEnd.Invoke();
|
||||
equipmentSubmodule.currentEquipments.ForEach(e => e.eventSubmodule.onRoundEnd.Invoke());
|
||||
combatBuffSubmodule.RoundEnd();
|
||||
deckSubmodule.GetAllCards().ForEach(c => c.eventSubmodule.onRoundEnd.Invoke());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 行动开始时分发:角色 → 装备 → Buff(含行动计数更新) → 卡牌。
|
||||
/// </summary>
|
||||
public void DispatchActionStart()
|
||||
{
|
||||
eventSubmodule.onActionStart.Invoke();
|
||||
equipmentSubmodule.currentEquipments.ForEach(e => e.eventSubmodule.onActionStart.Invoke());
|
||||
combatBuffSubmodule.ActionStart();
|
||||
deckSubmodule.GetAllCards().ForEach(c => c.eventSubmodule.onActionStart.Invoke());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 行动结束时分发:角色 → 装备 → Buff → 卡牌。
|
||||
/// </summary>
|
||||
public void DispatchActionEnd()
|
||||
{
|
||||
eventSubmodule.onActionEnd.Invoke();
|
||||
equipmentSubmodule.currentEquipments.ForEach(e => e.eventSubmodule.onActionEnd.Invoke());
|
||||
combatBuffSubmodule.ActionEnd();
|
||||
deckSubmodule.GetAllCards().ForEach(c => c.eventSubmodule.onActionEnd.Invoke());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 84dd7cd1188860440897bf13fe67b5bb
|
||||
@@ -8,9 +8,18 @@ namespace Continentis.MainGame.Character
|
||||
public partial class CharacterLogicBase
|
||||
{
|
||||
protected CharacterBase character;
|
||||
|
||||
/// <summary>在角色被创建后调用,用于注册 Intention 和订阅事件。</summary>
|
||||
public virtual void Initialize(CharacterBase character)
|
||||
{
|
||||
this.character = character;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 角色 HP 发生变化且百分比穿越整十档时由 <see cref="CharacterBase.HealthRemoval"/> 回调。
|
||||
/// Boss Logic 可重写此方法实现阶段切换。
|
||||
/// <paramref name="healthPercentage"/> 范围 [0, 1],例如 0.5 代表剩余 50% 血量。
|
||||
/// </summary>
|
||||
public virtual void OnHealthThreshold(float healthPercentage) { }
|
||||
}
|
||||
}
|
||||
@@ -79,22 +79,38 @@ namespace Continentis.MainGame.Character
|
||||
public partial class CharacterBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 攻击目标
|
||||
/// 攻击目标(新版本,使用 AttackContext 携带标签与来源信息)。
|
||||
/// </summary>
|
||||
/// <param name="target">目标</param>
|
||||
/// <param name="target">攻击目标</param>
|
||||
/// <param name="startDamage">初始伤害</param>
|
||||
/// <param name="ignoreDodge">是否无视闪避</param>
|
||||
/// <param name="ignoreBlock">是否无视格挡</param>
|
||||
/// <param name="ignoreShield">是否无视护盾</param>
|
||||
/// <returns>实际造成的伤害</returns>
|
||||
public AttackResult Attack(CharacterBase target, int startDamage, CardInstance attackCard = null, bool triggerAttackEvent = true, bool ignoreDodge = false, bool ignoreBlock = false, bool ignoreShield = false)
|
||||
/// <param name="context">攻击上下文(包含来源卡牌、标签等);传 null 等价于默认上下文</param>
|
||||
/// <returns>攻击结果</returns>
|
||||
public AttackResult Attack(CharacterBase target, int startDamage, AttackContext context)
|
||||
{
|
||||
if (triggerAttackEvent)
|
||||
context ??= new AttackContext();
|
||||
|
||||
bool isSilent = context.HasTag(AttackTags.Silent);
|
||||
bool isHpRemoval = context.HasTag(AttackTags.HpRemoval);
|
||||
bool isReactive = context.HasTag(AttackTags.Reactive);
|
||||
bool ignoreDodge = context.HasAnyTag(AttackTags.GuaranteedHit, AttackTags.HpRemoval);
|
||||
bool ignoreBlock = context.HasAnyTag(AttackTags.Penetrating, AttackTags.HpRemoval);
|
||||
bool ignoreShield = context.HasAnyTag(AttackTags.HpRemoval);
|
||||
|
||||
// 静默和生命移除均不触发 onStartAttack
|
||||
if (!isSilent && !isHpRemoval)
|
||||
{
|
||||
eventSubmodule.onStartAttack.Invoke(target);
|
||||
}
|
||||
|
||||
//闪避检测:如果闪避成功,直接结束
|
||||
// 生命移除:直接扣血,跳过所有防御和事件
|
||||
if (isHpRemoval)
|
||||
{
|
||||
target.HealthRemoval(startDamage, context);
|
||||
target.characterView.hudContainer.enablingHUDs["MainAttributesBar"].UpdateHud();
|
||||
return new AttackResult(this, target, startDamage, context, false, 0, 0, startDamage);
|
||||
}
|
||||
|
||||
// 闪避检测
|
||||
int modifiedStartDamageForDodge = Mathf.RoundToInt(startDamage * GetRawAttribute("DodgeCheckStartDamageMultiplier", 1));
|
||||
bool dodged = !ignoreDodge && target.CheckDodge(modifiedStartDamageForDodge);
|
||||
|
||||
@@ -113,25 +129,51 @@ namespace Continentis.MainGame.Character
|
||||
{
|
||||
shielded = remainingDamageAfterBlock - remainingDamageAfterShield;
|
||||
hurt = remainingDamageAfterShield;
|
||||
target.HealthRemoval(remainingDamageAfterShield);
|
||||
target.HealthRemoval(remainingDamageAfterShield, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target.characterView.hudContainer.enablingHUDs["MainAttributesBar"].UpdateHud();
|
||||
AttackResult attackResult = new AttackResult(this, target, startDamage, attackCard, dodged, blocked, shielded, hurt);
|
||||
if (triggerAttackEvent)
|
||||
AttackResult attackResult = new AttackResult(this, target, startDamage, context, dodged, blocked, shielded, hurt);
|
||||
|
||||
if (!isSilent)
|
||||
{
|
||||
// 角色 EventSubmodule 级别事件(始终触发,用于日志等)
|
||||
eventSubmodule.onFinishAttack.Invoke(target, attackResult);
|
||||
combatBuffSubmodule.buffList.For(buff =>
|
||||
target.eventSubmodule.onGetAttacked.Invoke(this, attackResult);
|
||||
|
||||
// Buff 层事件:响应式攻击不触发,防止无限递归
|
||||
if (!isReactive)
|
||||
{
|
||||
buff.eventSubmodule?.onDealAttack.Invoke(attackResult);
|
||||
});
|
||||
combatBuffSubmodule.buffList.For(buff =>
|
||||
{
|
||||
buff.eventSubmodule?.onDealAttack.Invoke(attackResult);
|
||||
});
|
||||
|
||||
target.combatBuffSubmodule.buffList.For(buff =>
|
||||
{
|
||||
buff.eventSubmodule?.onGetAttacked.Invoke(attackResult);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return attackResult;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 攻击目标(兼容旧版 API,内部转换为 AttackContext 调用)。
|
||||
/// 新代码请优先使用 Attack(target, damage, AttackContext) 重载。
|
||||
/// </summary>
|
||||
public AttackResult Attack(CharacterBase target, int startDamage, CardInstance attackCard = null, bool triggerAttackEvent = true, bool ignoreDodge = false, bool ignoreBlock = false, bool ignoreShield = false)
|
||||
{
|
||||
var context = new AttackContext(attackCard);
|
||||
if (!triggerAttackEvent) context.WithTag(AttackTags.Silent);
|
||||
if (ignoreDodge) context.WithTag(AttackTags.GuaranteedHit);
|
||||
if (ignoreBlock) context.WithTag(AttackTags.Penetrating);
|
||||
return Attack(target, startDamage, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查闪避(闪避失败后会清空闪避值)
|
||||
/// </summary>
|
||||
@@ -145,12 +187,16 @@ namespace Continentis.MainGame.Character
|
||||
{
|
||||
bool success = damage <= dodge;
|
||||
|
||||
if (!success)
|
||||
if (success)
|
||||
{
|
||||
MainGameManager.Instance.basePrefabs.GenerateInfoText("Dodged!", characterView);
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
attributeSubmodule.generalAttributeGroup.current["Dodge"] = 0;
|
||||
return false;
|
||||
}
|
||||
MainGameManager.Instance.basePrefabs.GenerateInfoText("Dodged!", characterView);
|
||||
return success;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -190,13 +236,13 @@ namespace Continentis.MainGame.Character
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查护盾(并且扣除护盾值)
|
||||
/// 检查临时生命(并且扣除临时生命值)
|
||||
/// </summary>
|
||||
/// <param name="damage">即将受到的伤害</param>
|
||||
/// <returns>护盾之后的剩余伤害</returns>
|
||||
/// <returns>临时生命吸收后的剩余伤害</returns>
|
||||
public int CheckShield(int damage)
|
||||
{
|
||||
int shield = attributeSubmodule.GetGeneralAttribute("Shield");
|
||||
int shield = attributeSubmodule.GetGeneralAttribute("TemporaryHealth");
|
||||
|
||||
if (shield > 0)
|
||||
{
|
||||
@@ -206,12 +252,12 @@ namespace Continentis.MainGame.Character
|
||||
|
||||
if (!success)
|
||||
{
|
||||
attributeSubmodule.generalAttributeGroup.current["Shield"] = 0;
|
||||
attributeSubmodule.generalAttributeGroup.current["TemporaryHealth"] = 0;
|
||||
remainingDamage = damage - shield;
|
||||
}
|
||||
else
|
||||
{
|
||||
attributeSubmodule.generalAttributeGroup.current["Shield"] = shield - damage;
|
||||
attributeSubmodule.generalAttributeGroup.current["TemporaryHealth"] = shield - damage;
|
||||
blockedDamage = damage;
|
||||
}
|
||||
|
||||
@@ -222,11 +268,68 @@ namespace Continentis.MainGame.Character
|
||||
return damage;
|
||||
}
|
||||
|
||||
public void HealthRemoval(int damage)
|
||||
public void HealthRemoval(int damage, AttackContext context = null)
|
||||
{
|
||||
int healthBefore = GetAttribute("Health");
|
||||
ModifyAttribute("Health", -damage);
|
||||
MainGameManager.Instance.basePrefabs.GenerateHurtText(damage, characterView);
|
||||
int healthAfter = GetAttribute("Health");
|
||||
int maxHealth = GetAttribute("MaximumHealth");
|
||||
|
||||
Color dmgTextColor = Color.white;
|
||||
if (context is { damageKeywords: { Count: > 0 } })
|
||||
{
|
||||
foreach (string elementTag in MainGameManager.Instance.elementTags)
|
||||
{
|
||||
if (context.damageKeywords.Contains(elementTag))
|
||||
{
|
||||
switch (elementTag)
|
||||
{
|
||||
case "Fire":
|
||||
dmgTextColor = Color.red;
|
||||
break;
|
||||
case "Ice":
|
||||
dmgTextColor = Color.cyan;
|
||||
break;
|
||||
case "Wind":
|
||||
dmgTextColor = Color.lightGreen;
|
||||
break;
|
||||
case "Earth":
|
||||
dmgTextColor = Color.darkGoldenRod;
|
||||
break;
|
||||
case "Storm":
|
||||
dmgTextColor = Color.magenta;
|
||||
break;
|
||||
case "Light":
|
||||
dmgTextColor = Color.yellowNice;
|
||||
break;
|
||||
case "Darkness":
|
||||
dmgTextColor = Color.rebeccaPurple;
|
||||
break;
|
||||
default:
|
||||
dmgTextColor = Color.white;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MainGameManager.Instance.basePrefabs.GenerateHurtText(damage, characterView, dmgTextColor);
|
||||
// 血量百分比阈值检查:穿越整十档时通知 LogicBase(如 Boss 阶段切换)
|
||||
if (maxHealth > 0 && logicBase != null)
|
||||
{
|
||||
float percentBefore = (float)healthBefore / maxHealth;
|
||||
float percentAfter = (float)healthAfter / maxHealth;
|
||||
// 找出所有被穿越的整十档(从高到低依次触发)
|
||||
for (int threshold = 9; threshold >= 0; threshold--)
|
||||
{
|
||||
float t = threshold * 0.1f;
|
||||
if (percentBefore > t && percentAfter <= t)
|
||||
{
|
||||
logicBase.OnHealthThreshold(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (GetAttribute("Health") <= 0)
|
||||
{
|
||||
Die();
|
||||
@@ -282,15 +385,15 @@ namespace Continentis.MainGame.Character
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加护盾(护盾不会自动清空)
|
||||
/// 添加临时生命(不会自动清空)
|
||||
/// </summary>
|
||||
public void AddShield(int shield, CharacterBase target = null)
|
||||
{
|
||||
int baseShieldAfterOffset = shield + GetAttribute("ShieldGainOffset");
|
||||
int finalShield = Mathf.RoundToInt(baseShieldAfterOffset * GetRawAttribute("ShieldGainMultiplier", 1));
|
||||
int baseShieldAfterOffset = shield + GetAttribute("TemporaryHealthGainOffset");
|
||||
int finalShield = Mathf.RoundToInt(baseShieldAfterOffset * GetRawAttribute("TemporaryHealthGainMultiplier", 1));
|
||||
|
||||
target ??= this;
|
||||
target.ModifyAttribute("Shield", finalShield);
|
||||
target.ModifyAttribute("TemporaryHealth", finalShield);
|
||||
target.characterView.hudContainer.UpdateAllHUD();
|
||||
}
|
||||
}
|
||||
@@ -355,8 +458,8 @@ namespace Continentis.MainGame.Character
|
||||
}
|
||||
|
||||
intended.Add(new IntendedCard(card, targets));
|
||||
remainingStamina -= card.GetAttribute("StaminaCost");
|
||||
remainingMana -= card.GetAttribute("ManaCost");
|
||||
remainingStamina -= card.GetAttribute(CardAttributes.StaminaCost);
|
||||
remainingMana -= card.GetAttribute(CardAttributes.ManaCost);
|
||||
}
|
||||
// 行动力不足则跳过该卡
|
||||
}
|
||||
@@ -398,8 +501,8 @@ namespace Continentis.MainGame.Character
|
||||
|
||||
intended.Add(new IntendedCard(chosen, targets));
|
||||
normal.Remove(chosen);
|
||||
remainingStamina -= chosen.GetAttribute("StaminaCost");
|
||||
remainingMana -= chosen.GetAttribute("ManaCost");
|
||||
remainingStamina -= chosen.GetAttribute(CardAttributes.StaminaCost);
|
||||
remainingMana -= chosen.GetAttribute(CardAttributes.ManaCost);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,8 +511,8 @@ namespace Continentis.MainGame.Character
|
||||
|
||||
bool CanAfford(CardInstance card, int stamina, int mana)
|
||||
{
|
||||
return card.GetAttribute("StaminaCost") <= stamina &&
|
||||
card.GetAttribute("ManaCost") <= mana;
|
||||
return card.GetAttribute(CardAttributes.StaminaCost) <= stamina &&
|
||||
card.GetAttribute(CardAttributes.ManaCost) <= mana;
|
||||
}
|
||||
|
||||
public bool CheckAvailabilityAndSetTargets(CardInstance card, out List<CharacterBase> targets)
|
||||
|
||||
@@ -60,6 +60,16 @@ namespace Continentis.MainGame.Character
|
||||
|
||||
public partial class CombatBuffSubmodule
|
||||
{
|
||||
public void CombatStart()
|
||||
{
|
||||
buffList.For(buff => buff.eventSubmodule?.onCombatStart?.Invoke());
|
||||
}
|
||||
|
||||
public void CombatEnd()
|
||||
{
|
||||
buffList.For(buff => buff.eventSubmodule?.onCombatEnd?.Invoke());
|
||||
}
|
||||
|
||||
public void RoundStart()
|
||||
{
|
||||
buffList.For(buff => buff.roundCountSubmodule?.Update());
|
||||
@@ -73,13 +83,13 @@ namespace Continentis.MainGame.Character
|
||||
|
||||
public void ActionStart()
|
||||
{
|
||||
Debug.Log($"{owner.data.displayName} is starting an action. Current action count this round: {owner.actionCountThisRound}");
|
||||
//Debug.Log($"{owner.data.displayName} is starting an action. Current action count this round: {owner.actionCountThisRound}");
|
||||
if (owner.actionCountThisRound == 0)
|
||||
{
|
||||
Debug.Log($"{owner.data.displayName} is starting their first action this round. Buff count of {buffList.Count} will update their round first action counts.");
|
||||
//Debug.Log($"{owner.data.displayName} is starting their first action this round. Buff count of {buffList.Count} will update their round first action counts.");
|
||||
buffList.For(buff =>
|
||||
{
|
||||
Debug.Log($"Updating round first action count for buff: {buff.contentSubmodule.displayName}");
|
||||
//Debug.Log($"Updating round first action count for buff: {buff.contentSubmodule.displayName}");
|
||||
buff.roundFirstActionCountSubmodule?.Update();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a7e70a75f86711418ab773b6494763e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -14,15 +14,17 @@ namespace Continentis.MainGame.Character
|
||||
|
||||
public DeckSubmodule(CharacterBase character) : base(character)
|
||||
{
|
||||
piles = new Dictionary<string, List<CardInstance>>();
|
||||
piles.Add("Storage", new List<CardInstance>());
|
||||
piles.Add("Hand", new List<CardInstance>());
|
||||
piles.Add("Draw", new List<CardInstance>());
|
||||
piles.Add("Discard", new List<CardInstance>());
|
||||
piles.Add("Exhaust", new List<CardInstance>());
|
||||
piles.Add("Grave", new List<CardInstance>());
|
||||
piles.Add("Pool", new List<CardInstance>());
|
||||
piles.Add("Intention", new List<CardInstance>());
|
||||
piles = new Dictionary<string, List<CardInstance>>
|
||||
{
|
||||
{ Piles.Storage, new List<CardInstance>() },
|
||||
{ Piles.Hand, new List<CardInstance>() },
|
||||
{ Piles.Draw, new List<CardInstance>() },
|
||||
{ Piles.Discard, new List<CardInstance>() },
|
||||
{ Piles.Exhaust, new List<CardInstance>() },
|
||||
{ Piles.Grave, new List<CardInstance>() },
|
||||
{ Piles.Pool, new List<CardInstance>() },
|
||||
{ Piles.Intention, new List<CardInstance>() }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,11 +53,11 @@ namespace Continentis.MainGame.Character
|
||||
|
||||
if (cardCount > DrawPile.Count && DiscardPile.Count > 0)
|
||||
{
|
||||
Debug.Log("抽牌堆牌数不足,且弃牌堆有牌,正在洗牌...");
|
||||
//Debug.Log("抽牌堆牌数不足,且弃牌堆有牌,正在洗牌...");
|
||||
ReshuffleDeck();
|
||||
}
|
||||
|
||||
Debug.Log($"准备抽取 {cardCount} 张卡牌。");
|
||||
//Debug.Log($"准备抽取 {cardCount} 张卡牌。");
|
||||
|
||||
return new CommandGroup(ExecutionMode.Sequential,
|
||||
new Cmd_DrawCards(this, cardCount, interval),
|
||||
@@ -71,7 +73,7 @@ namespace Continentis.MainGame.Character
|
||||
if (drawCardsGroup.groupContext.TryGet(CommandContextKeys.DrawnCards, out List<CardInstance> cards))
|
||||
return cards;
|
||||
|
||||
Debug.LogWarning("[DeckSubmodule] groupContext 中未找到 DrawnCards。");
|
||||
//Debug.LogWarning("[DeckSubmodule] groupContext 中未找到 DrawnCards。");
|
||||
return new List<CardInstance>();
|
||||
}
|
||||
|
||||
@@ -232,13 +234,13 @@ namespace Continentis.MainGame.Character
|
||||
throw new KeyNotFoundException($"Pile '{pileName}' not found.");
|
||||
}
|
||||
|
||||
public List<CardInstance> StoragePile => Pile("Storage");
|
||||
public List<CardInstance> HandPile => Pile("Hand");
|
||||
public List<CardInstance> DrawPile => Pile("Draw");
|
||||
public List<CardInstance> DiscardPile => Pile("Discard");
|
||||
public List<CardInstance> ExhaustPile => Pile("Exhaust");
|
||||
public List<CardInstance> GravePile => Pile("Grave");
|
||||
public List<CardInstance> PoolPile => Pile("Pool");
|
||||
public List<CardInstance> IntentionPile => Pile("Intention");
|
||||
public List<CardInstance> StoragePile => Pile(Piles.Storage);
|
||||
public List<CardInstance> HandPile => Pile(Piles.Hand);
|
||||
public List<CardInstance> DrawPile => Pile(Piles.Draw);
|
||||
public List<CardInstance> DiscardPile => Pile(Piles.Discard);
|
||||
public List<CardInstance> ExhaustPile => Pile(Piles.Exhaust);
|
||||
public List<CardInstance> GravePile => Pile(Piles.Grave);
|
||||
public List<CardInstance> PoolPile => Pile(Piles.Pool);
|
||||
public List<CardInstance> IntentionPile => Pile(Piles.Intention);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
using UnityEngine;
|
||||
|
||||
namespace Continentis.MainGame
|
||||
{
|
||||
public static class Piles
|
||||
{
|
||||
public static string Storage = "Storage";
|
||||
public static string Hand = "Hand";
|
||||
public static string Draw = "Draw";
|
||||
public static string Discard = "Discard";
|
||||
public static string Exhaust = "Exhaust";
|
||||
public static string Grave = "Grave";
|
||||
public static string Pool = "Pool";
|
||||
public static string Intention = "Intention";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4cc14aaaad81bf3428dc9c19c1138a5d
|
||||
@@ -24,6 +24,9 @@ namespace Continentis.MainGame.Character
|
||||
public OrderedDictionary<string, PrioritizedAction<CardInstance, List<CharacterBase>>> onBeforePlayCard; //使用卡牌前,参数为使用的卡牌
|
||||
public OrderedDictionary<string, PrioritizedAction<CardInstance, List<CharacterBase>>> onAfterPlayCard; //使用卡牌后,参数为使用的卡牌
|
||||
|
||||
/// <summary>角色死亡时触发,供 Buff / 技能系统订阅响应</summary>
|
||||
public OrderedDictionary<string, PrioritizedAction> onDeath; //角色死亡时
|
||||
|
||||
public EventSubmodule(CharacterBase character) : base(character)
|
||||
{
|
||||
onCombatStart = new OrderedDictionary<string, PrioritizedAction>();
|
||||
@@ -42,6 +45,8 @@ namespace Continentis.MainGame.Character
|
||||
onBeforePlayCard = new OrderedDictionary<string, PrioritizedAction<CardInstance, List<CharacterBase>>>();
|
||||
onAfterPlayCard = new OrderedDictionary<string, PrioritizedAction<CardInstance, List<CharacterBase>>>();
|
||||
|
||||
onDeath = new OrderedDictionary<string, PrioritizedAction>();
|
||||
|
||||
onActionStart.InsertByPriority("StaminaRecover", new PrioritizedAction(() =>
|
||||
{
|
||||
owner.ModifyAttribute("Stamina", owner.GetAttribute("StaminaRecoverPerAction"));
|
||||
@@ -89,15 +94,19 @@ namespace Continentis.MainGame.Character
|
||||
public int blockedDamage; //格挡掉的伤害
|
||||
public int shieldedDamage; //护盾吸收的伤害
|
||||
public int hurtDamage; //实际受到的伤害
|
||||
|
||||
/// <summary>本次攻击的上下文,包含标签等扩展信息。</summary>
|
||||
public AttackContext context;
|
||||
|
||||
public bool IsHurt => hurtDamage > 0; //是否实际受到伤害
|
||||
|
||||
public AttackResult(CharacterBase attacker, CharacterBase target, int startDamage, CardInstance attackCard, bool isDodged, int blocked, int shielded, int hurt)
|
||||
public AttackResult(CharacterBase attacker, CharacterBase target, int startDamage, AttackContext context, bool isDodged, int blocked, int shielded, int hurt)
|
||||
{
|
||||
this.attacker = attacker;
|
||||
this.target = target;
|
||||
this.attackCard = attackCard;
|
||||
this.attackCard = context?.sourceCard;
|
||||
this.startDamage = startDamage;
|
||||
this.context = context ?? new AttackContext();
|
||||
this.isDodged = isDodged;
|
||||
this.blockedDamage = blocked;
|
||||
this.shieldedDamage = shielded;
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 30d25484995a4fb45a05436995e9a0fa
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
@@ -2,12 +2,13 @@ using System;
|
||||
using System.Collections.Generic;
|
||||
using Continentis.MainGame.Card;
|
||||
using SLSFramework.General;
|
||||
using SoftCircuits.Collections;
|
||||
using UnityEngine;
|
||||
using UnityEngine.Events;
|
||||
|
||||
namespace Continentis.MainGame.Character
|
||||
{
|
||||
public class IntentionSubmodule : SubmoduleBase<CharacterBase>
|
||||
public partial class IntentionSubmodule : SubmoduleBase<CharacterBase>
|
||||
{
|
||||
public List<IntentionBase> allIntentions;
|
||||
public IntentionBase currentIntention;
|
||||
@@ -15,12 +16,25 @@ namespace Continentis.MainGame.Character
|
||||
|
||||
public List<IntendedCard> intendedCards;
|
||||
|
||||
/// <summary>意图卡被移除后触发,参数为被移除的 IntendedCard 和它原来所在的索引。</summary>
|
||||
public OrderedDictionary<string, PrioritizedAction<IntendedCard, int>> onIntendedCardRemoved;
|
||||
|
||||
/// <summary>意图卡被替换后触发,参数为旧 IntendedCard、新 IntendedCard 和所在的索引。</summary>
|
||||
public OrderedDictionary<string, PrioritizedAction<IntendedCard, IntendedCard, int>> onIntendedCardReplaced;
|
||||
|
||||
/// <summary>意图卡被插入后触发,参数为新 IntendedCard 和插入的索引。</summary>
|
||||
public OrderedDictionary<string, PrioritizedAction<IntendedCard, int>> onIntendedCardInserted;
|
||||
|
||||
public IntentionSubmodule(CharacterBase owner) : base(owner)
|
||||
{
|
||||
allIntentions = new List<IntentionBase>();
|
||||
currentIntention = new IntentionBase(this);
|
||||
getIntendedCards = owner.GetIntendedCards;
|
||||
intendedCards = new List<IntendedCard>();
|
||||
|
||||
onIntendedCardRemoved = new OrderedDictionary<string, PrioritizedAction<IntendedCard, int>>();
|
||||
onIntendedCardReplaced = new OrderedDictionary<string, PrioritizedAction<IntendedCard, IntendedCard, int>>();
|
||||
onIntendedCardInserted = new OrderedDictionary<string, PrioritizedAction<IntendedCard, int>>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,6 +85,10 @@ namespace Continentis.MainGame.Character
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// <summary>NPC 本回合出牌前调用,可用于播放蓄力台词、切换动画状态等。</summary>
|
||||
public virtual void PreAction() { }
|
||||
|
||||
/// <summary>NPC 本回合全部卡牌出完后调用,可用于播放结束台词、重置状态等。</summary>
|
||||
public virtual void PostAction() { }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Continentis.MainGame.Card;
|
||||
using SLSFramework.General;
|
||||
using UnityEngine;
|
||||
using Random = UnityEngine.Random;
|
||||
|
||||
namespace Continentis.MainGame.Character
|
||||
{
|
||||
public partial class IntentionSubmodule
|
||||
{
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 原子操作层
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>移除指定位置的意图卡并销毁其视图。</summary>
|
||||
/// <returns>被移除的 IntendedCard,索引越界时返回 null。</returns>
|
||||
public IntendedCard RemoveIntendedCardAt(int index)
|
||||
{
|
||||
if (!IsValidIndex(index)) return null;
|
||||
|
||||
IntendedCard removed = intendedCards[index];
|
||||
removed.cardInstance.DestroyIntentionCardView();
|
||||
intendedCards.RemoveAt(index);
|
||||
|
||||
onIntendedCardRemoved.Invoke(removed, index);
|
||||
return removed;
|
||||
}
|
||||
|
||||
/// <summary>替换指定位置的意图卡,销毁旧视图并生成新视图。</summary>
|
||||
/// <returns>被替换掉的旧 IntendedCard,索引越界时返回 null。</returns>
|
||||
public IntendedCard ReplaceIntendedCardAt(int index, IntendedCard newCard)
|
||||
{
|
||||
if (!IsValidIndex(index)) return null;
|
||||
if (newCard == null)
|
||||
{
|
||||
Debug.LogWarning("[IntentionSubmodule] ReplaceIntendedCardAt: newCard is null, performing remove instead.");
|
||||
return RemoveIntendedCardAt(index);
|
||||
}
|
||||
|
||||
IntendedCard oldCard = intendedCards[index];
|
||||
oldCard.cardInstance.DestroyIntentionCardView();
|
||||
|
||||
intendedCards[index] = newCard;
|
||||
newCard.cardInstance.GenerateIntentionCardView();
|
||||
|
||||
// 设置文本解析目标
|
||||
if (newCard.targets.Count > 0)
|
||||
{
|
||||
newCard.cardInstance.Targeting(newCard.targets[0]);
|
||||
newCard.cardInstance.contentSubmodule.dirtyMark = true;
|
||||
}
|
||||
|
||||
onIntendedCardReplaced.Invoke(oldCard, newCard, index);
|
||||
return oldCard;
|
||||
}
|
||||
|
||||
/// <summary>在指定位置插入一张新意图卡并生成视图。</summary>
|
||||
public void InsertIntendedCard(int index, IntendedCard newCard)
|
||||
{
|
||||
if (newCard == null)
|
||||
{
|
||||
Debug.LogWarning("[IntentionSubmodule] InsertIntendedCard: newCard is null, insertion skipped.");
|
||||
return;
|
||||
}
|
||||
|
||||
int clampedIndex = Mathf.Clamp(index, 0, intendedCards.Count);
|
||||
intendedCards.Insert(clampedIndex, newCard);
|
||||
newCard.cardInstance.GenerateIntentionCardView();
|
||||
|
||||
// 设置文本解析目标
|
||||
if (newCard.targets.Count > 0)
|
||||
{
|
||||
newCard.cardInstance.Targeting(newCard.targets[0]);
|
||||
newCard.cardInstance.contentSubmodule.dirtyMark = true;
|
||||
}
|
||||
|
||||
onIntendedCardInserted.Invoke(newCard, clampedIndex);
|
||||
}
|
||||
|
||||
/// <summary>在末尾追加一张新意图卡并生成视图。</summary>
|
||||
public void AddIntendedCard(IntendedCard newCard)
|
||||
{
|
||||
InsertIntendedCard(intendedCards.Count, newCard);
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 查询 / 工具层
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>返回 intendedCards 中的随机索引,列表为空时返回 -1。</summary>
|
||||
public int GetRandomIntendedCardIndex()
|
||||
{
|
||||
return intendedCards.Count == 0 ? -1 : Random.Range(0, intendedCards.Count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用过滤器从 intendedCards 中筛选出符合条件的意图卡索引列表。
|
||||
/// </summary>
|
||||
/// <param name="filter">返回 true 表示该意图卡通过筛选。</param>
|
||||
public List<int> GetFilteredIntendedCardIndices(Func<IntendedCard, bool> filter)
|
||||
{
|
||||
List<int> result = new List<int>();
|
||||
for (int i = 0; i < intendedCards.Count; i++)
|
||||
{
|
||||
if (filter(intendedCards[i]))
|
||||
result.Add(i);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用过滤器从 intendedCards 中随机选取一个符合条件的意图卡索引。
|
||||
/// </summary>
|
||||
/// <returns>随机索引,无符合条件的意图卡时返回 -1。</returns>
|
||||
public int GetRandomFilteredIntendedCardIndex(Func<IntendedCard, bool> filter)
|
||||
{
|
||||
List<int> filtered = GetFilteredIntendedCardIndices(filter);
|
||||
return filtered.Count == 0 ? -1 : filtered[Random.Range(0, filtered.Count)];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试从 PoolPile 中获取一张可替换的随机卡牌(排除当前已在 intendedCards 中的卡牌)。
|
||||
/// </summary>
|
||||
/// <param name="result">生成的 IntendedCard,失败时为 null。</param>
|
||||
/// <param name="checkAffordability">是否检查体力/法力消耗。</param>
|
||||
/// <returns>是否成功找到可替换的卡牌。</returns>
|
||||
public bool TryGetRandomReplacementCard(out IntendedCard result, bool checkAffordability = false)
|
||||
{
|
||||
result = null;
|
||||
|
||||
HashSet<CardInstance> currentCards = new HashSet<CardInstance>(
|
||||
intendedCards.Select(ic => ic.cardInstance));
|
||||
|
||||
List<CardInstance> candidates = owner.deckSubmodule.PoolPile
|
||||
.Where(card => !currentCards.Contains(card) && !card.weightSubmodule.forceIgnore)
|
||||
.ToList();
|
||||
|
||||
if (checkAffordability)
|
||||
{
|
||||
int stamina = owner.GetAttribute(CharacterAttributes.Stamina);
|
||||
int mana = owner.GetAttribute(CharacterAttributes.Mana);
|
||||
candidates = candidates.Where(card =>
|
||||
card.GetAttribute(CardAttributes.StaminaCost) <= stamina &&
|
||||
card.GetAttribute(CardAttributes.ManaCost) <= mana).ToList();
|
||||
}
|
||||
|
||||
if (candidates.Count == 0) return false;
|
||||
|
||||
CardInstance chosen = candidates[Random.Range(0, candidates.Count)];
|
||||
if (!owner.CheckAvailabilityAndSetTargets(chosen, out List<CharacterBase> targets))
|
||||
return false;
|
||||
|
||||
result = new IntendedCard(chosen, targets);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 组合操作层
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>随机移除一张意图卡。</summary>
|
||||
/// <returns>被移除的 IntendedCard,列表为空时返回 null。</returns>
|
||||
public IntendedCard RemoveRandomIntendedCard()
|
||||
{
|
||||
int index = GetRandomIntendedCardIndex();
|
||||
return index < 0 ? null : RemoveIntendedCardAt(index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 随机移除一张符合过滤条件的意图卡。
|
||||
/// </summary>
|
||||
/// <param name="filter">返回 true 表示该意图卡可被移除。</param>
|
||||
/// <returns>被移除的 IntendedCard,无符合条件的意图卡时返回 null。</returns>
|
||||
public IntendedCard RemoveRandomIntendedCard(Func<IntendedCard, bool> filter)
|
||||
{
|
||||
int index = GetRandomFilteredIntendedCardIndex(filter);
|
||||
return index < 0 ? null : RemoveIntendedCardAt(index);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 随机将一张意图卡替换为 PoolPile 中的另一张卡牌。
|
||||
/// 若无可替换卡牌则降级为移除。
|
||||
/// </summary>
|
||||
/// <param name="checkAffordability">是否检查体力/法力消耗。</param>
|
||||
/// <returns>操作是否成功执行(移除或替换均算成功)。</returns>
|
||||
public bool ChangeRandomIntendedCard(bool checkAffordability = false)
|
||||
{
|
||||
int index = GetRandomIntendedCardIndex();
|
||||
if (index < 0) return false;
|
||||
|
||||
if (TryGetRandomReplacementCard(out IntendedCard replacement, checkAffordability))
|
||||
{
|
||||
ReplaceIntendedCardAt(index, replacement);
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveIntendedCardAt(index);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 随机将一张符合过滤条件的意图卡替换为 PoolPile 中的另一张卡牌。
|
||||
/// 若无可替换卡牌则降级为移除。
|
||||
/// </summary>
|
||||
/// <param name="filter">返回 true 表示该意图卡可被替换。</param>
|
||||
/// <param name="checkAffordability">是否检查体力/法力消耗。</param>
|
||||
/// <returns>操作是否成功执行。</returns>
|
||||
public bool ChangeRandomIntendedCard(Func<IntendedCard, bool> filter, bool checkAffordability = false)
|
||||
{
|
||||
int index = GetRandomFilteredIntendedCardIndex(filter);
|
||||
if (index < 0) return false;
|
||||
|
||||
if (TryGetRandomReplacementCard(out IntendedCard replacement, checkAffordability))
|
||||
{
|
||||
ReplaceIntendedCardAt(index, replacement);
|
||||
}
|
||||
else
|
||||
{
|
||||
RemoveIntendedCardAt(index);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
// 内部工具
|
||||
// ──────────────────────────────────────────────────────────────
|
||||
|
||||
private bool IsValidIndex(int index)
|
||||
{
|
||||
if (index >= 0 && index < intendedCards.Count) return true;
|
||||
Debug.LogWarning($"[IntentionSubmodule] Index {index} out of range (count: {intendedCards.Count}).");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 41e6e47005c77fb4994040c6b9763669
|
||||
@@ -14,7 +14,6 @@ namespace Continentis.MainGame.Character
|
||||
public GameObject mainView;
|
||||
public Animator animator;
|
||||
public AnimatorPlus2D animatorPlus2D;
|
||||
public SerializableDictionary<string, AnimationClip> animations;
|
||||
|
||||
public Collider selector;
|
||||
|
||||
@@ -26,7 +25,16 @@ namespace Continentis.MainGame.Character
|
||||
|
||||
public List<SpriteRenderer> spriteRenderers;
|
||||
public List<Material> materials;
|
||||
|
||||
/// <summary>
|
||||
/// 当前使用的动画驱动器,由 Initialize 时自动检测。
|
||||
/// 外部通过此属性调用 PlayAction / ReturnToIdle 等方法。
|
||||
/// </summary>
|
||||
public ICharacterAnimator CharacterAnimator { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// 初始化角色视图:收集 SpriteRenderer / Material,自动检测并初始化动画驱动器。
|
||||
/// </summary>
|
||||
public void Initialize(CharacterBase character)
|
||||
{
|
||||
this.character = character;
|
||||
@@ -39,21 +47,18 @@ namespace Continentis.MainGame.Character
|
||||
}
|
||||
SetOutline(false);
|
||||
|
||||
animations = new SerializableDictionary<string, AnimationClip>();
|
||||
|
||||
foreach (KeyValuePair<string, AnimationClip> anim in character.data.animations)
|
||||
// 自动检测动画驱动器:优先级 Spine > Frame > Static
|
||||
CharacterAnimator = GetComponent<SpineAnimator>() as ICharacterAnimator
|
||||
?? GetComponent<FrameAnimator>() as ICharacterAnimator
|
||||
?? GetComponent<StaticSpriteAnimator>() as ICharacterAnimator;
|
||||
|
||||
if (CharacterAnimator != null)
|
||||
{
|
||||
animations.Add(anim.Key, anim.Value);
|
||||
}
|
||||
|
||||
if (animations.TryGetValue("Idle", out AnimationClip idle))
|
||||
{
|
||||
animatorPlus2D.defaultIdleClip = idle;
|
||||
animatorPlus2D.Initialize();
|
||||
CharacterAnimator.InitializeAnimator(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"No Idle animation found for character {character.data.displayName}");
|
||||
Debug.LogWarning($"[CharacterView] 角色 '{character.data.displayName}' 未挂载任何 ICharacterAnimator 实现,无动画驱动器。");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
108
Assets/Scripts/MainGame/Character/CharacterView/FrameAnimator.cs
Normal file
108
Assets/Scripts/MainGame/Character/CharacterView/FrameAnimator.cs
Normal file
@@ -0,0 +1,108 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using AnimatorPlus;
|
||||
using SLSFramework.General;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Continentis.MainGame.Character
|
||||
{
|
||||
/// <summary>
|
||||
/// Tier 2 动画驱动器:帧动画方案,基于 AnimatorPlus2D + AnimationClip。
|
||||
/// 通过 CharacterData.animations 映射表驱动 Sprite Sheet 帧动画。
|
||||
/// 适合有完整帧动画序列的像素风角色(如 PixelFantasy 系列)。
|
||||
/// </summary>
|
||||
public class FrameAnimator : MonoBehaviour, ICharacterAnimator
|
||||
{
|
||||
private AnimatorPlus2D _animatorPlus;
|
||||
private SerializableDictionary<string, AnimationClip> _animations;
|
||||
private Coroutine _completionCoroutine;
|
||||
|
||||
// ── ICharacterAnimator ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 初始化:从 CombatCharacterViewBase 获取 AnimatorPlus2D 引用和动画映射表。
|
||||
/// </summary>
|
||||
public void InitializeAnimator(CombatCharacterViewBase view)
|
||||
{
|
||||
_animatorPlus = view.animatorPlus2D;
|
||||
|
||||
// 从 CharacterData 拷贝动画映射
|
||||
_animations = new SerializableDictionary<string, AnimationClip>();
|
||||
foreach (KeyValuePair<string, AnimationClip> pair in view.character.data.animations)
|
||||
{
|
||||
_animations.Add(pair.Key, pair.Value);
|
||||
}
|
||||
|
||||
// 设置 Idle 并初始化 Playable Graph
|
||||
if (_animations.TryGetValue("Idle", out AnimationClip idle))
|
||||
{
|
||||
_animatorPlus.defaultIdleClip = idle;
|
||||
_animatorPlus.Initialize();
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogError($"[FrameAnimator] 角色 '{view.character.data.displayName}' 缺少 Idle 动画,无法初始化。");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 播放指定动作的 AnimationClip,播完自动回 Idle 并触发回调。
|
||||
/// </summary>
|
||||
public void PlayAction(string actionName, float speed = 1f, Action onComplete = null)
|
||||
{
|
||||
StopCompletionCoroutine();
|
||||
|
||||
if (!_animations.TryGetValue(actionName, out AnimationClip clip))
|
||||
{
|
||||
Debug.LogWarning($"[FrameAnimator] 找不到动画 '{actionName}',跳过播放。");
|
||||
onComplete?.Invoke();
|
||||
return;
|
||||
}
|
||||
|
||||
_animatorPlus.Play(clip, speed);
|
||||
|
||||
// 动画播完后触发回调(AnimatorPlus2D 自动回 Idle,此处仅等待时长)
|
||||
if (onComplete != null)
|
||||
{
|
||||
float duration = clip.length / Mathf.Max(speed, 0.01f);
|
||||
_completionCoroutine = StartCoroutine(WaitForCompletion(duration, onComplete));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 立即停止当前动作,切回 Idle。
|
||||
/// </summary>
|
||||
public void ReturnToIdle()
|
||||
{
|
||||
StopCompletionCoroutine();
|
||||
_animatorPlus.Stop();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 暂停或恢复 PlayableGraph。
|
||||
/// </summary>
|
||||
public void SetPause(bool isPaused)
|
||||
{
|
||||
_animatorPlus.SetPause(isPaused);
|
||||
}
|
||||
|
||||
// ── 内部逻辑 ────────────────────────────────────────────────────────
|
||||
|
||||
private IEnumerator WaitForCompletion(float duration, Action onComplete)
|
||||
{
|
||||
yield return new WaitForSeconds(duration);
|
||||
onComplete?.Invoke();
|
||||
_completionCoroutine = null;
|
||||
}
|
||||
|
||||
private void StopCompletionCoroutine()
|
||||
{
|
||||
if (_completionCoroutine != null)
|
||||
{
|
||||
StopCoroutine(_completionCoroutine);
|
||||
_completionCoroutine = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 63273f95cd581594d85ac454f2395265
|
||||
@@ -0,0 +1,36 @@
|
||||
using System;
|
||||
|
||||
namespace Continentis.MainGame.Character
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色动画驱动器统一接口。
|
||||
/// 由具体的 MonoBehaviour 实现,挂载在角色 Prefab 上,
|
||||
/// CombatCharacterViewBase 在初始化时自动检测并持有引用。
|
||||
/// </summary>
|
||||
public interface ICharacterAnimator
|
||||
{
|
||||
/// <summary>
|
||||
/// 初始化动画驱动器,由 CombatCharacterViewBase.Initialize() 调用。
|
||||
/// </summary>
|
||||
void InitializeAnimator(CombatCharacterViewBase view);
|
||||
|
||||
/// <summary>
|
||||
/// 播放指定名称的动作(如 "Attack"、"Hit"、"Skill")。
|
||||
/// 播放完毕后自动回到 Idle,并触发 onComplete 回调。
|
||||
/// </summary>
|
||||
/// <param name="actionName">动作名称,需与 CharacterData.animations 的 Key 一致。</param>
|
||||
/// <param name="speed">播放速度倍率,默认 1.0。</param>
|
||||
/// <param name="onComplete">动作播放完毕后的回调,可为 null。</param>
|
||||
void PlayAction(string actionName, float speed = 1f, Action onComplete = null);
|
||||
|
||||
/// <summary>
|
||||
/// 立即切回 Idle 状态。
|
||||
/// </summary>
|
||||
void ReturnToIdle();
|
||||
|
||||
/// <summary>
|
||||
/// 暂停或恢复动画播放。
|
||||
/// </summary>
|
||||
void SetPause(bool isPaused);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6a4fe7995a9658f4580e2984545da6ff
|
||||
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Continentis.MainGame.Character
|
||||
{
|
||||
/// <summary>
|
||||
/// Tier 3 动画驱动器:Spine 骨骼动画(预留接口)。
|
||||
/// 美术资源到位后,接入 Spine-Unity Runtime 的 SkeletonAnimation API。
|
||||
///
|
||||
/// 预期实现思路:
|
||||
/// - InitializeAnimator:获取 SkeletonAnimation 引用,设置 Idle 动画
|
||||
/// - PlayAction:调用 SkeletonAnimation.AnimationState.SetAnimation()
|
||||
/// - 利用 Spine 事件系统触发攻击命中帧、特效帧等
|
||||
/// - 支持动画混合(如上半身攻击 + 下半身移动)
|
||||
/// </summary>
|
||||
public class SpineAnimator : MonoBehaviour, ICharacterAnimator
|
||||
{
|
||||
// TODO: Spine Runtime 引用
|
||||
// private SkeletonAnimation _skeleton;
|
||||
|
||||
public void InitializeAnimator(CombatCharacterViewBase view)
|
||||
{
|
||||
Debug.LogWarning("[SpineAnimator] Spine 动画驱动器尚未实装,当前为占位。");
|
||||
// TODO: 获取 SkeletonAnimation 组件
|
||||
// _skeleton = view.mainView.GetComponent<SkeletonAnimation>();
|
||||
// _skeleton.AnimationState.SetAnimation(0, "idle", true);
|
||||
}
|
||||
|
||||
public void PlayAction(string actionName, float speed = 1f, Action onComplete = null)
|
||||
{
|
||||
Debug.LogWarning($"[SpineAnimator] PlayAction('{actionName}') 未实装。");
|
||||
// TODO:
|
||||
// var entry = _skeleton.AnimationState.SetAnimation(0, actionName, false);
|
||||
// entry.TimeScale = speed;
|
||||
// entry.Complete += _ => {
|
||||
// _skeleton.AnimationState.SetAnimation(0, "idle", true);
|
||||
// onComplete?.Invoke();
|
||||
// };
|
||||
onComplete?.Invoke();
|
||||
}
|
||||
|
||||
public void ReturnToIdle()
|
||||
{
|
||||
// TODO: _skeleton.AnimationState.SetAnimation(0, "idle", true);
|
||||
}
|
||||
|
||||
public void SetPause(bool isPaused)
|
||||
{
|
||||
// TODO: _skeleton.timeScale = isPaused ? 0f : 1f;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d607d72147dad7441aca976ff675e6f2
|
||||
@@ -0,0 +1,176 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using DG.Tweening;
|
||||
using SLSFramework.General;
|
||||
using UnityEngine;
|
||||
|
||||
namespace Continentis.MainGame.Character
|
||||
{
|
||||
/// <summary>
|
||||
/// Tier 1 动画驱动器:静态图片切换 + DOTween 位移。
|
||||
/// 不使用 AnimationClip,通过切换 SpriteRenderer.sprite 表现不同动作。
|
||||
/// 适合尚无帧动画资源的角色,或仅有立绘/静态图片的简易角色。
|
||||
/// </summary>
|
||||
public class StaticSpriteAnimator : MonoBehaviour, ICharacterAnimator
|
||||
{
|
||||
[Header("Sprite 映射")]
|
||||
[Tooltip("动作名称 → Sprite 映射表,Key 需与 CharacterData.animations 的 Key 对齐(如 Idle、Attack、Hit)")]
|
||||
public SerializableDictionary<string, Sprite> actionSprites;
|
||||
|
||||
[Header("DOTween 参数")]
|
||||
[Tooltip("攻击时前冲距离")]
|
||||
[SerializeField] private float attackLungeDistance = 0.5f;
|
||||
|
||||
[Tooltip("攻击前冲时长")]
|
||||
[SerializeField] private float attackLungeDuration = 0.15f;
|
||||
|
||||
[Tooltip("受击抖动强度")]
|
||||
[SerializeField] private float hitShakeStrength = 0.3f;
|
||||
|
||||
[Tooltip("受击抖动时长")]
|
||||
[SerializeField] private float hitShakeDuration = 0.2f;
|
||||
|
||||
private SpriteRenderer _mainSpriteRenderer;
|
||||
private Sprite _idleSprite;
|
||||
private Vector3 _originalLocalPosition;
|
||||
private Tween _currentTween;
|
||||
|
||||
// ── ICharacterAnimator ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 初始化:缓存主 SpriteRenderer 和 Idle Sprite。
|
||||
/// </summary>
|
||||
public void InitializeAnimator(CombatCharacterViewBase view)
|
||||
{
|
||||
_mainSpriteRenderer = view.mainView.GetComponent<SpriteRenderer>();
|
||||
if (_mainSpriteRenderer == null)
|
||||
_mainSpriteRenderer = view.mainView.GetComponentInChildren<SpriteRenderer>();
|
||||
|
||||
_originalLocalPosition = view.mainView.transform.localPosition;
|
||||
|
||||
// 尝试从映射表获取 Idle sprite,回退为当前显示的 sprite
|
||||
_idleSprite = actionSprites != null && actionSprites.TryGetValue("Idle", out Sprite idle)
|
||||
? idle
|
||||
: _mainSpriteRenderer != null ? _mainSpriteRenderer.sprite : null;
|
||||
|
||||
// 应用 Idle Sprite
|
||||
if (_mainSpriteRenderer != null && _idleSprite != null)
|
||||
_mainSpriteRenderer.sprite = _idleSprite;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 播放动作:切换 Sprite + DOTween 动效,完毕后自动回 Idle。
|
||||
/// </summary>
|
||||
public void PlayAction(string actionName, float speed = 1f, Action onComplete = null)
|
||||
{
|
||||
KillCurrentTween();
|
||||
|
||||
// 切换 Sprite
|
||||
if (_mainSpriteRenderer != null && actionSprites != null &&
|
||||
actionSprites.TryGetValue(actionName, out Sprite sprite))
|
||||
{
|
||||
_mainSpriteRenderer.sprite = sprite;
|
||||
}
|
||||
|
||||
// 根据动作类型执行 DOTween 动效
|
||||
float adjustedDuration = 1f / Mathf.Max(speed, 0.01f);
|
||||
Transform mainTransform = _mainSpriteRenderer != null
|
||||
? _mainSpriteRenderer.transform
|
||||
: transform;
|
||||
|
||||
switch (actionName)
|
||||
{
|
||||
case "Attack":
|
||||
case "Skill":
|
||||
PlayLungeAnimation(mainTransform, adjustedDuration, onComplete);
|
||||
break;
|
||||
|
||||
case "Hit":
|
||||
PlayShakeAnimation(mainTransform, adjustedDuration, onComplete);
|
||||
break;
|
||||
|
||||
default:
|
||||
// 无特殊动效,短暂延迟后回 Idle
|
||||
float holdDuration = DefaultActionHoldDuration * adjustedDuration;
|
||||
_currentTween = DOVirtual.DelayedCall(holdDuration, () =>
|
||||
{
|
||||
ReturnToIdle();
|
||||
onComplete?.Invoke();
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 立即切回 Idle Sprite 并复位位置。
|
||||
/// </summary>
|
||||
public void ReturnToIdle()
|
||||
{
|
||||
KillCurrentTween();
|
||||
|
||||
if (_mainSpriteRenderer != null && _idleSprite != null)
|
||||
_mainSpriteRenderer.sprite = _idleSprite;
|
||||
|
||||
if (_mainSpriteRenderer != null)
|
||||
_mainSpriteRenderer.transform.localPosition = _originalLocalPosition;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 暂停或恢复当前 Tween。
|
||||
/// </summary>
|
||||
public void SetPause(bool isPaused)
|
||||
{
|
||||
if (_currentTween != null && _currentTween.IsActive())
|
||||
{
|
||||
if (isPaused) _currentTween.Pause();
|
||||
else _currentTween.Play();
|
||||
}
|
||||
}
|
||||
|
||||
// ── 内部动效 ────────────────────────────────────────────────────────
|
||||
|
||||
private const float DefaultActionHoldDuration = 0.3f;
|
||||
|
||||
private void PlayLungeAnimation(Transform target, float durationScale, Action onComplete)
|
||||
{
|
||||
float duration = attackLungeDuration * durationScale;
|
||||
Vector3 lungeOffset = target.right * attackLungeDistance;
|
||||
|
||||
_currentTween = DOTween.Sequence()
|
||||
.Append(target.DOLocalMove(_originalLocalPosition + lungeOffset, duration).SetEase(Ease.OutQuad))
|
||||
.Append(target.DOLocalMove(_originalLocalPosition, duration).SetEase(Ease.InQuad))
|
||||
.OnComplete(() =>
|
||||
{
|
||||
ReturnToIdle();
|
||||
onComplete?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
private void PlayShakeAnimation(Transform target, float durationScale, Action onComplete)
|
||||
{
|
||||
float duration = hitShakeDuration * durationScale;
|
||||
|
||||
_currentTween = target.DOShakePosition(duration, hitShakeStrength, vibrato: 10, randomness: 90, fadeOut: true)
|
||||
.OnComplete(() =>
|
||||
{
|
||||
target.localPosition = _originalLocalPosition;
|
||||
ReturnToIdle();
|
||||
onComplete?.Invoke();
|
||||
});
|
||||
}
|
||||
|
||||
private void KillCurrentTween()
|
||||
{
|
||||
if (_currentTween != null && _currentTween.IsActive())
|
||||
{
|
||||
_currentTween.Kill();
|
||||
_currentTween = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnDestroy()
|
||||
{
|
||||
KillCurrentTween();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 665a1fe9fd0c77c46b1a8567c185f7ab
|
||||
@@ -5,14 +5,26 @@ namespace Continentis.MainGame.Character
|
||||
{
|
||||
public partial class CombatNPC : CharacterBase
|
||||
{
|
||||
/// <summary>当前阶段编号,从 0 开始。Boss Logic 在 OnPhaseChange 中推进此值。</summary>
|
||||
public int currentPhase;
|
||||
|
||||
public CombatNPC(CharacterData data, Fraction fraction) : base(data, fraction)
|
||||
{
|
||||
|
||||
currentPhase = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public partial class CombatNPC
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 阶段切换时由 <see cref="CharacterLogicBase.OnHealthThreshold"/> 调用。
|
||||
/// 子类(Boss Logic)重写此方法以改变可用 Intention 集合、播放阶段动画等。
|
||||
/// </summary>
|
||||
public virtual void OnPhaseChange(int newPhase)
|
||||
{
|
||||
Debug.Log($"[Combat] {data.displayName} 进入阶段 {newPhase}");
|
||||
// TODO: 阶段切换动画/台词,待动画系统完善后替换 Debug.Log
|
||||
currentPhase = newPhase;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user