Files
Cielonos/Assets/Scripts/MainGame/Characters/Base/CharacterBase.cs
SoulliesOfficial b5cb6152ff MusicBeat
2026-05-26 00:21:27 -04:00

389 lines
14 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}