using Unity.Entities; using Unity.Mathematics; using Unity.NetCode; namespace ProjectM.Simulation { /// /// MC-0 — live-tunable dash/Charger/melee 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, /// MeleeComboSystem) 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/MeleeComboSystem 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; // MC-4 melee combo (the primary verb). The whole per-step SHAPE derives from these few LIVE scalars + one // finisher multiplier (no per-step authored blob — keeps the combo fully live-tunable; MC-4 review F5/BURST-1). public float MeleeDamage; public float MeleeRange; public float MeleeConeHalfAngleRad; public float MeleeRecoverTicks; public float MeleeChainGraceTicks; public float MeleeSwingMoveScale; public float MeleeKnockbackSpeed; public float MeleeFinisherMult; public float MeleeComboLength; // 07-20 melee feel forks (guidelines G2/G7, design review wf_000bc247): /// Finisher REACH multiplier (reach honesty G2.2) — MeleeFinisherMult keeps damage/recover/knockback only. public float MeleeFinisherRangeMult; /// Attack input-buffer window (ticks before unlock a press is remembered). 0 = off. public float MeleeBufferTicks; /// Base contact delay (ticks from swing start to the blade landing; per-step via MeleeTiming). 0 = IMMEDIATE (legacy same-tick cleave). public float MeleeContactTicks; /// 07-21 G6 (socket honesty): ticks from the Cone-socket fire tick to its damage landing (the /// SpecialSlam visual contact). 0 = IMMEDIATE (legacy at-fire cleave). Keep < CastFacingTicks (26) or a /// resting gamepad stick resolves a movement-facing cone; default 21 sits 1 tick under WarriorCone cooldown 22. public float ConeContactTicks; // EB-1 fortress aggro: a <1 multiplier on a Husk's SQUARED distance to a structure (so structures are // preferred targets); a closer player 'in the way' still wins. Read server-side by EnemyAISystem. public float StructureAggroWeight; // Phase 1 B1/B2 (design review wf_fd177263): poise threshold on the EXISTING KnockbackState.Speed channel // (light melee 6 nudges; finisher 6x1.8=10.8 and cone 8 stagger), and the separation pass's push-speed cap. public float StaggerKnockbackSpeed; public float SeparationMaxSpeed; // Facing/underwater feel (07-15): DEV OVERRIDES with a 0 = NO-OVERRIDE sentinel (0 falls back to the // deterministic source: authored stat / compile-time const / authored sharpness). Unlike other knobs // these default to 0 and clamp >= 0 — consumers substitute their source, a 0 never reaches an integrator. public float TurnRateDeg; // locomotion body-turn (deg/s); 0 = EffectiveCharacterStats.TurnRateRadiansPerSec public float CastTurnRateDeg; // cast-window body-turn (deg/s); 0 = PlayerAimSystem's cast-turn const public float MoveSharpness; // grounded accel sharpness; 0 = CharacterComponent.DefaultGroundedSharpness /// 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 = 14f, // tune: was 12 (0.20s) -> 0.23s, i-frames better cover a reacted telegraph RecoverTailTicks = 9f, DashCooldownTicks = 36f, // tune: was 45 (0.75s) -> 0.60s, snappier horde-kiter cadence DashSharpness = 200f, ChargerWindupTicks = Tuning.ChargerWindupTicks, ChargerLungeSpeed = 16f, ChargerLungeDurationTicks = 18f, ChargerWhiffStaggerTicks = 36f, GruntWindupTicks = Tuning.AttackWindupTicks, // canonical Grunt-windup source (TelegraphTests couples to it) MeleeDamage = 36f, // 07-21 HEAVY LOCK (operator A/B): DPS ~held at 60 (dmg = recover ticks) MeleeRange = 2.2f, // 07-20 G2.2 reach honesty: axe tip ~1.8m + a 0.4m player-favoring margin (was 2.6) MeleeConeHalfAngleRad = 0.9f, // ~51.5 deg half-angle (~103 deg cleave arc) MeleeRecoverTicks = 36f, // 07-21 HEAVY LOCK: 0.60s light lock (finisher x1.5 = 54t) MeleeChainGraceTicks = 28f, // 07-21 HEAVY LOCK: ~0.47s chain window after the lock MeleeSwingMoveScale = 0.25f, // 07-21 HEAVY LOCK: deeper movement commit while swinging MeleeKnockbackSpeed = 6f, MeleeFinisherMult = 1.5f, // finisher (last hit) scales dmg/range/recover/knockback (07-19: 1.8 -> 1.5, recover was ballooning at the heavy cadence) MeleeComboLength = 3f, // light, light, finisher MeleeFinisherRangeMult = 1.25f, // 07-20 G2.2: finisher REACH (damage/recover/knockback keep MeleeFinisherMult) MeleeBufferTicks = 10f, // 07-21 HEAVY LOCK: wider buffer fits the slower cadence (0 = off) MeleeContactTicks = 20f, // 07-21 HEAVY LOCK: blade lands 20/12/25t per step -- clip speeds retuned to MATCH (wire tool 1.0/0.85/0.88; audit must show zero drift). Step-3 (25t) sits 1 tick under CastFacingTicks 26 -- do NOT raise without raising the cast window. ConeContactTicks = 21f, // 07-21 G6: SpecialSlam contact ~0.35s after fire; keep < CastFacingTicks 26 StructureAggroWeight = 0.7f, // EB-1: <1 prefers structures (fortress aggro); live-tunable StaggerKnockbackSpeed = 7f, // B2 poise: kb.Speed >= this interrupts windups/lunges; below = nudge only SeparationMaxSpeed = 3f, // B1: max separation push (units/s) so soft-collision can't fling TurnRateDeg = 0f, // 07-15 facing feel: 0 = no override (authored stat wins) CastTurnRateDeg = 0f, // 0 = no override (PlayerAimSystem cast-turn const wins) MoveSharpness = 0f, // 0 = no override (DefaultGroundedSharpness wins) }; /// 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: case TuningKnob.MeleeDamage: case TuningKnob.MeleeRange: case TuningKnob.MeleeConeHalfAngleRad: case TuningKnob.MeleeSwingMoveScale: case TuningKnob.MeleeKnockbackSpeed: case TuningKnob.MeleeFinisherMult: case TuningKnob.StructureAggroWeight: case TuningKnob.StaggerKnockbackSpeed: case TuningKnob.TurnRateDeg: // 07-15: 0 = no-override sentinel (never reaches an integrator) case TuningKnob.CastTurnRateDeg: case TuningKnob.MoveSharpness: case TuningKnob.SeparationMaxSpeed: case TuningKnob.MeleeFinisherRangeMult: case TuningKnob.MeleeBufferTicks: // 07-20: 0 = buffer OFF sentinel (must survive ClampKnob — review C10) case TuningKnob.MeleeContactTicks: // 07-20: 0 = IMMEDIATE sentinel (legacy same-tick cleave) case TuningKnob.ConeContactTicks: // 07-21 G6: 0 = IMMEDIATE sentinel (legacy at-fire cone) return math.max(0f, value); // tick knobs: >= 1 (a 0 tick count is degenerate; a 0 i-frame window divides-by-zero in DashSystem). // FinalSiegeMultiplier also lands here on purpose — a final siege should never be < 1x a normal one. 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; case TuningKnob.MeleeDamage: c.MeleeDamage = value; break; case TuningKnob.MeleeRange: c.MeleeRange = value; break; case TuningKnob.MeleeConeHalfAngleRad: c.MeleeConeHalfAngleRad = value; break; case TuningKnob.MeleeRecoverTicks: c.MeleeRecoverTicks = value; break; case TuningKnob.MeleeChainGraceTicks: c.MeleeChainGraceTicks = value; break; case TuningKnob.MeleeSwingMoveScale: c.MeleeSwingMoveScale = value; break; case TuningKnob.MeleeKnockbackSpeed: c.MeleeKnockbackSpeed = value; break; case TuningKnob.MeleeFinisherMult: c.MeleeFinisherMult = value; break; case TuningKnob.MeleeComboLength: c.MeleeComboLength = value; break; case TuningKnob.StructureAggroWeight: c.StructureAggroWeight = value; break; case TuningKnob.StaggerKnockbackSpeed: c.StaggerKnockbackSpeed = value; break; case TuningKnob.SeparationMaxSpeed: c.SeparationMaxSpeed = value; break; case TuningKnob.TurnRateDeg: c.TurnRateDeg = value; break; case TuningKnob.CastTurnRateDeg: c.CastTurnRateDeg = value; break; case TuningKnob.MoveSharpness: c.MoveSharpness = value; break; case TuningKnob.MeleeBufferTicks: c.MeleeBufferTicks = value; break; case TuningKnob.MeleeFinisherRangeMult: c.MeleeFinisherRangeMult = value; break; case TuningKnob.MeleeContactTicks: c.MeleeContactTicks = value; break; case TuningKnob.ConeContactTicks: c.ConeContactTicks = 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; case TuningKnob.MeleeDamage: return c.MeleeDamage; case TuningKnob.MeleeRange: return c.MeleeRange; case TuningKnob.MeleeConeHalfAngleRad: return c.MeleeConeHalfAngleRad; case TuningKnob.MeleeRecoverTicks: return c.MeleeRecoverTicks; case TuningKnob.MeleeChainGraceTicks: return c.MeleeChainGraceTicks; case TuningKnob.MeleeSwingMoveScale: return c.MeleeSwingMoveScale; case TuningKnob.MeleeKnockbackSpeed: return c.MeleeKnockbackSpeed; case TuningKnob.MeleeFinisherMult: return c.MeleeFinisherMult; case TuningKnob.MeleeComboLength: return c.MeleeComboLength; case TuningKnob.StructureAggroWeight: return c.StructureAggroWeight; case TuningKnob.StaggerKnockbackSpeed: return c.StaggerKnockbackSpeed; case TuningKnob.SeparationMaxSpeed: return c.SeparationMaxSpeed; case TuningKnob.TurnRateDeg: return c.TurnRateDeg; case TuningKnob.CastTurnRateDeg: return c.CastTurnRateDeg; case TuningKnob.MoveSharpness: return c.MoveSharpness; case TuningKnob.MeleeBufferTicks: return c.MeleeBufferTicks; case TuningKnob.MeleeFinisherRangeMult: return c.MeleeFinisherRangeMult; case TuningKnob.MeleeContactTicks: return c.MeleeContactTicks; case TuningKnob.ConeContactTicks: return c.ConeContactTicks; 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, MeleeDamage = c.MeleeDamage, MeleeRange = c.MeleeRange, MeleeConeHalfAngleRad = c.MeleeConeHalfAngleRad, MeleeRecoverTicks = c.MeleeRecoverTicks, MeleeChainGraceTicks = c.MeleeChainGraceTicks, MeleeSwingMoveScale = c.MeleeSwingMoveScale, MeleeKnockbackSpeed = c.MeleeKnockbackSpeed, MeleeFinisherMult = c.MeleeFinisherMult, MeleeComboLength = c.MeleeComboLength, StructureAggroWeight = c.StructureAggroWeight, StaggerKnockbackSpeed = c.StaggerKnockbackSpeed, SeparationMaxSpeed = c.SeparationMaxSpeed, TurnRateDeg = c.TurnRateDeg, CastTurnRateDeg = c.CastTurnRateDeg, MoveSharpness = c.MoveSharpness, MeleeBufferTicks = c.MeleeBufferTicks, MeleeFinisherRangeMult = c.MeleeFinisherRangeMult, MeleeContactTicks = c.MeleeContactTicks, ConeContactTicks = c.ConeContactTicks, }; /// 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, MeleeDamage = r.MeleeDamage, MeleeRange = r.MeleeRange, MeleeConeHalfAngleRad = r.MeleeConeHalfAngleRad, MeleeRecoverTicks = r.MeleeRecoverTicks, MeleeChainGraceTicks = r.MeleeChainGraceTicks, MeleeSwingMoveScale = r.MeleeSwingMoveScale, MeleeKnockbackSpeed = r.MeleeKnockbackSpeed, MeleeFinisherMult = r.MeleeFinisherMult, MeleeComboLength = r.MeleeComboLength, StructureAggroWeight = r.StructureAggroWeight, StaggerKnockbackSpeed = r.StaggerKnockbackSpeed, SeparationMaxSpeed = r.SeparationMaxSpeed, TurnRateDeg = r.TurnRateDeg, CastTurnRateDeg = r.CastTurnRateDeg, MoveSharpness = r.MoveSharpness, MeleeBufferTicks = r.MeleeBufferTicks, MeleeFinisherRangeMult = r.MeleeFinisherRangeMult, MeleeContactTicks = r.MeleeContactTicks, ConeContactTicks = r.ConeContactTicks, }; } /// 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; public const byte MeleeDamage = 10; public const byte MeleeRange = 11; public const byte MeleeConeHalfAngleRad = 12; public const byte MeleeRecoverTicks = 13; public const byte MeleeChainGraceTicks = 14; public const byte MeleeSwingMoveScale = 15; public const byte MeleeKnockbackSpeed = 16; public const byte MeleeFinisherMult = 17; public const byte MeleeComboLength = 18; public const byte StructureAggroWeight = 19; // RETIRED knob ids (LANTERN purge — keep 20-23 reserved, never renumber): // 20 = CoreDamagePerHusk · 21 = CoreRegenIntervalTicks · 22 = CoreOverrunDrainPct · 23 = FinalSiegeMultiplier public const byte StaggerKnockbackSpeed = 24; public const byte SeparationMaxSpeed = 25; // 07-15 facing/underwater feel dev-overrides (0 = no-override sentinel; see TuningConfig fields): public const byte TurnRateDeg = 26; public const byte CastTurnRateDeg = 27; public const byte MoveSharpness = 28; public const byte MeleeBufferTicks = 29; // 07-20 G7 (0 = off sentinel) public const byte MeleeFinisherRangeMult = 30; // 07-20 G2.2 public const byte MeleeContactTicks = 31; // 07-20 G2.1 (0 = immediate sentinel) public const byte ConeContactTicks = 32; // 07-21 G6 (0 = immediate sentinel) /// Knob count (overlay iteration bound). public const byte Count = 33; } /// /// 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; public float MeleeDamage; public float MeleeRange; public float MeleeConeHalfAngleRad; public float MeleeRecoverTicks; public float MeleeChainGraceTicks; public float MeleeSwingMoveScale; public float MeleeKnockbackSpeed; public float MeleeFinisherMult; public float MeleeComboLength; public float StructureAggroWeight; public float StaggerKnockbackSpeed; public float SeparationMaxSpeed; public float TurnRateDeg; public float CastTurnRateDeg; public float MoveSharpness; public float MeleeBufferTicks; // 07-20 melee feel forks (dev-protocol bump: rebuild both peers) public float MeleeFinisherRangeMult; public float MeleeContactTicks; public float ConeContactTicks; // 07-21 G6 socket honesty (dev-protocol bump: rebuild both peers) } // NOTE: appending fields = a DEV-PROTOCOL BUMP (RpcCollection hash) — rebuild both peers together. }