Combat feel: correct boss bar, player hit-flash, dash-to-move, finisher hit-stop

Boss bar reconstructs the true max client-side (Health.Max unreplicated) via
Kind==Charger + expedition-pos filter; boss slam telegraph = a full ground ring
at BossSlamRadius; IsAttacking OR's IsLunging. Player body flash on hurt (distinct
red, jitter-safe threshold). Dash toward movement input when moving else facing.
Warrior cone: muzzle-cue suppressed (cone arc is its cue). Minimal finisher-only
hit-stop (PrototypeCameraRig.Hold, behind FeelConfig flag; never Time.timeScale).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-05 20:21:32 -07:00
parent 3995af736c
commit 43d7cca2f8
7 changed files with 92 additions and 24 deletions
@@ -32,6 +32,7 @@ namespace ProjectM.Client
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)
public bool IsPlayer; // C2: a player entry flashes PlayerHurtFlashColor (red), an enemy the white overdrive rest)
}
readonly Dictionary<Entity, FlashEntry> _tracked = new();
@@ -50,12 +51,12 @@ namespace ProjectM.Client
_seen.Clear();
var ecb = new EntityCommandBuffer(Unity.Collections.Allocator.Temp);
foreach (var (health, entity) in
SystemAPI.Query<RefRO<Health>>().WithAll<EnemyTag, LinkedEntityGroup>().WithEntityAccess())
SystemAPI.Query<RefRO<Health>>().WithAny<EnemyTag, PlayerTag>().WithAll<LinkedEntityGroup>().WithEntityAccess())
{
_seen.Add(entity);
if (_tracked.ContainsKey(entity)) continue;
var entry = new FlashEntry { LastHp = health.ValueRO.Current, Flash = 0f, Settled = true };
var entry = new FlashEntry { LastHp = health.ValueRO.Current, Flash = 0f, Settled = true, IsPlayer = EntityManager.HasComponent<PlayerTag>(entity) };
var leg = EntityManager.GetBuffer<LinkedEntityGroup>(entity);
for (int i = 0; i < leg.Length; i++)
{
@@ -73,7 +74,9 @@ namespace ProjectM.Client
// 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);
float4 enemyPeak = new float4(bc.r, bc.g, bc.b, bc.a);
var pc = FeelConfig.PlayerHurtFlashColor;
float4 playerPeak = new float4(pc.r, pc.g, pc.b, pc.a);
float decay = dt / math.max(0.01f, FeelConfig.BodyFlashDurationSec);
foreach (var kv in _tracked)
{
@@ -82,7 +85,7 @@ namespace ProjectM.Client
if (!_seen.Contains(entity)) continue; // despawned -> pruned below
float cur = EntityManager.GetComponentData<Health>(entity).Current;
if (cur < entry.LastHp - 0.001f) { entry.Flash = 1f; entry.Settled = false; }
if (cur < entry.LastHp - 0.5f) { entry.Flash = 1f; entry.Settled = false; }
entry.LastHp = cur;
if (entry.Flash <= 0f)
@@ -92,6 +95,7 @@ namespace ProjectM.Client
}
entry.Flash = math.max(0f, entry.Flash - decay);
float4 peak = entry.IsPlayer ? playerPeak : enemyPeak;
WriteColor(entry, math.lerp(White, peak, entry.Flash));
}