Combat: MC-4 combo-chain melee as the primary verb (DR-030)
Melee combo (left-click / pad-West) becomes the player's primary verb; the ranged projectile is demoted to right-click / pad-left-trigger. Predicted, owner-replicated combo Step (path-dependent -> [GhostField] anchor + absolute-write idempotency, NOT derived like the dash), server-only cleave mirroring ProjectileDamageSystem (SourceTick-stamped DamageEvent + KnockbackState), dash-cancellable movement-commit, 9 live TuningConfig knobs, and swing juice scaling with the combo step. The MC-6 archetype byte is deferred (the melee is its own verb). See DR-030. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,14 +5,14 @@ using Unity.NetCode;
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// MC-0 — live-tunable dash/Charger feel knobs (a per-world singleton). UNCONDITIONAL type (no #if) so the
|
||||
/// 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) 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 stays in sync (an MPPM thin client with no
|
||||
/// overlay learns tuned values ONLY through this broadcast).
|
||||
/// 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
|
||||
@@ -34,6 +34,18 @@ namespace ProjectM.Simulation
|
||||
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;
|
||||
|
||||
/// <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
|
||||
{
|
||||
@@ -47,6 +59,15 @@ namespace ProjectM.Simulation
|
||||
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
|
||||
};
|
||||
|
||||
/// <summary>Clamp a knob to its safe floor: tick knobs >= 1, value knobs >= 0. Used by every write path
|
||||
@@ -59,6 +80,12 @@ namespace ProjectM.Simulation
|
||||
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:
|
||||
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:
|
||||
@@ -82,6 +109,15 @@ namespace ProjectM.Simulation
|
||||
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;
|
||||
// unknown index -> no-op (matches the no-default switch convention in DebugCommandReceiveSystem)
|
||||
}
|
||||
}
|
||||
@@ -101,6 +137,15 @@ namespace ProjectM.Simulation
|
||||
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;
|
||||
default: return 0f;
|
||||
}
|
||||
}
|
||||
@@ -118,6 +163,15 @@ namespace ProjectM.Simulation
|
||||
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,
|
||||
};
|
||||
|
||||
/// <summary>Reconstruct the full config from a wire snapshot (FULL state, not a delta).</summary>
|
||||
@@ -133,6 +187,15 @@ namespace ProjectM.Simulation
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -149,9 +212,18 @@ namespace ProjectM.Simulation
|
||||
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;
|
||||
|
||||
/// <summary>Knob count (overlay iteration bound).</summary>
|
||||
public const byte Count = 10;
|
||||
public const byte Count = 19;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -172,5 +244,14 @@ namespace ProjectM.Simulation
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user