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:
2026-06-10 17:22:57 -07:00
parent 08f16b689f
commit 3409c53148
13 changed files with 430 additions and 10 deletions
@@ -53,17 +53,21 @@ namespace ProjectM.Client
ParticleSystem _deathFx;
ParticleSystem _muzzleFx;
ParticleSystem _dashFx;
ParticleSystem _swingFx;
AudioClip _hitClip;
AudioClip _deathClip;
AudioClip _fireClip;
AudioClip _telegraphClip;
AudioClip _dashClip;
AudioClip _swingClip;
Entity _localPlayer = Entity.Null;
uint _lastLocalFireTick;
bool _fireTickInit;
uint _lastLocalDashTick;
bool _dashTickInit;
uint _lastLocalSwingTick;
bool _swingTickInit;
const int NumberPoolSize = 32;
const int MaxActiveVfx = 40; // bound one-shot VFX GameObject churn under sustained combat
@@ -77,6 +81,7 @@ namespace ProjectM.Client
_fireClip = MakeClip("fire", 880f, 1500f, 0.07f, 0.30f, noise: false);
_telegraphClip = MakeClip("telegraph", 680f, 1020f, 0.12f, 0.35f, noise: false);
_dashClip = MakeClip("dash", 950f, 240f, 0.12f, 0.50f, noise: false);
_swingClip = MakeClip("swing", 720f, 200f, 0.09f, 0.42f, noise: false);
}
protected override void OnStartRunning()
@@ -89,6 +94,7 @@ namespace ProjectM.Client
_deathFx = MakeBurst("DeathBurst", mat, new Color(3.2f, 0.7f, 0.25f), 0.22f, 9f, 0.55f, 512);
_muzzleFx = MakeBurst("Muzzle", mat, new Color(0.6f, 2.4f, 3.2f), 0.12f, 5f, 0.20f, 128);
_dashFx = MakeBurst("DashWhoosh", mat, new Color(0.7f, 2.6f, 3.0f), 0.16f, 4f, 0.30f, 256);
_swingFx = MakeBurst("MeleeSwing", mat, new Color(3.0f, 2.6f, 0.9f), 0.14f, 6f, 0.28f, 256);
for (int i = 0; i < NumberPoolSize; i++)
_numbers.Add(CreateNumber());
@@ -112,6 +118,7 @@ namespace ProjectM.Client
EntityManager.CompleteDependencyBeforeRO<AttackWindup>();
EntityManager.CompleteDependencyBeforeRO<DashState>();
EntityManager.CompleteDependencyBeforeRO<DashCooldown>();
EntityManager.CompleteDependencyBeforeRO<MeleeCombo>();
// Resolve the local player (for hit colouring + fire feedback).
_localPlayer = Entity.Null;
@@ -242,6 +249,31 @@ namespace ProjectM.Client
EmitAt(_dashFx, (Vector3)localPos + Vector3.up * 0.7f, FeelConfig.DashShimmerPerFrame);
}
// Local-player melee swing feedback (MC-4): MeleeCombo.SwingStartTick advances once per swing (owner-predicted
// [GhostField]; raw uint edge like the muzzle/dash, cosmetic only). Whoosh + arc burst + a small camera
// nudge ahead of the player; the burst scales with the combo step so the finisher visibly pops.
if (_localPlayer != Entity.Null && EntityManager.HasComponent<MeleeCombo>(_localPlayer))
{
var mc = EntityManager.GetComponentData<MeleeCombo>(_localPlayer);
if (_swingTickInit && mc.SwingStartTick != 0 && mc.SwingStartTick != _lastLocalSwingTick)
{
int step = math.max(1, (int)mc.Step);
Vector3 face = Vector3.forward;
if (EntityManager.HasComponent<PlayerFacing>(_localPlayer))
{
var d = EntityManager.GetComponentData<PlayerFacing>(_localPlayer).Direction;
if (math.lengthsq(d) > 1e-6f) face = new Vector3(d.x, 0f, d.y).normalized;
}
EmitAt(_swingFx, (Vector3)localPos + Vector3.up * 0.9f + face * 0.8f, 6 + (step - 1) * 5);
PlayClip(_swingClip, (Vector3)localPos, 0.45f);
PrototypeCameraRig.AddShake(0.04f * step);
int comboLen = SystemAPI.TryGetSingleton<TuningConfig>(out var tcfg) ? (int)math.clamp((int)tcfg.MeleeComboLength, 1, 3) : 3;
if (step >= comboLen) PrototypeCameraRig.PunchFov(FeelConfig.DashFovKick * 0.6f, FeelConfig.HitStopDurationMs); // finisher pop keyed off the live combo length (MC-4 review)
}
_lastLocalSwingTick = mc.SwingStartTick;
_swingTickInit = true;
}
UpdateProjectileTrails(cfg);
PruneVfx();
AnimateNumbers(dt, cam);