236 lines
9.7 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|