using UnityEngine; using BaseGames.Core.Events; using BaseGames.Core.Save; namespace BaseGames.Player { /// /// 玩家数值管理组件。负责 HP、灵魂、灵气、弹簧充能、Geo、能力解锁与存档读写。 /// public class PlayerStats : MonoBehaviour, ISaveable, BaseGames.Core.IRestoreOnSave { [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 _onGeoChanged; [SerializeField] private AbilityTypeEventChannelSO _onAbilityUnlocked; [SerializeField] private VoidEventChannelSO _onPlayerDied; // ── 运行时数值 ───────────────────────────────────────────────────────── 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 CurrentGeo { get; private set; } public bool IsInvincible => _invincibleTimer > 0f; public bool IsAlive => CurrentHP > 0; private float _invincibleTimer; private float _spiritRegenTimer; private AbilityType _unlockedAbilities = AbilityType.None; private void Awake() { if (_config == null) { Debug.LogWarning("[PlayerStats] PlayerStatsSO not assigned.", this); return; } MaxHP = _config.MaxHP; CurrentHP = MaxHP; MaxSoulPower = _config.MaxSoulPower; MaxSpiritPower = _config.MaxSpiritPower; MaxSpringCharges = _config.MaxSpringCharges; CurrentSpringCharges = MaxSpringCharges; CurrentGeo = _config.InitialGeo; } private void Update() { float dt = Time.deltaTime; if (_invincibleTimer > 0f) _invincibleTimer -= dt; if (_config != null && _config.SpiritRegenRate > 0) { _spiritRegenTimer += dt; if (_spiritRegenTimer >= 1f) { _spiritRegenTimer -= 1f; AddSpiritPower(_config.SpiritRegenRate); } } } // ── HP ──────────────────────────────────────────────────────────────── public void TakeDamage(int amount) { if (IsInvincible || !IsAlive || amount <= 0) return; CurrentHP = Mathf.Max(0, CurrentHP - amount); _onHPChanged?.Raise(CurrentHP); if (CurrentHP == 0) _onPlayerDied?.Raise(); } 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); } 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 ──────────────────────────────────────────────────────────── public bool UseSpring() { if (CurrentSpringCharges <= 0) return false; CurrentSpringCharges--; _onSpringChargesChanged?.Raise(CurrentSpringCharges); if (_config != null) HealHP(_config.SpringHealAmount); 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) { if (_config == null) return; SpringKillPoints += points; if (SpringKillPoints >= _config.SpringKillThreshold) { SpringKillPoints = 0; RestoreSpringCharges(1); } } // ── Geo ─────────────────────────────────────────────────────────────── public void AddGeo(int amount) { if (amount <= 0) return; CurrentGeo += amount; _onGeoChanged?.Raise(CurrentGeo); } public bool SpendGeo(int amount) { if (CurrentGeo < amount) return false; CurrentGeo -= amount; _onGeoChanged?.Raise(CurrentGeo); return true; } // ── Invincibility ───────────────────────────────────────────────────── public void BeginInvincibility(float duration = -1f) { float d = duration >= 0f ? duration : (_config != null ? _config.InvincibilityDuration : 0.6f); _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); } public void LockAbility(AbilityType ability) => _unlockedAbilities &= ~ability; // ── ISaveable ───────────────────────────────────────────────────────── public void OnSave(SaveData saveData) { var p = saveData.Player; p.CurrentHP = CurrentHP; p.MaxHP = MaxHP; p.CurrentGeo = CurrentGeo; p.AbilityFlags = (uint)_unlockedAbilities; } public void OnLoad(SaveData saveData) { var p = saveData.Player; MaxHP = p.MaxHP; CurrentHP = Mathf.Clamp(p.CurrentHP, 0, MaxHP); CurrentGeo = p.CurrentGeo; _unlockedAbilities = (AbilityType)p.AbilityFlags; _onHPChanged?.Raise(CurrentHP); _onMaxHPChanged?.Raise(MaxHP); _onGeoChanged?.Raise(CurrentGeo); } } }