using System.Collections.Generic;
using ProjectM.Simulation;
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
using Unity.Transforms;
using UnityEngine;
namespace ProjectM.Client
{
///
/// Slice 1, Feature B — client-only enemy world-space HEALTH BARS (one pooled world-space Canvas per live Husk).
/// Observe-only presentation in that reads replicated
/// state and never mutates the sim or destroys a ghost. SELF-QUERIES enemies (Health + LocalTransform
/// ) and self-detects the damage edge from a per-enemy LastHp stored on the bar entry:
/// a decrease arms/refreshes that enemy's bar (sticky for , then fades). A bar
/// stays permanently on below HP; when more than
/// bars exist, distant ones (beyond of the local player) are hidden.
/// Billboards to the main camera. Prunes its cache against its own seen-set EVERY frame (a despawn drops the bar).
/// Extracted from CombatFeedbackSystem; owns its own FX-root + UI materials.
///
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
[UpdateInGroup(typeof(PresentationSystemGroup))]
public partial class EnemyHealthBarSystem : SystemBase
{
// CanvasGo == null => a tracking-only entry (enemy seen, not yet damaged, so no bar built). LastHp/MaxHp/Pos are
// refreshed from the live query each frame; LastHp is the self-owned damage-edge source (was the core's _cache).
struct HealthBarEntry
{
public GameObject CanvasGo; public UnityEngine.UI.Image Fill; public UnityEngine.UI.Image Bg;
public float ShowTimer; public bool Visible;
public float LastHp; public float MaxHp; public float3 Pos;
}
const int HealthBarPoolLimit = 24;
const float HealthBarShowDuration = 3f;
const float HealthBarFadeDuration = 0.5f;
const float HealthBarAlwaysOnThreshold = 0.25f;
const float HealthBarWorldYOffset = 2.3f;
readonly Dictionary _healthBars = new();
readonly List _barStale = new();
readonly List _barKeys = new();
readonly HashSet _seen = new(); // own enemy seen-set (per-frame prune)
Material _barBgMat, _barFillMat;
Transform _fxRoot;
Entity _localPlayer = Entity.Null;
protected override void OnStartRunning()
{
if (_fxRoot != null) return;
_fxRoot = new GameObject("~EnemyHealthBarFX").transform;
// Health-bar materials (UI/Default = always-included URP-compatible UI shader; per-instance Image.color carries alpha).
Shader uiShader = Shader.Find("UI/Default") ?? Shader.Find("Sprites/Default");
_barBgMat = new Material(uiShader) { name = "HealthBarBg" };
_barFillMat = new Material(uiShader) { name = "HealthBarFill" };
}
protected override void OnDestroy()
{
if (_fxRoot != null) Object.Destroy(_fxRoot.gameObject);
if (_barBgMat != null) Object.Destroy(_barBgMat);
if (_barFillMat != null) Object.Destroy(_barFillMat);
foreach (var kv in _healthBars)
if (kv.Value.CanvasGo != null) Object.Destroy(kv.Value.CanvasGo);
}
protected override void OnUpdate()
{
float dt = SystemAPI.Time.DeltaTime;
var cam = Camera.main;
// Predicted/physics jobs writing these must finish before this main-thread read.
EntityManager.CompleteDependencyBeforeRO();
EntityManager.CompleteDependencyBeforeRO();
// Local player (drives the pool-cap distance gate).
_localPlayer = Entity.Null;
float3 localPos = default;
foreach (var (xf, entity) in SystemAPI.Query>()
.WithAll().WithEntityAccess())
{
_localPlayer = entity;
localPos = xf.ValueRO.Position;
}
// Self-query enemies: track HP per enemy to self-detect the damage edge; a decrease arms/refreshes the bar.
_seen.Clear();
foreach (var (health, xf, entity) in
SystemAPI.Query, RefRO>().WithAll().WithEntityAccess())
{
_seen.Add(entity);
float cur = health.ValueRO.Current;
float max = health.ValueRO.Max;
float3 pos = xf.ValueRO.Position;
bool existed = _healthBars.TryGetValue(entity, out var entry);
bool damaged = existed && cur < entry.LastHp - 0.001f; // own damage edge (was core _cache prev.Hp)
entry.LastHp = cur; entry.MaxHp = max; entry.Pos = pos;
_healthBars[entity] = entry;
if (damaged) ShowHealthBar(entity); // arm/refresh this enemy's bar on a damage edge
}
UpdateHealthBars(dt, cam, localPos);
}
// ---- Enemy Health Bars (Slice 1, Feature B) — pooled world-space Canvas, on-damage sticky + fade ----
void ShowHealthBar(Entity entity)
{
if (!_healthBars.TryGetValue(entity, out var entry) || entry.CanvasGo == null)
entry = CreateHealthBar(entity);
entry.ShowTimer = HealthBarShowDuration;
if (!entry.Visible) { entry.CanvasGo.SetActive(true); entry.Visible = true; }
_healthBars[entity] = entry; // struct — must re-assign
}
HealthBarEntry CreateHealthBar(Entity entity)
{
var go = new GameObject("EnemyHPBar");
if (_fxRoot != null) go.transform.SetParent(_fxRoot, false);
var canvas = go.AddComponent