using System.Collections.Generic;
using ProjectM.Simulation;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
namespace ProjectM.Client
{
///
/// Client-only harvest BODY feedback for resource nodes + Blight clutter — the "you're mining this down" cue that
/// complements 's chip particles/SFX. Observe-only presentation (SystemBase, main
/// thread, no Burst) in : it edge-detects each node/clutter ghost's replicated
/// Remaining and drives a CLIENT-OWNED scale on the ghost root so the node
/// (1) SHRINKS as Remaining falls toward 0 and (2) POPS briefly larger on each hit — a clear per-hit reaction plus
/// a running "how much is left" read. PostTransformMatrix composes on top of the replicated LocalTransform and is
/// NOT a ghost field, so this is pure client cosmetics: no wire change, no sim mutation, and it never destroys a
/// ghost (GhostDespawnSystem owns despawn; the shatter burst is WorldFeedbackSystem's prune path). Uses a scale
/// reaction rather than a _BaseColor flash because node materials (unlike the white-based enemy/player
/// bodies) aren't guaranteed white, so a base-color override could clobber their rest tint. Knobs in
/// (NodeMinScale / NodeHitPopScale / NodePopDurationSec).
///
[WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)]
[UpdateInGroup(typeof(PresentationSystemGroup))]
public partial class NodeFeedbackSystem : SystemBase
{
class Entry
{
public int LastRemaining;
public int MaxRemaining; // client "full" reference = the largest Remaining ever seen for this node
public float Pop; // 1 on a fresh hit, decays to 0 (the per-hit scale-up)
}
readonly Dictionary _tracked = new();
readonly HashSet _seen = new();
readonly List _stale = new();
protected override void OnUpdate()
{
if (!WorldFeelConfig.Enabled) { RestoreAll(); return; }
float dt = SystemAPI.Time.DeltaTime;
EntityManager.CompleteDependencyBeforeRO();
EntityManager.CompleteDependencyBeforeRO();
_seen.Clear();
var ecb = new EntityCommandBuffer(Unity.Collections.Allocator.Temp);
float popDecay = dt / math.max(0.01f, WorldFeelConfig.NodePopDurationSec);
float minScale = math.clamp(WorldFeelConfig.NodeMinScale, 0.05f, 1f);
float popAmt = math.max(0f, WorldFeelConfig.NodeHitPopScale);
foreach (var (node, entity) in SystemAPI.Query>().WithEntityAccess())
Drive(entity, node.ValueRO.Remaining, popDecay, minScale, popAmt, ecb);
foreach (var (clut, entity) in SystemAPI.Query>().WithEntityAccess())
Drive(entity, clut.ValueRO.Remaining, popDecay, minScale, popAmt, ecb);
// Track B: this only ever records on a node's FIRST sighting, but Playback is a structural-change
// sync point and used to run every frame regardless.
if (!ecb.IsEmpty) ecb.Playback(EntityManager);
ecb.Dispose();
// Prune despawned (depleted/shattered) nodes — their PostTransformMatrix dies with the ghost.
if (_tracked.Count != _seen.Count)
{
_stale.Clear();
foreach (var kv in _tracked) if (!_seen.Contains(kv.Key)) _stale.Add(kv.Key);
for (int i = 0; i < _stale.Count; i++) _tracked.Remove(_stale[i]);
}
}
void Drive(Entity e, int remaining, float popDecay, float minScale, float popAmt, EntityCommandBuffer ecb)
{
_seen.Add(e);
if (!_tracked.TryGetValue(e, out var entry))
{
entry = new Entry { LastRemaining = remaining, MaxRemaining = math.max(1, remaining), Pop = 0f };
_tracked[e] = entry;
if (!EntityManager.HasComponent(e))
ecb.AddComponent(e, new PostTransformMatrix { Value = float4x4.Scale(1f) }); // present next frame
}
else
{
if (remaining < entry.LastRemaining) entry.Pop = 1f; // a hit landed
entry.LastRemaining = remaining;
if (remaining > entry.MaxRemaining) entry.MaxRemaining = remaining;
}
entry.Pop = math.max(0f, entry.Pop - popDecay);
// Progressive shrink toward NodeMinScale as Remaining -> 0, plus a brief per-hit pop.
float frac = math.saturate(remaining / (float)math.max(1, entry.MaxRemaining));
float scale = math.lerp(minScale, 1f, frac) * (1f + entry.Pop * popAmt);
if (EntityManager.HasComponent(e))
EntityManager.SetComponentData(e, new PostTransformMatrix { Value = float4x4.Scale(scale) });
}
// Toggled off (WorldFeelConfig.Enabled=false) -> restore every tracked node to full size and stop tracking.
void RestoreAll()
{
foreach (var kv in _tracked)
if (EntityManager.Exists(kv.Key) && EntityManager.HasComponent(kv.Key))
EntityManager.SetComponentData(kv.Key, new PostTransformMatrix { Value = float4x4.Scale(1f) });
_tracked.Clear();
}
}
}