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:
@@ -94,6 +94,16 @@ namespace ProjectM.Client
|
||||
TuningRow("Chgr lunge t", TuningKnob.ChargerLungeDurationTicks, 1f, "0");
|
||||
TuningRow("Chgr stagger t", TuningKnob.ChargerWhiffStaggerTicks, 1f, "0");
|
||||
TuningRow("Grunt windup t", TuningKnob.GruntWindupTicks, 1f, "0");
|
||||
GUILayout.Space(4);
|
||||
TuningRow("Melee dmg", TuningKnob.MeleeDamage, 1f, "0.0");
|
||||
TuningRow("Melee range", TuningKnob.MeleeRange, 0.2f, "0.0");
|
||||
TuningRow("Melee cone rad", TuningKnob.MeleeConeHalfAngleRad, 0.05f, "0.00");
|
||||
TuningRow("Melee recover t", TuningKnob.MeleeRecoverTicks, 1f, "0");
|
||||
TuningRow("Melee chain t", TuningKnob.MeleeChainGraceTicks, 1f, "0");
|
||||
TuningRow("Melee move x", TuningKnob.MeleeSwingMoveScale, 0.05f, "0.00");
|
||||
TuningRow("Melee knock spd", TuningKnob.MeleeKnockbackSpeed, 1f, "0.0");
|
||||
TuningRow("Melee finish x", TuningKnob.MeleeFinisherMult, 0.1f, "0.0");
|
||||
TuningRow("Melee combo len", TuningKnob.MeleeComboLength, 1f, "0");
|
||||
}
|
||||
|
||||
GUILayout.EndScrollView();
|
||||
|
||||
@@ -71,13 +71,16 @@ namespace ProjectM.Client
|
||||
|
||||
// Movement is source-agnostic (WASD or left stick) — read from the merged action.
|
||||
float2 move = (float2)gameplay.Move.ReadValue<UnityEngine.Vector2>();
|
||||
bool firePressed = gameplay.Fire.WasPressedThisFrame() && !BuildPaletteState.Active; // no fire while placing a build
|
||||
// MC-4: melee (left-click / pad West) is the PRIMARY verb; ranged (right-click / pad left-trigger) the secondary poke - both read as direct device reads below (after the device locals).
|
||||
|
||||
// --- Active-device detection: last meaningful actuation wins; hold last when idle ---
|
||||
var gamepad = UnityEngine.InputSystem.Gamepad.current;
|
||||
var mouse = UnityEngine.InputSystem.Mouse.current;
|
||||
var keyboard = UnityEngine.InputSystem.Keyboard.current;
|
||||
bool dashPressed = ((keyboard != null && keyboard.leftShiftKey.wasPressedThisFrame) || (gamepad != null && gamepad.buttonEast.wasPressedThisFrame)) && !BuildPaletteState.Active;
|
||||
// MC-4 offense rebind: melee combo = PRIMARY (left-click / pad West); ranged projectile demoted to right-click / pad left-trigger. Both suppressed while placing a build (like dash/old fire).
|
||||
bool attackPressed = ((mouse != null && mouse.leftButton.wasPressedThisFrame) || (gamepad != null && gamepad.buttonWest.wasPressedThisFrame)) && !BuildPaletteState.Active;
|
||||
bool firePressed = ((mouse != null && mouse.rightButton.wasPressedThisFrame) || (gamepad != null && gamepad.leftTrigger.wasPressedThisFrame)) && !BuildPaletteState.Active;
|
||||
|
||||
float2 rightStick = float2.zero;
|
||||
bool gamepadActive = false;
|
||||
@@ -164,6 +167,9 @@ namespace ProjectM.Client
|
||||
input.ValueRW.Dash = default;
|
||||
if (dashPressed)
|
||||
input.ValueRW.Dash.Set();
|
||||
input.ValueRW.Attack = default;
|
||||
if (attackPressed)
|
||||
input.ValueRW.Attack.Set();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user