4f0b4e8087
At GoalProgress.Charge>=Target a new server-only GoalReachedSystem arms a larger final siege (x live FinalSiegeMultiplier) and flips RunPhase=FinalDefense; CyclePhaseSystem latches a REPLICATED RunOutcome (Victory on clear / Loss on Core breach) and halts the director. RunOutcome is a [GhostField] byte on the global CycleDirector ghost (the client banner observes it); RunPhase stays server-only. ThreatDirector/CoreRestore/CoreDamage halt once decided; SiegeTimeout is off during the final siege. SaveData v5 persists the outcome so a won/lost run loads finished. GoalProgress.Target 10->4. Completes Path A's spine. See DR-036. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
312 lines
18 KiB
C#
312 lines
18 KiB
C#
using Unity.Entities;
|
|
using Unity.Mathematics;
|
|
using Unity.NetCode;
|
|
|
|
namespace ProjectM.Simulation
|
|
{
|
|
/// <summary>
|
|
/// 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 <c>#if UNITY_EDITOR</c>. Consumers (DashSystem, EnemyAISystem,
|
|
/// MeleeComboSystem) read it via <c>TryGetSingleton</c> and FALL BACK to <see cref="Defaults"/> when it is absent
|
|
/// (release builds + EditMode worlds), so behaviour is identical to the old baked consts when no dev singleton
|
|
/// exists. NOT a <c>[GhostField]</c> (no ghost-hash change / re-bake); the server broadcasts it to clients via
|
|
/// <see cref="DebugTuningReport"/> so a PREDICTING client's DashSystem/MeleeComboSystem stays in sync (an MPPM thin
|
|
/// client with no overlay learns tuned values ONLY through this broadcast).
|
|
/// <para>
|
|
/// Values match the historical baked consts (DashSystem / EnemyAISystem / <see cref="Tuning.AttackWindupTicks"/>);
|
|
/// <c>TuningConfigTests</c> pins <see cref="Defaults"/> to them so the promote-to-singleton refactor is
|
|
/// behaviour-preserving. Tick knobs clamp to >= 1 and value knobs to >= 0 (<see cref="ClampKnob"/>) on EVERY
|
|
/// write path (<see cref="Apply"/> + 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).
|
|
/// </para>
|
|
/// </summary>
|
|
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;
|
|
|
|
// 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;
|
|
|
|
// END-1 Engine Core (live feel knobs; read server-side by CoreDamageSystem/CoreRestoreSystem/CyclePhaseSystem).
|
|
// CoreDamagePerHusk + CoreOverrunDrainPct are value knobs (>=0); CoreRegenIntervalTicks is a tick knob (>=1).
|
|
public float CoreDamagePerHusk; // integrity drained by one breaching Husk (~5 unintercepted = serious dent)
|
|
public float CoreRegenIntervalTicks; // ticks between +1 regen in Calm (18 -> +1/0.3s -> ~full over one short Calm)
|
|
public float CoreOverrunDrainPct; // fraction (0..1) of the shared ledger lost on a breach (soft-loss penalty)
|
|
|
|
// END-2 final siege: the would-be-next normal siege size is multiplied by this for the FINAL siege so the
|
|
// climax reads visibly larger. Floors at 1 (the default ClampKnob bucket) — a final siege is never smaller
|
|
// than a normal one; GoalReachedSystem also math.max(1, ...) at the use-site.
|
|
public float FinalSiegeMultiplier;
|
|
|
|
/// <summary>The baked feel defaults == the pre-MC-0 consts. Single source of truth for the fallback path.</summary>
|
|
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)
|
|
MeleeDamage = 18f,
|
|
MeleeRange = 2.6f,
|
|
MeleeConeHalfAngleRad = 0.9f, // ~51.5 deg half-angle (~103 deg cleave arc)
|
|
MeleeRecoverTicks = 16f, // light-swing lock/recovery (~0.27s)
|
|
MeleeChainGraceTicks = 18f, // window after the lock to chain the next hit (~0.3s)
|
|
MeleeSwingMoveScale = 0.35f, // movement-commit while swinging (0..1)
|
|
MeleeKnockbackSpeed = 6f,
|
|
MeleeFinisherMult = 1.8f, // finisher (last hit) scales dmg/range/recover/knockback
|
|
MeleeComboLength = 3f, // light, light, finisher
|
|
StructureAggroWeight = 0.7f, // EB-1: <1 prefers structures (fortress aggro); live-tunable
|
|
CoreDamagePerHusk = 10f, // END-1: 10 breaching Husks = full loss; ~5 = a serious dent
|
|
CoreRegenIntervalTicks = 18f, // END-1: +1 integrity / 0.3s in Calm (~30s to refill 100 from 0)
|
|
CoreOverrunDrainPct = 0.5f, // END-1: a breach costs half the shared ledger (soft-loss penalty)
|
|
FinalSiegeMultiplier = 2.5f, // END-2: the final siege is ~2.5x the would-be-next normal siege
|
|
};
|
|
|
|
/// <summary>Clamp a knob to its safe floor: tick knobs >= 1, value knobs >= 0. Used by every write path
|
|
/// (<see cref="Apply"/> + the client-side optimistic SetLocal) so no 0/negative ever reaches a consumer.</summary>
|
|
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.CoreDamagePerHusk:
|
|
case TuningKnob.CoreOverrunDrainPct:
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>Authoritatively set one knob (clamped) by its <see cref="TuningKnob"/> index. Unknown index = no-op.</summary>
|
|
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.CoreDamagePerHusk: c.CoreDamagePerHusk = value; break;
|
|
case TuningKnob.CoreRegenIntervalTicks: c.CoreRegenIntervalTicks = value; break;
|
|
case TuningKnob.CoreOverrunDrainPct: c.CoreOverrunDrainPct = value; break;
|
|
case TuningKnob.FinalSiegeMultiplier: c.FinalSiegeMultiplier = value; break;
|
|
// unknown index -> no-op (matches the no-default switch convention in DebugCommandReceiveSystem)
|
|
}
|
|
}
|
|
|
|
/// <summary>Read one knob by its <see cref="TuningKnob"/> index (overlay display). Unknown index = 0.</summary>
|
|
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.CoreDamagePerHusk: return c.CoreDamagePerHusk;
|
|
case TuningKnob.CoreRegenIntervalTicks: return c.CoreRegenIntervalTicks;
|
|
case TuningKnob.CoreOverrunDrainPct: return c.CoreOverrunDrainPct;
|
|
case TuningKnob.FinalSiegeMultiplier: return c.FinalSiegeMultiplier;
|
|
default: return 0f;
|
|
}
|
|
}
|
|
|
|
/// <summary>Project the full config onto the wire snapshot.</summary>
|
|
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,
|
|
CoreDamagePerHusk = c.CoreDamagePerHusk,
|
|
CoreRegenIntervalTicks = c.CoreRegenIntervalTicks,
|
|
CoreOverrunDrainPct = c.CoreOverrunDrainPct,
|
|
FinalSiegeMultiplier = c.FinalSiegeMultiplier,
|
|
};
|
|
|
|
/// <summary>Reconstruct the full config from a wire snapshot (FULL state, not a delta).</summary>
|
|
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,
|
|
CoreDamagePerHusk = r.CoreDamagePerHusk,
|
|
CoreRegenIntervalTicks = r.CoreRegenIntervalTicks,
|
|
CoreOverrunDrainPct = r.CoreOverrunDrainPct,
|
|
FinalSiegeMultiplier = r.FinalSiegeMultiplier,
|
|
};
|
|
}
|
|
|
|
/// <summary>Byte indices for <see cref="TuningConfig"/> knobs (bytes — never an enum on a Bursted/RPC path).</summary>
|
|
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;
|
|
public const byte CoreDamagePerHusk = 20;
|
|
public const byte CoreRegenIntervalTicks = 21;
|
|
public const byte CoreOverrunDrainPct = 22;
|
|
public const byte FinalSiegeMultiplier = 23;
|
|
|
|
/// <summary>Knob count (overlay iteration bound).</summary>
|
|
public const byte Count = 24;
|
|
}
|
|
|
|
/// <summary>
|
|
/// MC-0 — server → dev-client snapshot of the full <see cref="TuningConfig"/> (sent periodically by the
|
|
/// editor-only broadcaster). UNCONDITIONAL wire type (like <see cref="DebugTelemetryReport"/>) for RpcCollection
|
|
/// hash parity; only the send/receive SYSTEMS are <c>#if UNITY_EDITOR</c>. FULL state (not a delta) so a
|
|
/// late-joining client converges in one report.
|
|
/// </summary>
|
|
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 CoreDamagePerHusk;
|
|
public float CoreRegenIntervalTicks;
|
|
public float CoreOverrunDrainPct;
|
|
public float FinalSiegeMultiplier;
|
|
}
|
|
}
|