using ProjectM.Simulation;
using Unity.Entities;
using Unity.NetCode;
using UnityEngine;
using UnityEngine.UI;
namespace ProjectM.Client
{
///
/// Client-only screen HUD. A managed presentation SystemBase () that builds
/// a uGUI overlay canvas in code and drives it from the LOCAL player ghost each frame: a health bar
/// (Health / EffectiveCharacterStats.MaxHealth), an ability-cooldown bar (AbilityCooldown vs NetworkTime
/// ServerTick + EffectiveAbilityStats.CooldownTicks), a live Husk threat count, and a DOWNED/RESPAWNING overlay
/// (the derived gate). Presentation only — no simulation, client world only. Bars are
/// RawImages over Texture2D.whiteTexture (always available; the fill width is the RectTransform's
/// anchorMax.x), so the HUD needs no sprite assets — only a resolved builtin font for the labels.
///
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
[UpdateInGroup(typeof(PresentationSystemGroup))]
public partial class HudSystem : SystemBase
{
Canvas _canvas;
RectTransform _healthFill;
RectTransform _cooldownFill;
Text _healthText;
Text _threatText;
Text _phaseText;
Text _resourceText;
Text _locationText;
GameObject _respawnOverlay;
EntityQuery _huskQuery;
protected override void OnCreate()
{
_huskQuery = GetEntityQuery(ComponentType.ReadOnly());
}
protected override void OnStartRunning()
{
if (_canvas == null) BuildHud();
}
protected override void OnDestroy()
{
if (_canvas != null) Object.Destroy(_canvas.gameObject);
}
protected override void OnUpdate()
{
if (_canvas == null) return;
bool haveTick = SystemAPI.TryGetSingleton(out var nt);
// Macro-loop HUD (phase + cycle + countdown + location), read before the per-player early-out so it persists pre-spawn.
bool haveCycle = SystemAPI.TryGetSingleton(out var cyc);
if (_phaseText != null && haveCycle)
{
var endTick = new NetworkTick(cyc.PhaseEndTick);
string detail;
if (cyc.Phase == CyclePhase.Defend)
detail = _huskQuery.CalculateEntityCount() + " HUSKS";
else if (haveTick && cyc.PhaseEndTick != 0 && endTick.IsValid && endTick.IsNewerThan(nt.ServerTick))
detail = (endTick.TicksSince(nt.ServerTick) / 60) + "s";
else
detail = "";
_phaseText.text = PhaseLabel(cyc.Phase) + (detail.Length > 0 ? " - " + detail : "") + " CYCLE " + cyc.CycleNumber;
_phaseText.color = PhaseColor(cyc.Phase);
}
else if (_phaseText != null)
{
_phaseText.text = "";
}
if (_locationText != null)
{
var cam = Camera.main;
bool onExpedition = cam != null && cam.transform.position.x > 500f;
_locationText.text = onExpedition
? "ON EXPEDITION - return through the gate"
: "AT BASE" + (haveCycle && cyc.Phase == CyclePhase.Expedition ? " - step into the gate to deploy" : "");
_locationText.color = onExpedition ? new Color(1f, 0.8f, 0.4f) : new Color(0.6f, 0.85f, 1f);
}
if (_resourceText != null)
{
string res = "";
if (SystemAPI.TryGetSingletonEntity(out var ledgerE))
{
var buf = SystemAPI.GetBuffer(ledgerE);
int aether = 0, ore = 0, bio = 0;
for (int i = 0; i < buf.Length; i++)
{
var en = buf[i];
if (en.ItemId == ResourceId.Aether) aether = en.Count;
else if (en.ItemId == ResourceId.Ore) ore = en.Count;
else if (en.ItemId == ResourceId.Biomass) bio = en.Count;
}
res = "AETHER " + aether + " ORE " + ore + " BIO " + bio;
}
_resourceText.text = res;
}
bool found = false;
float hp = 0f, maxHp = 1f, cdFrac = 1f;
bool dead = false, shielded = false;
foreach (var (health, effChar, effAbility, cd, invuln, entity) in
SystemAPI.Query, RefRO, RefRO,
RefRO, RefRO>()
.WithAll().WithEntityAccess())
{
found = true;
hp = health.ValueRO.Current;
maxHp = effChar.ValueRO.MaxHealth > 0f ? effChar.ValueRO.MaxHealth : health.ValueRO.Max;
dead = SystemAPI.IsComponentEnabled(entity);
// Cooldown fraction via wrap-safe NetworkTick compare (raw uint subtraction is unsafe across
// tick wraparound — the project convention, mirroring AbilityFireSystem/EnemyAISystem).
uint nextFire = cd.ValueRO.NextFireTick;
int cdTicks = effAbility.ValueRO.CooldownTicks;
var nextTick = new NetworkTick(nextFire);
cdFrac = (haveTick && nextFire != 0 && cdTicks > 0 && nextTick.IsValid && nextTick.IsNewerThan(nt.ServerTick))
? Mathf.Clamp01(1f - nextTick.TicksSince(nt.ServerTick) / (float)cdTicks)
: 1f;
uint invulnUntil = invuln.ValueRO.UntilTick;
var invulnTick = new NetworkTick(invulnUntil);
shielded = haveTick && invulnUntil != 0 && invulnTick.IsValid && invulnTick.IsNewerThan(nt.ServerTick);
break;
}
_canvas.enabled = found || haveCycle;
if (!found) return;
float frac = maxHp > 0f ? Mathf.Clamp01(hp / maxHp) : 0f;
SetFill(_healthFill, frac);
var hc = _healthFill.GetComponent();
if (hc != null)
hc.color = shielded
? new Color(0.45f, 0.85f, 1f)
: Color.Lerp(new Color(0.92f, 0.16f, 0.16f), new Color(0.25f, 0.9f, 0.5f), frac);
if (_healthText != null)
_healthText.text = Mathf.CeilToInt(Mathf.Max(0f, hp)) + " / " + Mathf.CeilToInt(maxHp) + (shielded ? " SHIELDED" : "");
SetFill(_cooldownFill, cdFrac);
if (_threatText != null)
_threatText.text = "HUSKS " + _huskQuery.CalculateEntityCount();
_respawnOverlay.SetActive(dead);
}
static void SetFill(RectTransform fill, float frac)
{
if (fill == null) return;
var max = fill.anchorMax;
max.x = Mathf.Clamp01(frac);
fill.anchorMax = max;
}
// ---- uGUI construction (code-built; no prefab/sprite assets) ----
void BuildHud()
{
var go = new GameObject("~HUD");
_canvas = go.AddComponent