using Unity.Entities; using Unity.Mathematics; using Unity.NetCode; namespace ProjectM.Simulation { /// /// MC-0 — live-tunable dash/Charger feel knobs (a per-world singleton). UNCONDITIONAL type (no #if) so the /// reflection-built RpcCollection hash matches across release/dev peers — only the dev SYSTEMS that create, /// mutate, broadcast and receive it are #if UNITY_EDITOR. Consumers (DashSystem, EnemyAISystem) read it /// via TryGetSingleton and FALL BACK to when it is absent (release builds + /// EditMode worlds), so behaviour is identical to the old baked consts when no dev singleton exists. NOT a /// [GhostField] (no ghost-hash change / re-bake); the server broadcasts it to clients via /// so a PREDICTING client's DashSystem stays in sync (an MPPM thin client with no /// overlay learns tuned values ONLY through this broadcast). /// /// Values match the historical baked consts (DashSystem / EnemyAISystem / ); /// TuningConfigTests pins to them so the promote-to-singleton refactor is /// behaviour-preserving. Tick knobs clamp to >= 1 and value knobs to >= 0 () on EVERY /// write path ( + the client-side optimistic SetLocal), and DashSystem additionally clamps the /// i-frame window AT the divide site — a 0 there NaNs the kinematic body permanently (MC-0 design-review F1). /// /// public struct TuningConfig : IComponentData { public float DashDistance; public float IFrameWindowTicks; public float RecoverTailTicks; public float DashCooldownTicks; public float DashSharpness; public float ChargerWindupTicks; public float ChargerLungeSpeed; public float ChargerLungeDurationTicks; public float ChargerWhiffStaggerTicks; public float GruntWindupTicks; /// The baked feel defaults == the pre-MC-0 consts. Single source of truth for the fallback path. public static TuningConfig Defaults() => new TuningConfig { DashDistance = 4.0f, IFrameWindowTicks = 12f, RecoverTailTicks = 9f, DashCooldownTicks = 45f, DashSharpness = 200f, ChargerWindupTicks = 30f, ChargerLungeSpeed = 16f, ChargerLungeDurationTicks = 18f, ChargerWhiffStaggerTicks = 36f, GruntWindupTicks = Tuning.AttackWindupTicks, // canonical Grunt-windup source (TelegraphTests couples to it) }; /// Clamp a knob to its safe floor: tick knobs >= 1, value knobs >= 0. Used by every write path /// ( + the client-side optimistic SetLocal) so no 0/negative ever reaches a consumer. public static float ClampKnob(byte knob, float value) { switch (knob) { // value knobs: non-negative case TuningKnob.DashDistance: case TuningKnob.DashSharpness: case TuningKnob.ChargerLungeSpeed: return math.max(0f, value); // tick knobs: >= 1 (a 0 tick count is degenerate; a 0 i-frame window divides-by-zero in DashSystem) default: return math.max(1f, value); } } /// Authoritatively set one knob (clamped) by its index. Unknown index = no-op. public static void Apply(ref TuningConfig c, byte knob, float value) { value = ClampKnob(knob, value); switch (knob) { case TuningKnob.DashDistance: c.DashDistance = value; break; case TuningKnob.IFrameWindowTicks: c.IFrameWindowTicks = value; break; case TuningKnob.RecoverTailTicks: c.RecoverTailTicks = value; break; case TuningKnob.DashCooldownTicks: c.DashCooldownTicks = value; break; case TuningKnob.DashSharpness: c.DashSharpness = value; break; case TuningKnob.ChargerWindupTicks: c.ChargerWindupTicks = value; break; case TuningKnob.ChargerLungeSpeed: c.ChargerLungeSpeed = value; break; case TuningKnob.ChargerLungeDurationTicks: c.ChargerLungeDurationTicks = value; break; case TuningKnob.ChargerWhiffStaggerTicks: c.ChargerWhiffStaggerTicks = value; break; case TuningKnob.GruntWindupTicks: c.GruntWindupTicks = value; break; // unknown index -> no-op (matches the no-default switch convention in DebugCommandReceiveSystem) } } /// Read one knob by its index (overlay display). Unknown index = 0. public static float Get(in TuningConfig c, byte knob) { switch (knob) { case TuningKnob.DashDistance: return c.DashDistance; case TuningKnob.IFrameWindowTicks: return c.IFrameWindowTicks; case TuningKnob.RecoverTailTicks: return c.RecoverTailTicks; case TuningKnob.DashCooldownTicks: return c.DashCooldownTicks; case TuningKnob.DashSharpness: return c.DashSharpness; case TuningKnob.ChargerWindupTicks: return c.ChargerWindupTicks; case TuningKnob.ChargerLungeSpeed: return c.ChargerLungeSpeed; case TuningKnob.ChargerLungeDurationTicks: return c.ChargerLungeDurationTicks; case TuningKnob.ChargerWhiffStaggerTicks: return c.ChargerWhiffStaggerTicks; case TuningKnob.GruntWindupTicks: return c.GruntWindupTicks; default: return 0f; } } /// Project the full config onto the wire snapshot. public static DebugTuningReport ToReport(in TuningConfig c) => new DebugTuningReport { DashDistance = c.DashDistance, IFrameWindowTicks = c.IFrameWindowTicks, RecoverTailTicks = c.RecoverTailTicks, DashCooldownTicks = c.DashCooldownTicks, DashSharpness = c.DashSharpness, ChargerWindupTicks = c.ChargerWindupTicks, ChargerLungeSpeed = c.ChargerLungeSpeed, ChargerLungeDurationTicks = c.ChargerLungeDurationTicks, ChargerWhiffStaggerTicks = c.ChargerWhiffStaggerTicks, GruntWindupTicks = c.GruntWindupTicks, }; /// Reconstruct the full config from a wire snapshot (FULL state, not a delta). public static TuningConfig FromReport(in DebugTuningReport r) => new TuningConfig { DashDistance = r.DashDistance, IFrameWindowTicks = r.IFrameWindowTicks, RecoverTailTicks = r.RecoverTailTicks, DashCooldownTicks = r.DashCooldownTicks, DashSharpness = r.DashSharpness, ChargerWindupTicks = r.ChargerWindupTicks, ChargerLungeSpeed = r.ChargerLungeSpeed, ChargerLungeDurationTicks = r.ChargerLungeDurationTicks, ChargerWhiffStaggerTicks = r.ChargerWhiffStaggerTicks, GruntWindupTicks = r.GruntWindupTicks, }; } /// Byte indices for knobs (bytes — never an enum on a Bursted/RPC path). public static class TuningKnob { public const byte DashDistance = 0; public const byte IFrameWindowTicks = 1; public const byte RecoverTailTicks = 2; public const byte DashCooldownTicks = 3; public const byte DashSharpness = 4; public const byte ChargerWindupTicks = 5; public const byte ChargerLungeSpeed = 6; public const byte ChargerLungeDurationTicks = 7; public const byte ChargerWhiffStaggerTicks = 8; public const byte GruntWindupTicks = 9; /// Knob count (overlay iteration bound). public const byte Count = 10; } /// /// MC-0 — server → dev-client snapshot of the full (sent periodically by the /// editor-only broadcaster). UNCONDITIONAL wire type (like ) for RpcCollection /// hash parity; only the send/receive SYSTEMS are #if UNITY_EDITOR. FULL state (not a delta) so a /// late-joining client converges in one report. /// public struct DebugTuningReport : IRpcCommand { public float DashDistance; public float IFrameWindowTicks; public float RecoverTailTicks; public float DashCooldownTicks; public float DashSharpness; public float ChargerWindupTicks; public float ChargerLungeSpeed; public float ChargerLungeDurationTicks; public float ChargerWhiffStaggerTicks; public float GruntWindupTicks; } }