Slice 1: combat readability + HUD declutter (DR-038)

Four playtest do-now wins:
- Enemy health bars: pooled world-space Canvas, on-damage-sticky + fade,
  always-on <25% HP (CombatFeedbackSystem; no new replication).
- Telegraph fix: new baked client-safe EnemyTelegraph sizes the danger-cone ramp
  per enemy (0->1 ending at impact, fixes the Charger plateau); windup 18->22;
  a windup scale-pulse.
- Build-mode toggle: BuildPaletteState.PaletteOpen hides the palette by default,
  Tab / gamepad-Y toggles, with a discovery chip (HudSystem/BuildSendSystem).
- Charger committed-lunge tell: [GhostEnabledBit] IsLunging derived once/tick from
  LungeState (the Dead idiom); the danger cone persists through the lunge.

345/345 EditMode (+3 IsLunging derive tests); Play-validated: ghost-hash change
did not break the handshake, bake correct (telegraph on all enemies, IsLunging
baked-disabled on the Charger, replicated to client), no runtime errors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-17 12:48:08 -07:00
parent 5292940f9d
commit f3eccec524
12 changed files with 360 additions and 24 deletions
@@ -16,4 +16,20 @@ namespace ProjectM.Simulation
/// <summary>Server tick the wind-up completes + the strike lands (0 = not winding up; scheduled via TickUtil.NonZero).</summary>
[GhostField] public uint WindUpUntilTick;
}
/// <summary>Client-safe BAKED telegraph metadata (NOT a [GhostField] — baked values are identical on
/// both worlds, so the client reads it directly to size the danger-cone ramp + pick the Charger look).
/// WindupTicks = the variant's wind-up lead in ticks (the client danger-ramp denominator: Grunt =
/// Tuning.AttackWindupTicks, Charger = ChargerWindupTicks); IsCharger distinguishes the variant since
/// LungeState is server-only. Both stored as byte (no enum on a client-read path — honours the Burst
/// byte-not-enum convention); never changes at runtime, so a missed snapshot is irrelevant. Baked by
/// EnemyAuthoring.EnemyBaker (the SOLE writer — reads sibling ChargerAuthoring to set the Charger values).</summary>
public struct EnemyTelegraph : IComponentData
{
/// <summary>Per-variant wind-up DURATION in ticks (the client danger-ramp denominator).</summary>
public byte WindupTicks;
/// <summary>0 = Grunt-style; 1 = Charger (committed-lunge tell).</summary>
public byte IsCharger;
}
}
@@ -1,5 +1,6 @@
using Unity.Entities;
using Unity.Mathematics;
using Unity.NetCode;
namespace ProjectM.Simulation
{
@@ -30,4 +31,19 @@ namespace ProjectM.Simulation
/// rides EnemyAttackCooldown.NextAttackTick; this field only scores the punish.</summary>
public uint StaggerUntilTick;
}
/// <summary>
/// REPLICATED enableable MID-LUNGE flag on a Charger (Slice 1, Feature D). ENABLED for exactly the ticks a
/// Charger is committed to its locked-direction lunge (<see cref="LungeState.UntilTick"/> active), DISABLED
/// otherwise. The ONLY replicated Charger surface beyond the stock LocalTransform — a <c>[GhostEnabledBit]</c>,
/// NOT a [GhostField], because the client needs only on/off: the lunge HEADING is already carried by the
/// replicated LocalTransform.Rotation (EnemyAISystem writes LookRotationSafe(lungeDir) each lunge tick), so the
/// client indicator derives direction via AnimParamMath.PlanarForward like the danger cone already does. Fixes
/// the cue VANISHING at commit (AttackWindup zeroes on commit, so a windup-gated cone disappears exactly when
/// the danger is realest): this bit STAYS on through the committed travel. Server-derived once per tick from
/// LungeState.UntilTick in EnemyAISystem (the sole LungeState writer); BAKE DISABLED (a Charger spawns
/// not-lunging) + visit via .WithPresent&lt;IsLunging&gt;() to write the bit while disabled (the Dead idiom).
/// </summary>
[GhostEnabledBit]
public struct IsLunging : IComponentData, IEnableableComponent { }
}
+3 -2
View File
@@ -48,8 +48,9 @@ namespace ProjectM.Simulation
// ---- Husk attack telegraph (EnemyAISystem 2-phase strike; client cue in CombatFeedbackSystem) ----
/// <summary>Wind-up ticks before a Husk strike lands (~0.3s @ 60 ticks/sec). 0/1 = near-instant (legacy behaviour).</summary>
public const int AttackWindupTicks = 18;
/// <summary>Wind-up ticks before a Husk strike lands (~0.37s @ 60 ticks/sec) — sized for a fair tell
/// under interp lag (>= ~250ms reaction + interp buffer; Slice 1 readability). 0/1 = near-instant (legacy).</summary>
public const int AttackWindupTicks = 22;
// ---- Production / automation (M7: Harvester/Conveyor/Fabricator) ----