using UnityEngine; using UnityEngine.UI; using Ashwild.Player; namespace Ashwild.UI { public class PlayerStatsUI : MonoBehaviour { [Header("Health Bar")] [SerializeField] private Image healthBarFill; [Header("Hunger Bar")] [SerializeField] private Image hungerBarFill; [Header("Thirst Bar")] [SerializeField] private Image thirstBarFill; [Header("Settings")] [SerializeField] private float smoothSpeed = 5f; [SerializeField] private Color damageFlashColor = new Color(1f, 0f, 0f, 0.3f); [SerializeField] private float flashDuration = 0.3f; private float displayedHealth; private float displayedHunger; private float displayedThirst; private Image flashOverlay; private float flashTimer; private PlayerStats playerStats; private bool bound; /// /// Waits for the networked local player to spawn before reading its stats. /// private void OnEnable() { PlayerEvents.LocalPlayerSpawned += HandleLocalPlayerSpawned; } /// /// Unsubscribes — mirrors OnEnable exactly. /// private void OnDisable() { PlayerEvents.LocalPlayerSpawned -= HandleLocalPlayerSpawned; } private void Start() { displayedHealth = 1f; displayedHunger = 1f; displayedThirst = 1f; // Set up fill bars — does not need the player. SetupBar(healthBarFill); SetupBar(hungerBarFill); SetupBar(thirstBarFill); // The player may already exist (late UI init); otherwise we wait for the spawn event. if (PlayerStats.Instance != null) BindToStats(); } /// /// Binds to the local player's stats as soon as it spawns on the network. /// private void HandleLocalPlayerSpawned() => BindToStats(); /// /// Caches the stats reference and listens for health changes; runs once. /// private void BindToStats() { if (bound) return; playerStats = PlayerStats.Instance; if (playerStats == null) return; bound = true; playerStats.onHealthChanged.AddListener(OnHealthChanged); } private void SetupBar(Image bar) { if (bar == null) return; bar.type = Image.Type.Filled; bar.fillMethod = Image.FillMethod.Horizontal; bar.fillOrigin = (int)Image.OriginHorizontal.Left; } private void Update() { if (playerStats == null) return; // Smooth fill towards actual values displayedHealth = Mathf.Lerp(displayedHealth, playerStats.HealthNormalized, Time.deltaTime * smoothSpeed); displayedHunger = Mathf.Lerp(displayedHunger, playerStats.HungerNormalized, Time.deltaTime * smoothSpeed); displayedThirst = Mathf.Lerp(displayedThirst, playerStats.ThirstNormalized, Time.deltaTime * smoothSpeed); if (healthBarFill != null) healthBarFill.fillAmount = displayedHealth; if (hungerBarFill != null) hungerBarFill.fillAmount = displayedHunger; if (thirstBarFill != null) thirstBarFill.fillAmount = displayedThirst; // Damage flash if (flashTimer > 0f) { flashTimer -= Time.deltaTime; if (flashOverlay != null) { Color c = damageFlashColor; c.a = damageFlashColor.a * (flashTimer / flashDuration); flashOverlay.color = c; } } } private float previousHealth = -1f; private void OnHealthChanged(float newHealth) { // Flash only when health decreases (damage taken) float normalized = newHealth / playerStats.MaxHealth; if (previousHealth >= 0f && normalized < previousHealth) flashTimer = flashDuration; previousHealth = normalized; } private void OnDestroy() { if (playerStats != null) playerStats.onHealthChanged.RemoveListener(OnHealthChanged); } } }