389 lines
14 KiB
C#
389 lines
14 KiB
C#
using System;
|
||
using System.Text;
|
||
using Cielonos.MainGame.Buffs.Character;
|
||
using Cielonos.MainGame.Inventory;
|
||
using DamageNumbersPro;
|
||
using SickscoreGames.HUDNavigationSystem;
|
||
using Sirenix.OdinInspector;
|
||
using SLSUtilities.General;
|
||
using SLSUtilities.WwiseAssistance;
|
||
using SLSUtilities.FunctionalAnimation;
|
||
using UniRx;
|
||
using UnityEngine;
|
||
using UnityEngine.AI;
|
||
using UnityEngine.Serialization;
|
||
|
||
namespace Cielonos.MainGame.Characters
|
||
{
|
||
public enum Fraction
|
||
{
|
||
Player = 0,
|
||
AlliedMinion = 1,
|
||
Enemy = 10,
|
||
Neutral = 20
|
||
}
|
||
|
||
public partial class CharacterBase : SerializedMonoBehaviour, IFuncAnimExecutor
|
||
{
|
||
public Fraction fraction;
|
||
public Transform centerPoint => bodyPartsSc.flexibleCenterPoint;
|
||
public Vector3 centerPosition => centerPoint.position;
|
||
|
||
[TitleGroup("Data")]
|
||
public AttributeData attributeData;
|
||
public VFXData vfxData;
|
||
public BlockData blockData;
|
||
|
||
[TitleGroup("Submodules")] [HideInEditorMode]
|
||
public SelfTimeSubmodule selfTimeSm;
|
||
|
||
[HideInEditorMode] public AttributeSubmodule attributeSm;
|
||
[HideInEditorMode] public EventSubmodule eventSm;
|
||
[HideInEditorMode] public BuffSubmodule buffSm;
|
||
[HideInEditorMode] public StatusSubmodule statusSm;
|
||
|
||
[TitleGroup("Subcontrollers")] public CollisionSubcontrollerBase collisionSc;
|
||
public MovementSubcontrollerBase movementSc;
|
||
public AnimationSubcontrollerBase animationSc;
|
||
public RenderSubcontrollerBase renderSc;
|
||
public BodyPartsSubcontroller bodyPartsSc;
|
||
public AudioSubcontroller audioSc;
|
||
public ReactionSubcontroller reactionSc;
|
||
public FeedbackSubcontroller feedbackSc;
|
||
|
||
[TitleGroup("Navigation")] public HUDNavigationElement navigationElement;
|
||
|
||
protected void Awake()
|
||
{
|
||
InitializeSubmodules();
|
||
InitializeSubcontrollers();
|
||
}
|
||
|
||
protected virtual void Start()
|
||
{
|
||
selfTimeSm?.SetUp(this);
|
||
|
||
|
||
}
|
||
|
||
protected virtual void Update()
|
||
{
|
||
selfTimeSm.Update();
|
||
buffSm.Update();
|
||
}
|
||
|
||
public virtual void Die()
|
||
{
|
||
Destroy(gameObject); //TODO: 后续改为死亡动画+回收
|
||
}
|
||
}
|
||
|
||
public partial class CharacterBase
|
||
{
|
||
protected virtual void InitializeSubmodules()
|
||
{
|
||
selfTimeSm ??= new SelfTimeSubmodule(this);
|
||
attributeSm ??= new AttributeSubmodule(this);
|
||
eventSm ??= new EventSubmodule(this);
|
||
buffSm ??= new BuffSubmodule(this);
|
||
statusSm ??= new StatusSubmodule(this);
|
||
|
||
RegisterAttributeCallbacks();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 注册属性变更回调,使 Health/Energy 变更时自动触发对应的 EventSubmodule 事件。
|
||
/// 子类可 override 追加额外回调(如 UI 更新)。
|
||
/// </summary>
|
||
protected virtual void RegisterAttributeCallbacks()
|
||
{
|
||
attributeSm.RegisterValueChangedCallback(CharacterAttribute.Health,
|
||
(oldVal, newVal) => eventSm.onHealthChanged.Invoke(newVal - oldVal));
|
||
|
||
attributeSm.RegisterValueChangedCallback(CharacterAttribute.Energy,
|
||
(oldVal, newVal) => eventSm.onEnergyChanged.Invoke(newVal - oldVal));
|
||
}
|
||
|
||
protected virtual void InitializeSubcontrollers()
|
||
{
|
||
renderSc?.Initialize();
|
||
movementSc?.Initialize();
|
||
animationSc?.Initialize();
|
||
collisionSc?.Initialize();
|
||
bodyPartsSc?.Initialize();
|
||
audioSc?.Initialize();
|
||
reactionSc?.Initialize();
|
||
feedbackSc?.Initialize();
|
||
}
|
||
}
|
||
|
||
public partial class CharacterBase
|
||
{
|
||
/// <summary>
|
||
/// 护甲减伤公式的缩放常量。armor / (armor + K) = 减伤比例。
|
||
/// <para> K = 100 时,100 护甲 = 50% 减伤,200 护甲 ≈ 66.7% 减伤。 </para>
|
||
/// </summary>
|
||
private const float ArmorScalingConstant = 100f;
|
||
|
||
/// <summary>
|
||
/// 根据攻击者属性和自身属性计算最终伤害值。
|
||
/// </summary>
|
||
/// <param name="attacker">攻击来源角色,为 null 时跳过攻击者侧的伤害倍率。</param>
|
||
/// <param name="attackValue">攻击数值参数。</param>
|
||
public float GetDamageValue(CharacterBase attacker, Attack.Value attackValue)
|
||
{
|
||
string dealtMultiplier = attackValue.type.AttackTypeToString() + "DamageDealtMultiplier";
|
||
string receivedMultiplier = attackValue.type.AttackTypeToString() + "DamageReceivedMultiplier";
|
||
|
||
float baseDamage = attackValue.damage;
|
||
|
||
// Phase 1: 攻击者侧倍率
|
||
baseDamage *= attacker is not null ? attacker.attributeSm[dealtMultiplier] : 1;
|
||
baseDamage *= attacker is not null ? attacker.attributeSm[CharacterAttribute.FinalDamageDealtMultiplier] : 1;
|
||
|
||
// Phase 2: 护甲减伤(非线性,先于 ReceivedMultiplier)
|
||
float armor = attributeSm[CharacterAttribute.Armor];
|
||
if (armor > 0f)
|
||
{
|
||
float armorReduction = armor / (armor + ArmorScalingConstant);
|
||
baseDamage *= 1f - armorReduction;
|
||
}
|
||
else
|
||
{
|
||
// 负护甲增加伤害,线性叠加,每 -100 护甲增加 100% 伤害
|
||
float negativeArmor = Mathf.Abs(armor);
|
||
float negativeArmorIncrease = negativeArmor / ArmorScalingConstant;
|
||
baseDamage *= 1f + negativeArmorIncrease;
|
||
}
|
||
|
||
// Phase 3: 受击者侧倍率
|
||
baseDamage *= attributeSm[receivedMultiplier];
|
||
baseDamage *= attributeSm[CharacterAttribute.FinalDamageReceivedMultiplier];
|
||
|
||
return (baseDamage + attackValue.additionalFlatDamage) * attackValue.damageMultiplier;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 对角色施加伤害,填充 attackResult 中的最终伤害、护盾吸收和死亡标记。
|
||
/// </summary>
|
||
public void TakeDamage(Attack.Result attackResult)
|
||
{
|
||
float damage = GetDamageValue(attackResult.attacker, attackResult.value);
|
||
if (attributeSm.Has(CharacterAttribute.Shield) && attributeSm[CharacterAttribute.Shield] > 0)
|
||
{
|
||
attackResult.shieldBlockedDamage = Mathf.Min(damage, attributeSm[CharacterAttribute.Shield]);
|
||
|
||
if (damage > attributeSm[CharacterAttribute.Shield])
|
||
{
|
||
damage -= attributeSm[CharacterAttribute.Shield];
|
||
attributeSm[CharacterAttribute.Shield] = 0;
|
||
}
|
||
else
|
||
{
|
||
attributeSm[CharacterAttribute.Shield] -= damage;
|
||
damage = 0;
|
||
}
|
||
}
|
||
|
||
attributeSm[CharacterAttribute.Health] -= damage;
|
||
attackResult.finalDamage = damage;
|
||
|
||
// 实际扣除了生命值才算受伤,被盾完全抵挡则不触发
|
||
if (damage > 0)
|
||
{
|
||
// 通过 EventSubmodule.onHurt 统一分发;无攻击区域来源(Buff路径)时传 null
|
||
eventSm.onHurt.Invoke(null);
|
||
}
|
||
|
||
if (attributeSm[CharacterAttribute.Health] <= 0)
|
||
{
|
||
attackResult.causedDeath = true;
|
||
attributeSm[CharacterAttribute.Health] = 0;
|
||
Die();
|
||
}
|
||
else
|
||
{
|
||
attackResult.causedDeath = false;
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 对角色施加伤害,并生成伤害数字。适用于有明确攻击来源和攻击数值的情况,如直接攻击;
|
||
/// 对于持续伤害等无明确攻击来源的情况,建议先调用 TakeDamage(Attack.Result) 再手动生成伤害数字,以避免重复计算伤害值。
|
||
/// </summary>
|
||
public void TakeDamage(Attack.Result attackResult, out DamageNumber damageNumber)
|
||
{
|
||
TakeDamage(attackResult);
|
||
|
||
// 护盾吸收伤害单独显示
|
||
if (attackResult.shieldBlockedDamage > 0)
|
||
{
|
||
MainGameBaseCollection.Instance.ShieldedDamageNumber()
|
||
.Spawn(attackResult.hitPosition, attackResult.shieldBlockedDamage, centerPoint);
|
||
}
|
||
|
||
Attack.Type type = attackResult.value.type;
|
||
bool isCritical = attackResult.value.isCritical;
|
||
damageNumber = MainGameBaseCollection.Instance.DamageNumber(type, isCritical)
|
||
.Spawn(attackResult.hitPosition, attackResult.finalDamage, attackResult.target.centerPoint);
|
||
damageNumber.SetSpamGroup(attackResult.spamGroupID);
|
||
}
|
||
|
||
public virtual void Heal(float healAmount)
|
||
{
|
||
if (healAmount <= 0) return;
|
||
|
||
attributeSm[CharacterAttribute.Health] += healAmount;
|
||
attributeSm[CharacterAttribute.Health] = Mathf.Min(attributeSm[CharacterAttribute.Health], attributeSm[CharacterAttribute.MaximumHealth]);
|
||
MainGameBaseCollection.Instance.HealText().Spawn(centerPosition, healAmount, centerPoint);
|
||
}
|
||
}
|
||
|
||
public partial class CharacterBase
|
||
{
|
||
public virtual bool CheckBreakthrough(Breakthrough.Type breakthroughType)
|
||
{
|
||
return !reactionSc.breakthroughResistances[breakthroughType].Value;
|
||
}
|
||
|
||
public virtual bool CheckDisruption(DisruptionType disruptionType)
|
||
{
|
||
return animationSc.fullBodyFuncAnimSm.CheckDisruption(disruptionType);
|
||
}
|
||
|
||
public virtual bool GetHit(Breakthrough.Type breakthroughType, out float recoveryTime,
|
||
DisruptionType disruptionType = DisruptionType.NormalExternal,
|
||
Vector3 direction = default, string funcAnimName = "")
|
||
{
|
||
renderSc.GetHitBlink();
|
||
|
||
float intensity = breakthroughType switch
|
||
{
|
||
Breakthrough.Type.None => 0,
|
||
Breakthrough.Type.Weak => 0.2f,
|
||
Breakthrough.Type.Medium => 0.4f,
|
||
Breakthrough.Type.Heavy or Breakthrough.Type.Disruption or Breakthrough.Type.Forced => 0.8f,
|
||
_ => 0
|
||
};
|
||
|
||
if (string.IsNullOrEmpty(funcAnimName))
|
||
{
|
||
funcAnimName = GetHitFuncAnimName(breakthroughType, direction);
|
||
}
|
||
|
||
if (CheckBreakthrough(breakthroughType))
|
||
{
|
||
if (!animationSc.fullBodyFuncAnimSm.Stop(disruptionType))
|
||
{
|
||
recoveryTime = 0f;
|
||
return true;
|
||
}
|
||
|
||
if (breakthroughType >= Breakthrough.Type.Medium)
|
||
{
|
||
animationSc.PlayGetHitAnimation(funcAnimName, out recoveryTime);
|
||
}
|
||
else
|
||
{
|
||
recoveryTime = 0f;
|
||
animationSc.PlayGetHitBoneShake(intensity, direction);
|
||
}
|
||
|
||
statusSm.AddStatus(StatusType.Stun, recoveryTime);
|
||
return true;
|
||
}
|
||
|
||
recoveryTime = 0f;
|
||
animationSc.PlayGetHitBoneShake(intensity, direction);
|
||
return false;
|
||
}
|
||
|
||
protected virtual string GetHitFuncAnimName(Breakthrough.Type breakthroughType, Vector3 direction)
|
||
{
|
||
string prefix = "GetHitMedium";
|
||
|
||
if (breakthroughType >= Breakthrough.Type.Medium)
|
||
{
|
||
prefix = breakthroughType switch
|
||
{
|
||
Breakthrough.Type.Medium => "GetHitMedium",
|
||
Breakthrough.Type.Heavy => "GetHitHeavy",
|
||
Breakthrough.Type.Disruption => "GetHitHeavy",
|
||
Breakthrough.Type.Forced => "GetHitForced",
|
||
_ => "GetHit"
|
||
};
|
||
}
|
||
|
||
string directionStr = "Front";
|
||
if (direction != default)
|
||
{
|
||
float angle = Vector3.SignedAngle(transform.forward, direction, Vector3.up);
|
||
directionStr = angle switch
|
||
{
|
||
> -45f and <= 45f => "Back",
|
||
> 45f and <= 135f => "Left",
|
||
> -135f and <= -45f => "Right",
|
||
_ => "Front"
|
||
};
|
||
|
||
if (directionStr != "Front" && !animationSc.fullBodyFuncAnimSm.collection.ContainsKey(prefix + directionStr))
|
||
{
|
||
directionStr = "Front";
|
||
}
|
||
}
|
||
|
||
string fullName = prefix + directionStr;
|
||
|
||
if (!animationSc.fullBodyFuncAnimSm.collection.ContainsKey(fullName))
|
||
{
|
||
if (prefix == "GetHitForced")
|
||
{
|
||
prefix = "GetHitHeavy"; //如果没有专门的“强制等级”受击动画,就使用“重度等级”的动画
|
||
fullName = prefix + directionStr;
|
||
}
|
||
}
|
||
|
||
return fullName;
|
||
}
|
||
}
|
||
|
||
public partial class CharacterBase
|
||
{
|
||
public Vector2 GetNormalizedScreenPosition(Camera cam = null)
|
||
{
|
||
if (this is Player player)
|
||
{
|
||
cam ??= player.viewSc.playerCamera;
|
||
}
|
||
else
|
||
{
|
||
if (cam == null)
|
||
{
|
||
throw new ArgumentNullException(nameof(cam), "Camera must be provided for non-player characters.");
|
||
}
|
||
}
|
||
|
||
return SpaceConverter.WorldPointToNormalizedScreenPoint(centerPoint.position, cam);
|
||
}
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
public partial class CharacterBase
|
||
{
|
||
[Title("Editor Tools")]
|
||
[HideInPlayMode]
|
||
[Button]
|
||
protected virtual void CollectSubcontrollers()
|
||
{
|
||
movementSc ??= GetComponent<MovementSubcontrollerBase>();
|
||
animationSc ??= GetComponent<AnimationSubcontrollerBase>();
|
||
collisionSc ??= GetComponent<CollisionSubcontrollerBase>();
|
||
renderSc ??= GetComponent<RenderSubcontrollerBase>();
|
||
bodyPartsSc ??= GetComponent<BodyPartsSubcontroller>();
|
||
audioSc ??= GetComponent<AudioSubcontroller>();
|
||
reactionSc ??= GetComponent<ReactionSubcontroller>();
|
||
feedbackSc ??= GetComponent<FeedbackSubcontroller>();
|
||
}
|
||
}
|
||
#endif
|
||
} |