using System.Collections.Generic; using ProjectM.Simulation; using Unity.Entities; using Unity.Mathematics; using Unity.Rendering; // URPMaterialPropertyBaseColor, MaterialMeshInfo (Entities Graphics) using UnityEngine; namespace ProjectM.Client { /// /// Client-only TRUE BODY hit-flash for enemies (the focused follow-up DR-041 deferred as "its own ShaderGraph /// slice"). Enemies render via Rukhanka GPU deformation: the ghost ROOT holds the gameplay components /// (, ) while the visible meshes are LinkedEntityGroup CHILD render /// entities (each with a + the AnimatedLitShader material, whose _BaseColor /// is white). 's colored particle puff is the asset-free stand-in; THIS system /// flashes the actual body by driving the built-in Entities-Graphics per-instance override /// (a registered [MaterialProperty("_BaseColor")]) on those /// render children: on an enemy -decrease edge it lerps _BaseColor toward /// and decays back to white. No new component type, NO ShaderGraph edit, /// no server work, no [GhostField] — observe-only client presentation, so it is rollback-irrelevant. /// The render children gain lazily (added once per enemy via ECB); /// white is the baked rest value (every enemy uses the same AnimatedLitShader/Synty-atlas convention), so /// Flash==0 restores the untouched look and the override is never visible at rest. /// [WorldSystemFilter(WorldSystemFilterFlags.ClientSimulation)] [UpdateInGroup(typeof(PresentationSystemGroup))] public partial class EnemyHitFlashSystem : SystemBase { class FlashEntry { public readonly List RenderKids = new(); public float LastHp; public float Flash; // 1 on a fresh hit, decays to 0 public bool Settled; // wrote the final white frame after a flash ended (skips per-frame writes at rest) } readonly Dictionary _tracked = new(); readonly HashSet _seen = new(); readonly List _stale = new(); static readonly float4 White = new float4(1f, 1f, 1f, 1f); protected override void OnUpdate() { if (!FeelConfig.BodyFlashEnabled) { RestoreAllToRest(); return; } // settle any mid-flash body back to white before bailing float dt = SystemAPI.Time.DeltaTime; EntityManager.CompleteDependencyBeforeRO(); // Pass 1: discover enemies, ensure each is tracked + its render children carry the override component. _seen.Clear(); var ecb = new EntityCommandBuffer(Unity.Collections.Allocator.Temp); foreach (var (health, entity) in SystemAPI.Query>().WithAll().WithEntityAccess()) { _seen.Add(entity); if (_tracked.ContainsKey(entity)) continue; var entry = new FlashEntry { LastHp = health.ValueRO.Current, Flash = 0f, Settled = true }; var leg = EntityManager.GetBuffer(entity); for (int i = 0; i < leg.Length; i++) { var c = leg[i].Value; if (!EntityManager.Exists(c) || !EntityManager.HasComponent(c)) continue; entry.RenderKids.Add(c); if (!EntityManager.HasComponent(c)) ecb.AddComponent(c, new URPMaterialPropertyBaseColor { Value = White }); } // Render children can lag ghost instantiation a frame; only finalize once we actually found them (else retry next frame). if (entry.RenderKids.Count > 0) _tracked[entity] = entry; } ecb.Playback(EntityManager); ecb.Dispose(); // Pass 2: edge-detect Health, drive + decay the flash, write _BaseColor to the render children. var bc = FeelConfig.BodyFlashColor; float4 peak = new float4(bc.r, bc.g, bc.b, bc.a); float decay = dt / math.max(0.01f, FeelConfig.BodyFlashDurationSec); foreach (var kv in _tracked) { var entity = kv.Key; var entry = kv.Value; if (!_seen.Contains(entity)) continue; // despawned -> pruned below float cur = EntityManager.GetComponentData(entity).Current; if (cur < entry.LastHp - 0.001f) { entry.Flash = 1f; entry.Settled = false; } entry.LastHp = cur; if (entry.Flash <= 0f) { if (!entry.Settled) { WriteColor(entry, White); entry.Settled = true; } // settle to baked white once continue; } entry.Flash = math.max(0f, entry.Flash - decay); WriteColor(entry, math.lerp(White, peak, entry.Flash)); } // Prune despawned enemies (their render children die with them; just drop the managed entry). 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]); } } // Settle every tracked enemy's body back to its baked white rest color and stop tracking — invoked when // BodyFlashEnabled is toggled OFF mid-flash so no body is left frozen at an overdriven tint (re-tracked on re-enable). void RestoreAllToRest() { foreach (var kv in _tracked) WriteColor(kv.Value, White); _tracked.Clear(); } void WriteColor(FlashEntry entry, float4 col) { for (int i = 0; i < entry.RenderKids.Count; i++) { var c = entry.RenderKids[i]; if (EntityManager.Exists(c) && EntityManager.HasComponent(c)) EntityManager.SetComponentData(c, new URPMaterialPropertyBaseColor { Value = col }); } } } }