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); 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(); } } }