Files
zeling_v2/Assets/Scripts/Player/PlayerStats.cs
2026-05-08 11:04:00 +08:00

236 lines
9.7 KiB
C#

using UnityEngine;
using BaseGames.Core.Events;
using BaseGames.Core.Save;
namespace BaseGames.Player
{
/// <summary>
/// 玩家数值管理组件。负责 HP、灵魂、灵气、弹簧充能、Geo、能力解锁与存档读写。
/// </summary>
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);
}
}
}