- Added DownDash ability with cooldown and speed configuration. - Introduced DownDashState to handle down dashing mechanics, including gravity manipulation and animation playback. - Updated PlayerMovement to support DownDash functionality. - Enhanced PlayerStats to manage spring charge consumption and healing. - Modified PlayerCombat and WeaponHitBoxInstance to support new hit confirmation events. - Updated AbilityType to include new form types for character abilities. - Improved Gizmos for better visualization of enemy detection and attack ranges. - Added feedback systems for form switching in PlayerFeedback and IFeedbackPlayer. - Refactored combat and movement states to accommodate new abilities and ensure smooth transitions.
370 lines
17 KiB
C#
370 lines
17 KiB
C#
using System.Collections.Generic;
|
||
using UnityEngine;
|
||
using BaseGames.Core;
|
||
using BaseGames.Core.Events;
|
||
using BaseGames.Core.Save;
|
||
|
||
namespace BaseGames.Player
|
||
{
|
||
/// <summary>
|
||
/// 玩家数值管理组件。负责 HP、灵魂、灵气、弹簧充能、LingZhu、能力解锁与存档读写。
|
||
/// 实现 <see cref="IRewardTarget"/> 供 RewardSO 使用,避免 Quest 程序集直接依赖 Player 程序集。
|
||
/// </summary>
|
||
public class PlayerStats : MonoBehaviour, ISaveable, BaseGames.Core.IRestoreOnSave, IRewardTarget, BaseGames.Core.ILingZhuProvider
|
||
{
|
||
[Header("配置")]
|
||
[SerializeField] private PlayerStatsSO _config;
|
||
|
||
[Header("事件频道")]
|
||
[SerializeField] private IntEventChannelSO _onHPChanged;
|
||
[SerializeField] private IntEventChannelSO _onMaxHPChanged;
|
||
[SerializeField] private IntEventChannelSO _onSoulPowerChanged;
|
||
[SerializeField] private IntEventChannelSO _onSpiritPowerChanged;
|
||
[SerializeField] private IntEventChannelSO _onSpringChargesChanged;
|
||
[SerializeField] private IntEventChannelSO _onLingZhuChanged;
|
||
[SerializeField] private AbilityTypeEventChannelSO _onAbilityUnlocked;
|
||
[SerializeField] private DifficultyChangedEventChannel _onDifficultyChanged;
|
||
|
||
// ── 运行时数值 ─────────────────────────────────────────────────────────
|
||
public int CurrentHP { get; private set; }
|
||
public int MaxHP { get; private set; }
|
||
public int CurrentSoulPower { get; private set; }
|
||
public int MaxSoulPower { get; private set; }
|
||
public int CurrentSpiritPower { get; private set; }
|
||
public int MaxSpiritPower { get; private set; }
|
||
public int CurrentSpringCharges { get; private set; }
|
||
public int MaxSpringCharges { get; private set; }
|
||
public int SpringKillPoints { get; private set; }
|
||
public int CurrentLingZhu { get; private set; }
|
||
private int _lifetimeLingZhu;
|
||
|
||
public bool IsInvincible => _invincibleTimer > 0f;
|
||
public bool IsAlive => CurrentHP > 0;
|
||
|
||
private float _invincibleTimer;
|
||
private float _spiritRegenTimer;
|
||
private AbilityType _unlockedAbilities = AbilityType.None;
|
||
private bool _isGodMode;
|
||
private readonly CompositeDisposable _subs = new();
|
||
|
||
#if UNITY_EDITOR
|
||
// ── 运行时调试(Inspector 中可见)───────────────────────────────
|
||
[Header("\u2500\u2500 \u8fd0\u884c\u65f6\u8c03\u8bd5 \u2500\u2500")]
|
||
[SerializeField] private string _dbg_HP;
|
||
[SerializeField] private string _dbg_Soul;
|
||
[SerializeField] private string _dbg_Spirit;
|
||
[SerializeField] private string _dbg_Spring;
|
||
[SerializeField] private bool _dbg_IsInvincible;
|
||
[SerializeField] private float _dbg_InvincibleTimer;
|
||
[SerializeField] private bool _dbg_GodMode;
|
||
[SerializeField] private string _dbg_Abilities;
|
||
#endif
|
||
|
||
// ── 护符属性修改器 ─────────────────────────────────────────────────────────
|
||
private readonly Dictionary<StatType, float> _flatModifiers = new();
|
||
private readonly Dictionary<StatType, float> _percentModifiers = new();
|
||
private float _animatorSpeedBonus = 0f;
|
||
private int _soulCostReduction = 0;
|
||
|
||
/// <summary>动画速度倍率(AttackSpeedEffect 调节,初始 1.0)。</summary>
|
||
public float AnimatorSpeedMultiplier => 1f + _animatorSpeedBonus;
|
||
|
||
/// <summary>法术灵力消耗减免(SoulSpellEffect 叠加)。</summary>
|
||
public int SoulCostReduction => _soulCostReduction;
|
||
|
||
private void Awake()
|
||
{
|
||
Debug.Assert(_config != null, "[PlayerStats] _config 未赋值,请在 Inspector 中指定 PlayerStatsSO。", this);
|
||
MaxHP = _config.MaxHP;
|
||
CurrentHP = MaxHP;
|
||
MaxSoulPower = _config.MaxSoulPower;
|
||
MaxSpiritPower = _config.MaxSpiritPower;
|
||
MaxSpringCharges = _config.MaxSpringCharges;
|
||
CurrentSpringCharges = MaxSpringCharges;
|
||
CurrentLingZhu = _config.InitialLingZhu;
|
||
_unlockedAbilities = _config.InitialAbilities;
|
||
}
|
||
|
||
private void OnEnable()
|
||
{
|
||
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Register(this);
|
||
_onDifficultyChanged?.Subscribe(HandleDifficultyChanged).AddTo(_subs);
|
||
}
|
||
|
||
private void OnDisable()
|
||
{
|
||
ServiceLocator.GetOrDefault<ISaveableRegistry>()?.Unregister(this);
|
||
_subs.Clear();
|
||
}
|
||
|
||
private void HandleDifficultyChanged(DifficultyLevel _)
|
||
{
|
||
var scaler = ServiceLocator.GetOrDefault<IDifficultyService>()?.CurrentScaler;
|
||
if (scaler == null) return;
|
||
// 按比例缩放当前 HP(架构 19 §5:难度切换时保持 HP 比例)
|
||
float hpRatio = MaxHP > 0 ? (float)CurrentHP / MaxHP : 1f;
|
||
MaxHP = Mathf.Max(1, Mathf.RoundToInt(_config.MaxHP * scaler.PlayerMaxHPMultiplier));
|
||
CurrentHP = Mathf.Clamp(Mathf.RoundToInt(MaxHP * hpRatio), IsAlive ? 1 : 0, MaxHP);
|
||
_onMaxHPChanged?.Raise(MaxHP);
|
||
_onHPChanged?.Raise(CurrentHP);
|
||
}
|
||
|
||
private void Update()
|
||
{
|
||
float dt = Time.deltaTime;
|
||
|
||
if (_invincibleTimer > 0f)
|
||
_invincibleTimer -= dt;
|
||
|
||
if (_config.SpiritRegenRate > 0)
|
||
{
|
||
_spiritRegenTimer += dt;
|
||
if (_spiritRegenTimer >= 1f)
|
||
{
|
||
_spiritRegenTimer -= 1f;
|
||
AddSpiritPower(_config.SpiritRegenRate);
|
||
}
|
||
}
|
||
|
||
#if UNITY_EDITOR
|
||
_dbg_HP = $"{CurrentHP} / {MaxHP}";
|
||
_dbg_Soul = $"{CurrentSoulPower} / {MaxSoulPower}";
|
||
_dbg_Spirit = $"{CurrentSpiritPower} / {MaxSpiritPower}";
|
||
_dbg_Spring = $"{CurrentSpringCharges} / {MaxSpringCharges}";
|
||
_dbg_IsInvincible = IsInvincible;
|
||
_dbg_InvincibleTimer = _invincibleTimer;
|
||
_dbg_GodMode = _isGodMode;
|
||
_dbg_Abilities = _unlockedAbilities == AbilityType.None ? "\u65e0" : _unlockedAbilities.ToString();
|
||
#endif
|
||
}
|
||
// ── 护符修改器 API ─────────────────────────────────────────────────────
|
||
|
||
/// <summary>叠加属性修改器(护符效果装备时调用)。</summary>
|
||
public void AddModifier(StatType stat, float flat, float percent)
|
||
{
|
||
_flatModifiers[stat] = _flatModifiers.GetValueOrDefault(stat) + flat;
|
||
_percentModifiers[stat] = _percentModifiers.GetValueOrDefault(stat) + percent;
|
||
if (stat == StatType.MaxHP) RecalcMaxHP();
|
||
}
|
||
|
||
/// <summary>移除属性修改器(护符效果卸下时调用)。</summary>
|
||
public void RemoveModifier(StatType stat, float flat, float percent)
|
||
{
|
||
_flatModifiers[stat] = _flatModifiers.GetValueOrDefault(stat) - flat;
|
||
_percentModifiers[stat] = _percentModifiers.GetValueOrDefault(stat) - percent;
|
||
if (stat == StatType.MaxHP) RecalcMaxHP();
|
||
}
|
||
|
||
/// <summary>查询指定属性的固定加成(由 PlayerMovement、战斗等外部系统查询)。</summary>
|
||
public float GetFlatModifier(StatType stat) => _flatModifiers.GetValueOrDefault(stat);
|
||
|
||
/// <summary>查询指定属性的百分比加成。</summary>
|
||
public float GetPercentModifier(StatType stat) => _percentModifiers.GetValueOrDefault(stat);
|
||
|
||
/// <summary>加载动画速度加成(AttackSpeedEffect 装备时调用)。</summary>
|
||
public void AddAnimatorSpeedBonus(float delta) => _animatorSpeedBonus += delta;
|
||
|
||
/// <summary>移除动画速度加成(AttackSpeedEffect 卸下时调用)。</summary>
|
||
public void RemoveAnimatorSpeedBonus(float delta) => _animatorSpeedBonus -= delta;
|
||
|
||
/// <summary>增加灵力消耗减免(SoulSpellEffect 装备时调用)。</summary>
|
||
public void AddSoulCostReduction(int amount) => _soulCostReduction += amount;
|
||
|
||
/// <summary>移除灵力消耗减免(SoulSpellEffect 卸下时调用)。</summary>
|
||
public void RemoveSoulCostReduction(int amount) => _soulCostReduction = Mathf.Max(0, _soulCostReduction - amount);
|
||
|
||
private void RecalcMaxHP()
|
||
{
|
||
float flat = _flatModifiers.GetValueOrDefault(StatType.MaxHP);
|
||
float percent = _percentModifiers.GetValueOrDefault(StatType.MaxHP);
|
||
int newMax = Mathf.Max(1, Mathf.RoundToInt(_config.MaxHP * (1f + percent) + flat));
|
||
SetMaxHP(newMax);
|
||
}
|
||
// ── HP ────────────────────────────────────────────────────────────────
|
||
/// <summary>受到伤害时触发(已扣血,不包含无敌/死亡情况)。</summary>
|
||
public event System.Action OnDamaged;
|
||
|
||
/// <summary>Debug:开启/关闭无敌模式(不计入无敌帧,永久生效直至关闭)。</summary>
|
||
public void SetGodMode(bool v) { _isGodMode = v; }
|
||
|
||
public void TakeDamage(int amount)
|
||
{
|
||
if (_isGodMode || IsInvincible || !IsAlive || amount <= 0) return;
|
||
CurrentHP = Mathf.Max(0, CurrentHP - amount);
|
||
_onHPChanged?.Raise(CurrentHP);
|
||
OnDamaged?.Invoke();
|
||
}
|
||
|
||
public void FullHeal()
|
||
{
|
||
if (!IsAlive) return;
|
||
CurrentHP = MaxHP;
|
||
_onHPChanged?.Raise(CurrentHP);
|
||
}
|
||
|
||
// ── IRestoreOnSave ────────────────────────────────────────────────────
|
||
void BaseGames.Core.IRestoreOnSave.FullRestore() => FullHeal();
|
||
void BaseGames.Core.IRestoreOnSave.RestoreSpring() => RestoreSpringCharges();
|
||
|
||
public void HealHP(int amount)
|
||
{
|
||
if (!IsAlive || amount <= 0) return;
|
||
CurrentHP = Mathf.Min(MaxHP, CurrentHP + amount);
|
||
_onHPChanged?.Raise(CurrentHP);
|
||
}
|
||
|
||
public void SetMaxHP(int newMax)
|
||
{
|
||
MaxHP = Mathf.Max(1, newMax);
|
||
CurrentHP = Mathf.Min(CurrentHP, MaxHP);
|
||
_onMaxHPChanged?.Raise(MaxHP);
|
||
_onHPChanged?.Raise(CurrentHP);
|
||
}
|
||
|
||
// ── Soul Power ────────────────────────────────────────────────────────
|
||
public void AddSoulPower(int amount)
|
||
{
|
||
if (amount <= 0) return;
|
||
CurrentSoulPower = Mathf.Min(MaxSoulPower, CurrentSoulPower + amount);
|
||
_onSoulPowerChanged?.Raise(CurrentSoulPower);
|
||
}
|
||
|
||
/// <summary>AddSoul 是 AddSoulPower 的别名(架构 06_CombatModule §8 ParrySystem 使用此名称)。</summary>
|
||
public void AddSoul(int amount) => AddSoulPower(amount);
|
||
|
||
public bool ConsumeSoulPower(int amount)
|
||
{
|
||
if (CurrentSoulPower < amount) return false;
|
||
CurrentSoulPower -= amount;
|
||
_onSoulPowerChanged?.Raise(CurrentSoulPower);
|
||
return true;
|
||
}
|
||
|
||
// ── Spirit Power ──────────────────────────────────────────────────────
|
||
public void AddSpiritPower(int amount)
|
||
{
|
||
if (amount <= 0) return;
|
||
CurrentSpiritPower = Mathf.Min(MaxSpiritPower, CurrentSpiritPower + amount);
|
||
_onSpiritPowerChanged?.Raise(CurrentSpiritPower);
|
||
}
|
||
|
||
public bool ConsumeSpiritPower(int amount)
|
||
{
|
||
if (CurrentSpiritPower < amount) return false;
|
||
CurrentSpiritPower -= amount;
|
||
_onSpiritPowerChanged?.Raise(CurrentSpiritPower);
|
||
return true;
|
||
}
|
||
|
||
// ── Spring ────────────────────────────────────────────────────────────
|
||
|
||
/// <summary>
|
||
/// 仅扣除灵泉充能(SpringState 进入前摇时调用)。
|
||
/// 回血需前摇结束后调用 <see cref="ApplySpringHeal"/>。
|
||
/// </summary>
|
||
public bool ConsumeSpringCharge()
|
||
{
|
||
if (CurrentSpringCharges <= 0) return false;
|
||
CurrentSpringCharges--;
|
||
_onSpringChargesChanged?.Raise(CurrentSpringCharges);
|
||
return true;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 执行灵泉回血(SpringState 前摇动画结束时调用)。
|
||
/// </summary>
|
||
public void ApplySpringHeal() => HealHP(_config.SpringHealAmount);
|
||
|
||
/// <summary>扣除充能并立即回血(兼容旧调用路径)。</summary>
|
||
public bool UseSpring()
|
||
{
|
||
if (!ConsumeSpringCharge()) return false;
|
||
ApplySpringHeal();
|
||
return true;
|
||
}
|
||
|
||
public void RestoreSpringCharges(int amount = -1)
|
||
{
|
||
if (amount < 0) amount = MaxSpringCharges;
|
||
CurrentSpringCharges = Mathf.Min(MaxSpringCharges, CurrentSpringCharges + amount);
|
||
_onSpringChargesChanged?.Raise(CurrentSpringCharges);
|
||
}
|
||
|
||
public void AddKillPoints(int points = 1)
|
||
{
|
||
SpringKillPoints += points;
|
||
if (SpringKillPoints >= _config.SpringKillThreshold)
|
||
{
|
||
SpringKillPoints = 0;
|
||
RestoreSpringCharges(1);
|
||
}
|
||
}
|
||
|
||
// ── LingZhu ─────────────────────────────────────────────────────────────────────
|
||
public void AddLingZhu(int amount)
|
||
{
|
||
if (amount <= 0) return;
|
||
CurrentLingZhu += amount;
|
||
_lifetimeLingZhu += amount;
|
||
_onLingZhuChanged?.Raise(CurrentLingZhu);
|
||
}
|
||
|
||
public bool SpendLingZhu(int amount)
|
||
{
|
||
if (CurrentLingZhu < amount) return false;
|
||
CurrentLingZhu -= amount;
|
||
_onLingZhuChanged?.Raise(CurrentLingZhu);
|
||
return true;
|
||
}
|
||
|
||
// ── Invincibility ─────────────────────────────────────────────────────
|
||
public void BeginInvincibility(float duration = -1f)
|
||
{
|
||
float d = duration >= 0f ? duration : _config.InvincibilityDuration;
|
||
_invincibleTimer = Mathf.Max(_invincibleTimer, d);
|
||
}
|
||
|
||
// ── Abilities ─────────────────────────────────────────────────────────
|
||
public bool HasAbility(AbilityType ability)
|
||
=> (_unlockedAbilities & ability) == ability;
|
||
|
||
public void UnlockAbility(AbilityType ability)
|
||
{
|
||
if (HasAbility(ability)) return;
|
||
_unlockedAbilities |= ability;
|
||
_onAbilityUnlocked?.Raise(ability);
|
||
}
|
||
|
||
/// <summary>IRewardTarget 实现:以 uint 位掩码解锁能力(避免跨程序集枚举引用)。</summary>
|
||
void IRewardTarget.UnlockAbilityFlag(uint abilityFlag) => UnlockAbility((AbilityType)abilityFlag);
|
||
|
||
public void LockAbility(AbilityType ability)
|
||
=> _unlockedAbilities &= ~ability;
|
||
|
||
// ── ISaveable ─────────────────────────────────────────────────────────
|
||
public void OnSave(SaveData saveData)
|
||
{
|
||
var p = saveData.Player;
|
||
p.CurrentHP = CurrentHP;
|
||
p.MaxHP = MaxHP;
|
||
p.CurrentLingZhu = CurrentLingZhu;
|
||
p.LifetimeLingZhu = _lifetimeLingZhu;
|
||
p.AbilityFlags = (uint)_unlockedAbilities;
|
||
}
|
||
|
||
public void OnLoad(SaveData saveData)
|
||
{
|
||
var p = saveData.Player;
|
||
MaxHP = p.MaxHP;
|
||
CurrentHP = Mathf.Clamp(p.CurrentHP, 0, MaxHP);
|
||
CurrentLingZhu = p.CurrentLingZhu;
|
||
_lifetimeLingZhu = p.LifetimeLingZhu;
|
||
_unlockedAbilities = (AbilityType)p.AbilityFlags;
|
||
|
||
_onHPChanged?.Raise(CurrentHP);
|
||
_onMaxHPChanged?.Raise(MaxHP);
|
||
_onLingZhuChanged?.Raise(CurrentLingZhu);
|
||
}
|
||
}
|
||
}
|