104 lines
5.3 KiB
C#
104 lines
5.3 KiB
C#
using System.Collections.Generic;
|
|
using ProjectM.Simulation;
|
|
using Unity.Entities;
|
|
using Unity.Mathematics;
|
|
using Unity.Transforms;
|
|
|
|
namespace ProjectM.Client
|
|
{
|
|
/// <summary>
|
|
/// Client-only harvest BODY feedback for resource nodes + Blight clutter — the "you're mining this down" cue that
|
|
/// complements <see cref="WorldFeedbackSystem"/>'s chip particles/SFX. Observe-only presentation (SystemBase, main
|
|
/// thread, no Burst) in <see cref="PresentationSystemGroup"/>: it edge-detects each node/clutter ghost's replicated
|
|
/// <c>Remaining</c> and drives a CLIENT-OWNED <see cref="PostTransformMatrix"/> 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 <c>_BaseColor</c> 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
|
|
/// <see cref="WorldFeelConfig"/> (<c>NodeMinScale</c> / <c>NodeHitPopScale</c> / <c>NodePopDurationSec</c>).
|
|
/// </summary>
|
|
[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<Entity, Entry> _tracked = new();
|
|
readonly HashSet<Entity> _seen = new();
|
|
readonly List<Entity> _stale = new();
|
|
|
|
protected override void OnUpdate()
|
|
{
|
|
if (!WorldFeelConfig.Enabled) { RestoreAll(); return; }
|
|
float dt = SystemAPI.Time.DeltaTime;
|
|
EntityManager.CompleteDependencyBeforeRO<ResourceNode>();
|
|
EntityManager.CompleteDependencyBeforeRO<BlightClutter>();
|
|
|
|
_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<RefRO<ResourceNode>>().WithEntityAccess())
|
|
Drive(entity, node.ValueRO.Remaining, popDecay, minScale, popAmt, ecb);
|
|
foreach (var (clut, entity) in SystemAPI.Query<RefRO<BlightClutter>>().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<PostTransformMatrix>(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<PostTransformMatrix>(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<PostTransformMatrix>(kv.Key))
|
|
EntityManager.SetComponentData(kv.Key, new PostTransformMatrix { Value = float4x4.Scale(1f) });
|
|
_tracked.Clear();
|
|
}
|
|
}
|
|
}
|