Files
zeling_v2/Assets/_Game/Scripts/Player/PlayerStats.cs
Joywayer b7baf7ad6a Add WeaponFeedback component and AddressableManagerWindow meta file
- Implemented WeaponFeedback class for handling weapon-related feedbacks such as hit effects and attack sounds.
- Added meta file for AddressableManagerWindow to manage addressable assets.
- Included a new jump.data file for profiler data.
2026-05-22 22:03:32 +08:00

387 lines
18 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.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_IsInvincible = IsInvincible;
_dbg_InvincibleTimer = _invincibleTimer;
_dbg_GodMode = _isGodMode;
// 字符串插值限速到 ~10 Hz避免每帧分配GC
if (Time.frameCount % 6 == 0)
{
_dbg_HP = $"{CurrentHP} / {MaxHP}";
_dbg_Soul = $"{CurrentSoulPower} / {MaxSoulPower}";
_dbg_Spirit = $"{CurrentSpiritPower} / {MaxSpiritPower}";
_dbg_Spring = $"{CurrentSpringCharges} / {MaxSpringCharges}";
_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();
}
/// <summary>
/// 强制即死,无视无敌帧(危险区域、深渊等环境击杀专用)。
/// GodMode 下仍然豁免。
/// </summary>
public void Kill()
{
if (_isGodMode || !IsAlive) return;
CurrentHP = 0;
_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);
}
}
}