G6+G4: Cone damage-at-contact + zone fill telegraph (ZoneEffect GhostFields) + co-op saturation budget

Cone/SpecialSlam damage lands at its visual contact via ConeContactPending (knob 32, 0=legacy;
early-flush + resolve re-validate; death + BOTH class-swap paths drop armed pendings — fixes the
shipped melee death-strand in the same stroke). Zones: [GhostField] Caster/Radius/NextTick +
ZoneTelegraphSystem (Geyser latch contract on InterpolationTick; rim = true radius; arm grows,
persistent phase drains). Cone cues latch to contact (FireStartRaw, C14); TuningConfig.Defaults()
fallback at client cue sites (release-build timing fix). G4: SaturationMath ally-FX degrade
(living-enemy census, solo-exempt; enemy telegraphs structurally exempt) + CombatStressDebug +
overlay saturation rows. Reviews wf_98bf1268 (13 confirmed folded) / wf_9757d214 (5 confirmed fixed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 13:27:50 -07:00
parent 9d6977cd64
commit 2aebc37115
25 changed files with 598 additions and 64 deletions
@@ -0,0 +1,24 @@
using Unity.Mathematics;
namespace ProjectM.Client
{
/// <summary>07-21 G4 (review wf_98bf1268) — pure co-op saturation-budget math (no ECS; unit-tested like
/// <c>HudVisualMath</c>, the Client-side presentation-math precedent). Maps a live on-screen enemy count to
/// the ALLY-FX degradation scale: 1 at &lt;= start, linear down to <paramref name="floor"/> at &gt;= full.
/// Enemy telegraphs never ride this (guidelines G4: they never degrade); local-player FX never ride this.
/// Note: <c>EnemyMarkerSystem</c>'s pip fade equals <c>AllyScale(count, start, 2*start, floor)</c> — a later
/// pass can retrofit it onto this helper.</summary>
public static class SaturationMath
{
/// <summary>1 at enemies &lt;= start; linear to <paramref name="floor"/> at enemies &gt;= full; clamped.
/// Degenerate inputs (full &lt;= start) snap straight to floor once past start.</summary>
public static float AllyScale(int enemies, int start, int full, float floor)
{
floor = math.clamp(floor, 0f, 1f);
if (enemies <= start) return 1f;
if (full <= start) return floor;
float t = math.saturate((enemies - start) / (float)(full - start));
return math.lerp(1f, floor, t);
}
}
}