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:
@@ -87,6 +87,8 @@ namespace ProjectM.Authoring
|
|||||||
// MC-1 dash: predicted dash window (derived from PlayerInput.Dash) + cooldown gate, baked idle/ready.
|
// MC-1 dash: predicted dash window (derived from PlayerInput.Dash) + cooldown gate, baked idle/ready.
|
||||||
AddComponent<DashState>(entity);
|
AddComponent<DashState>(entity);
|
||||||
AddComponent(entity, new DashCooldown { NextTick = 0 });
|
AddComponent(entity, new DashCooldown { NextTick = 0 });
|
||||||
|
// MC-4 melee combo: predicted, owner-replicated combo anchor (Step/SwingStartTick/LockUntilTick), baked idle/zero.
|
||||||
|
AddComponent<MeleeCombo>(entity);
|
||||||
|
|
||||||
// Death gate (enableable, derived from Health by PlayerDeathStateSystem) baked DISABLED = alive;
|
// Death gate (enableable, derived from Health by PlayerDeathStateSystem) baked DISABLED = alive;
|
||||||
// plus the server-only respawn timer.
|
// plus the server-only respawn timer.
|
||||||
|
|||||||
@@ -94,6 +94,16 @@ namespace ProjectM.Client
|
|||||||
TuningRow("Chgr lunge t", TuningKnob.ChargerLungeDurationTicks, 1f, "0");
|
TuningRow("Chgr lunge t", TuningKnob.ChargerLungeDurationTicks, 1f, "0");
|
||||||
TuningRow("Chgr stagger t", TuningKnob.ChargerWhiffStaggerTicks, 1f, "0");
|
TuningRow("Chgr stagger t", TuningKnob.ChargerWhiffStaggerTicks, 1f, "0");
|
||||||
TuningRow("Grunt windup t", TuningKnob.GruntWindupTicks, 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();
|
GUILayout.EndScrollView();
|
||||||
|
|||||||
@@ -71,13 +71,16 @@ namespace ProjectM.Client
|
|||||||
|
|
||||||
// Movement is source-agnostic (WASD or left stick) — read from the merged action.
|
// Movement is source-agnostic (WASD or left stick) — read from the merged action.
|
||||||
float2 move = (float2)gameplay.Move.ReadValue<UnityEngine.Vector2>();
|
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 ---
|
// --- Active-device detection: last meaningful actuation wins; hold last when idle ---
|
||||||
var gamepad = UnityEngine.InputSystem.Gamepad.current;
|
var gamepad = UnityEngine.InputSystem.Gamepad.current;
|
||||||
var mouse = UnityEngine.InputSystem.Mouse.current;
|
var mouse = UnityEngine.InputSystem.Mouse.current;
|
||||||
var keyboard = UnityEngine.InputSystem.Keyboard.current;
|
var keyboard = UnityEngine.InputSystem.Keyboard.current;
|
||||||
bool dashPressed = ((keyboard != null && keyboard.leftShiftKey.wasPressedThisFrame) || (gamepad != null && gamepad.buttonEast.wasPressedThisFrame)) && !BuildPaletteState.Active;
|
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;
|
float2 rightStick = float2.zero;
|
||||||
bool gamepadActive = false;
|
bool gamepadActive = false;
|
||||||
@@ -164,6 +167,9 @@ namespace ProjectM.Client
|
|||||||
input.ValueRW.Dash = default;
|
input.ValueRW.Dash = default;
|
||||||
if (dashPressed)
|
if (dashPressed)
|
||||||
input.ValueRW.Dash.Set();
|
input.ValueRW.Dash.Set();
|
||||||
|
input.ValueRW.Attack = default;
|
||||||
|
if (attackPressed)
|
||||||
|
input.ValueRW.Attack.Set();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -53,17 +53,21 @@ namespace ProjectM.Client
|
|||||||
ParticleSystem _deathFx;
|
ParticleSystem _deathFx;
|
||||||
ParticleSystem _muzzleFx;
|
ParticleSystem _muzzleFx;
|
||||||
ParticleSystem _dashFx;
|
ParticleSystem _dashFx;
|
||||||
|
ParticleSystem _swingFx;
|
||||||
AudioClip _hitClip;
|
AudioClip _hitClip;
|
||||||
AudioClip _deathClip;
|
AudioClip _deathClip;
|
||||||
AudioClip _fireClip;
|
AudioClip _fireClip;
|
||||||
AudioClip _telegraphClip;
|
AudioClip _telegraphClip;
|
||||||
AudioClip _dashClip;
|
AudioClip _dashClip;
|
||||||
|
AudioClip _swingClip;
|
||||||
|
|
||||||
Entity _localPlayer = Entity.Null;
|
Entity _localPlayer = Entity.Null;
|
||||||
uint _lastLocalFireTick;
|
uint _lastLocalFireTick;
|
||||||
bool _fireTickInit;
|
bool _fireTickInit;
|
||||||
uint _lastLocalDashTick;
|
uint _lastLocalDashTick;
|
||||||
bool _dashTickInit;
|
bool _dashTickInit;
|
||||||
|
uint _lastLocalSwingTick;
|
||||||
|
bool _swingTickInit;
|
||||||
|
|
||||||
const int NumberPoolSize = 32;
|
const int NumberPoolSize = 32;
|
||||||
const int MaxActiveVfx = 40; // bound one-shot VFX GameObject churn under sustained combat
|
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);
|
_fireClip = MakeClip("fire", 880f, 1500f, 0.07f, 0.30f, noise: false);
|
||||||
_telegraphClip = MakeClip("telegraph", 680f, 1020f, 0.12f, 0.35f, noise: false);
|
_telegraphClip = MakeClip("telegraph", 680f, 1020f, 0.12f, 0.35f, noise: false);
|
||||||
_dashClip = MakeClip("dash", 950f, 240f, 0.12f, 0.50f, 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()
|
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);
|
_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);
|
_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);
|
_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++)
|
for (int i = 0; i < NumberPoolSize; i++)
|
||||||
_numbers.Add(CreateNumber());
|
_numbers.Add(CreateNumber());
|
||||||
@@ -112,6 +118,7 @@ namespace ProjectM.Client
|
|||||||
EntityManager.CompleteDependencyBeforeRO<AttackWindup>();
|
EntityManager.CompleteDependencyBeforeRO<AttackWindup>();
|
||||||
EntityManager.CompleteDependencyBeforeRO<DashState>();
|
EntityManager.CompleteDependencyBeforeRO<DashState>();
|
||||||
EntityManager.CompleteDependencyBeforeRO<DashCooldown>();
|
EntityManager.CompleteDependencyBeforeRO<DashCooldown>();
|
||||||
|
EntityManager.CompleteDependencyBeforeRO<MeleeCombo>();
|
||||||
|
|
||||||
// Resolve the local player (for hit colouring + fire feedback).
|
// Resolve the local player (for hit colouring + fire feedback).
|
||||||
_localPlayer = Entity.Null;
|
_localPlayer = Entity.Null;
|
||||||
@@ -242,6 +249,31 @@ namespace ProjectM.Client
|
|||||||
EmitAt(_dashFx, (Vector3)localPos + Vector3.up * 0.7f, FeelConfig.DashShimmerPerFrame);
|
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);
|
UpdateProjectileTrails(cfg);
|
||||||
PruneVfx();
|
PruneVfx();
|
||||||
AnimateNumbers(dt, cam);
|
AnimateNumbers(dt, cam);
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
using Unity.Mathematics;
|
||||||
|
|
||||||
|
namespace ProjectM.Simulation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Pure, deterministic cone hit-test for the MC-4 melee cleave. Shares the EXACT range + bearing predicate with
|
||||||
|
/// <see cref="AutoTarget"/> (planar XZ distance gate + <c>dot(bearing, facing) >= cos(halfAngle)</c>) but as a
|
||||||
|
/// per-candidate boolean, so the cleave can collect ALL enemies in the cone — <see cref="AutoTarget.Resolve"/> is a
|
||||||
|
/// single-winner reducer and cannot be reused for collect-all (MC-4 review REUSE-1). Burst-safe, allocation-free,
|
||||||
|
/// no wall-clock / no randomness; intended to run server-side inside the predicted <c>MeleeComboSystem</c>.
|
||||||
|
/// </summary>
|
||||||
|
public static class MeleeConeMath
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// True iff <paramref name="targetPos"/> lies within <paramref name="range"/> (planar XZ) of
|
||||||
|
/// <paramref name="from"/> AND its bearing from <paramref name="from"/> is within the cone half-angle of
|
||||||
|
/// <paramref name="facingDir"/> (<paramref name="cosHalfAngle"/> = cos(halfAngle)). A coincident
|
||||||
|
/// (zero-distance) target is excluded (undefined bearing). <paramref name="facingDir"/> must be
|
||||||
|
/// caller-normalized; a zero-length facing or a non-positive range returns false.
|
||||||
|
/// </summary>
|
||||||
|
public static bool InCone(float3 from, float2 facingDir, float range, float cosHalfAngle, float3 targetPos)
|
||||||
|
{
|
||||||
|
if (range <= 0f || math.lengthsq(facingDir) < 1e-6f)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
float3 offset = targetPos - from;
|
||||||
|
float2 planar = new float2(offset.x, offset.z);
|
||||||
|
float distSq = math.lengthsq(planar);
|
||||||
|
|
||||||
|
if (distSq < 1e-6f)
|
||||||
|
return false; // coincident -> undefined bearing
|
||||||
|
if (distSq > range * range)
|
||||||
|
return false; // out of range
|
||||||
|
|
||||||
|
float2 bearing = planar * math.rsqrt(distSq); // normalized planar bearing
|
||||||
|
return math.dot(bearing, facingDir) >= cosHalfAngle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 4f2769f11116e304d8674e254ef9bd75
|
||||||
@@ -5,14 +5,14 @@ using Unity.NetCode;
|
|||||||
namespace ProjectM.Simulation
|
namespace ProjectM.Simulation
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <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,
|
/// 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
|
/// mutate, broadcast and receive it are <c>#if UNITY_EDITOR</c>. Consumers (DashSystem, EnemyAISystem,
|
||||||
/// via <c>TryGetSingleton</c> and FALL BACK to <see cref="Defaults"/> when it is absent (release builds +
|
/// MeleeComboSystem) read it via <c>TryGetSingleton</c> and FALL BACK to <see cref="Defaults"/> when it is absent
|
||||||
/// EditMode worlds), so behaviour is identical to the old baked consts when no dev singleton exists. NOT a
|
/// (release builds + EditMode worlds), so behaviour is identical to the old baked consts when no dev singleton
|
||||||
/// <c>[GhostField]</c> (no ghost-hash change / re-bake); the server broadcasts it to clients via
|
/// 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
|
/// <see cref="DebugTuningReport"/> so a PREDICTING client's DashSystem/MeleeComboSystem stays in sync (an MPPM thin
|
||||||
/// overlay learns tuned values ONLY through this broadcast).
|
/// client with no overlay learns tuned values ONLY through this broadcast).
|
||||||
/// <para>
|
/// <para>
|
||||||
/// Values match the historical baked consts (DashSystem / EnemyAISystem / <see cref="Tuning.AttackWindupTicks"/>);
|
/// 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
|
/// <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 ChargerWhiffStaggerTicks;
|
||||||
public float GruntWindupTicks;
|
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>
|
/// <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
|
public static TuningConfig Defaults() => new TuningConfig
|
||||||
{
|
{
|
||||||
@@ -47,6 +59,15 @@ namespace ProjectM.Simulation
|
|||||||
ChargerLungeDurationTicks = 18f,
|
ChargerLungeDurationTicks = 18f,
|
||||||
ChargerWhiffStaggerTicks = 36f,
|
ChargerWhiffStaggerTicks = 36f,
|
||||||
GruntWindupTicks = Tuning.AttackWindupTicks, // canonical Grunt-windup source (TelegraphTests couples to it)
|
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
|
/// <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.DashDistance:
|
||||||
case TuningKnob.DashSharpness:
|
case TuningKnob.DashSharpness:
|
||||||
case TuningKnob.ChargerLungeSpeed:
|
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);
|
return math.max(0f, value);
|
||||||
// tick knobs: >= 1 (a 0 tick count is degenerate; a 0 i-frame window divides-by-zero in DashSystem)
|
// tick knobs: >= 1 (a 0 tick count is degenerate; a 0 i-frame window divides-by-zero in DashSystem)
|
||||||
default:
|
default:
|
||||||
@@ -82,6 +109,15 @@ namespace ProjectM.Simulation
|
|||||||
case TuningKnob.ChargerLungeDurationTicks: c.ChargerLungeDurationTicks = value; break;
|
case TuningKnob.ChargerLungeDurationTicks: c.ChargerLungeDurationTicks = value; break;
|
||||||
case TuningKnob.ChargerWhiffStaggerTicks: c.ChargerWhiffStaggerTicks = value; break;
|
case TuningKnob.ChargerWhiffStaggerTicks: c.ChargerWhiffStaggerTicks = value; break;
|
||||||
case TuningKnob.GruntWindupTicks: c.GruntWindupTicks = 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)
|
// 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.ChargerLungeDurationTicks: return c.ChargerLungeDurationTicks;
|
||||||
case TuningKnob.ChargerWhiffStaggerTicks: return c.ChargerWhiffStaggerTicks;
|
case TuningKnob.ChargerWhiffStaggerTicks: return c.ChargerWhiffStaggerTicks;
|
||||||
case TuningKnob.GruntWindupTicks: return c.GruntWindupTicks;
|
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;
|
default: return 0f;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -118,6 +163,15 @@ namespace ProjectM.Simulation
|
|||||||
ChargerLungeDurationTicks = c.ChargerLungeDurationTicks,
|
ChargerLungeDurationTicks = c.ChargerLungeDurationTicks,
|
||||||
ChargerWhiffStaggerTicks = c.ChargerWhiffStaggerTicks,
|
ChargerWhiffStaggerTicks = c.ChargerWhiffStaggerTicks,
|
||||||
GruntWindupTicks = c.GruntWindupTicks,
|
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>
|
/// <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,
|
ChargerLungeDurationTicks = r.ChargerLungeDurationTicks,
|
||||||
ChargerWhiffStaggerTicks = r.ChargerWhiffStaggerTicks,
|
ChargerWhiffStaggerTicks = r.ChargerWhiffStaggerTicks,
|
||||||
GruntWindupTicks = r.GruntWindupTicks,
|
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 ChargerLungeDurationTicks = 7;
|
||||||
public const byte ChargerWhiffStaggerTicks = 8;
|
public const byte ChargerWhiffStaggerTicks = 8;
|
||||||
public const byte GruntWindupTicks = 9;
|
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>
|
/// <summary>Knob count (overlay iteration bound).</summary>
|
||||||
public const byte Count = 10;
|
public const byte Count = 19;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -172,5 +244,14 @@ namespace ProjectM.Simulation
|
|||||||
public float ChargerLungeDurationTicks;
|
public float ChargerLungeDurationTicks;
|
||||||
public float ChargerWhiffStaggerTicks;
|
public float ChargerWhiffStaggerTicks;
|
||||||
public float GruntWindupTicks;
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
using Unity.Entities;
|
||||||
|
using Unity.NetCode;
|
||||||
|
|
||||||
|
namespace ProjectM.Simulation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// MC-4 — predicted, owner-replicated melee combo state on the player (the PRIMARY offense verb). UNLIKE the dash
|
||||||
|
/// (DashState is non-replicated because a dash is a STATELESS-per-tick decision), the combo <see cref="Step"/> is
|
||||||
|
/// PATH-DEPENDENT — which chain link you are on depends on the sequence + timing of prior presses — and the bounded
|
||||||
|
/// input command buffer cannot reconstruct it across a reconnect / long rollback (MC-4 review PRED-1 / ROLLBACK-1).
|
||||||
|
/// So the minimal anchor is replicated as owner-predicted <c>[GhostField]</c>s: a rollback restores the
|
||||||
|
/// authoritative combo position, then MeleeComboSystem re-simulates forward with ABSOLUTE-WRITE windows (never an
|
||||||
|
/// in-place prev+1 of a non-restored field — the DashSystem idempotency idiom). The derived chain deadline
|
||||||
|
/// (LockUntilTick + grace) is NOT stored. All ticks routed through <c>TickUtil.NonZero</c>; compared via
|
||||||
|
/// <see cref="NetworkTick"/> only. Baked all-zero (idle). This is the player ghost's only net-new replicated melee
|
||||||
|
/// state (one re-bake; in-family with DashCooldown/AbilityCooldown).
|
||||||
|
/// </summary>
|
||||||
|
public struct MeleeCombo : IComponentData
|
||||||
|
{
|
||||||
|
/// <summary>Current/last swing index: 0 = idle, 1..N = chain link. Owner-predicted so a rollback restores the
|
||||||
|
/// authoritative combo position (the only legitimately path-dependent value).</summary>
|
||||||
|
[GhostField] public byte Step;
|
||||||
|
|
||||||
|
/// <summary>Raw ServerTick the current swing started (NonZero). Inclusive lower bound of the movement-commit
|
||||||
|
/// window; also the juice edge signal and the (instant) damage frame.</summary>
|
||||||
|
[GhostField] public uint SwingStartTick;
|
||||||
|
|
||||||
|
/// <summary>Raw ServerTick the swing's movement-commit / recovery lock ends (NonZero). Gates the next press
|
||||||
|
/// (locked while now < this) and anchors the chain window [LockUntilTick, LockUntilTick + grace).</summary>
|
||||||
|
[GhostField] public uint LockUntilTick;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 0dfbfd9f7db58e84e8bc59402cebaafb
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
using Unity.Burst;
|
||||||
|
using Unity.Collections;
|
||||||
|
using Unity.Entities;
|
||||||
|
using Unity.Mathematics;
|
||||||
|
using Unity.NetCode;
|
||||||
|
using Unity.Transforms;
|
||||||
|
|
||||||
|
namespace ProjectM.Simulation
|
||||||
|
{
|
||||||
|
/// <summary>One server-side cleave queued from a player's swing-start this tick, resolved after the player loop so
|
||||||
|
/// the living-enemy snapshot is gathered ONCE and ONLY when a swing actually fired (never on the client, never on an
|
||||||
|
/// idle tick). Blittable (Burst-friendly).</summary>
|
||||||
|
struct PendingCleave
|
||||||
|
{
|
||||||
|
public float3 From;
|
||||||
|
public float2 Face;
|
||||||
|
public float Damage;
|
||||||
|
public float Range;
|
||||||
|
public float KnockSpeed;
|
||||||
|
public int OwnerId;
|
||||||
|
public uint Stamp;
|
||||||
|
public uint KnockUntil;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// MC-4 — the predicted melee combo (Hades-style 2-3 hit chain; the player's PRIMARY verb). On a fresh
|
||||||
|
/// <see cref="PlayerInput.Attack"/> press (not locked, not mid-dash) it ADVANCES <see cref="MeleeCombo.Step"/>
|
||||||
|
/// (chain if re-pressed inside [LockUntilTick, LockUntilTick + grace), else reset to 1) and opens a movement-commit
|
||||||
|
/// lock; the finisher (Step == ComboLength) hits bigger. The Step/SwingStartTick/LockUntilTick anchor is
|
||||||
|
/// owner-replicated (<see cref="MeleeCombo"/> [GhostField]s) so a rollback restores the authoritative combo
|
||||||
|
/// position; every write is an ABSOLUTE function of (restored Step, tick) — re-running a tick re-derives identical
|
||||||
|
/// state (the DashSystem idempotency idiom; never an in-place prev+1 of a non-restored field — MC-4 review PRED-1).
|
||||||
|
/// <para>
|
||||||
|
/// Runs in <see cref="PredictedSimulationSystemGroup"/> AFTER <see cref="PlayerControlSystem"/> (it scales the
|
||||||
|
/// MoveVelocity that system wrote) and BEFORE <see cref="DashSystem"/> (a dash OVERRIDES the swing's movement =
|
||||||
|
/// dash-cancel) — and so before HealthApplyDamageSystem ([UpdateAfter(DashSystem)]) which drains the cleave's
|
||||||
|
/// DamageEvent the same tick. Movement-commit re-applies every predicted pass, lower-bounded on SwingStartTick (a
|
||||||
|
/// re-simulated pre-swing tick must NOT inherit the scale — the DashSystem inDashWindow fix). DAMAGE is SERVER-ONLY
|
||||||
|
/// (enemies are interpolated ghosts; the client never predicts enemy health) and mirrors ProjectileDamageSystem:
|
||||||
|
/// queue each swing, then collect ALL living enemies in the per-step cone (<see cref="MeleeConeMath"/>) ONCE, append
|
||||||
|
/// a SourceTick-stamped DamageEvent + stamp KnockbackState. All ticks via TickUtil.NonZero, compared with
|
||||||
|
/// NetworkTick only; feel knobs live in <see cref="TuningConfig"/> (MC-0, fallback to Defaults()).
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
|
||||||
|
[UpdateAfter(typeof(PlayerControlSystem))]
|
||||||
|
[UpdateBefore(typeof(DashSystem))]
|
||||||
|
[BurstCompile]
|
||||||
|
public partial struct MeleeComboSystem : ISystem
|
||||||
|
{
|
||||||
|
ComponentLookup<KnockbackState> m_KnockbackLookup;
|
||||||
|
|
||||||
|
[BurstCompile]
|
||||||
|
public void OnCreate(ref SystemState state)
|
||||||
|
{
|
||||||
|
m_KnockbackLookup = state.GetComponentLookup<KnockbackState>(isReadOnly: false);
|
||||||
|
state.RequireForUpdate<NetworkTime>();
|
||||||
|
}
|
||||||
|
|
||||||
|
[BurstCompile]
|
||||||
|
public void OnUpdate(ref SystemState state)
|
||||||
|
{
|
||||||
|
if (!SystemAPI.TryGetSingleton<NetworkTime>(out var netTime) || !netTime.ServerTick.IsValid)
|
||||||
|
return;
|
||||||
|
var serverTick = netTime.ServerTick;
|
||||||
|
uint now = serverTick.TickIndexForValidTick;
|
||||||
|
|
||||||
|
var t = SystemAPI.TryGetSingleton<TuningConfig>(out var tc) ? tc : TuningConfig.Defaults();
|
||||||
|
float baseDamage = math.max(0f, t.MeleeDamage);
|
||||||
|
float baseRange = math.max(0f, t.MeleeRange);
|
||||||
|
float cosHalf = math.cos(math.max(0f, t.MeleeConeHalfAngleRad));
|
||||||
|
uint recoverTicks = (uint)math.max(1f, t.MeleeRecoverTicks);
|
||||||
|
uint graceTicks = (uint)math.max(1f, t.MeleeChainGraceTicks);
|
||||||
|
float moveScale = math.max(0f, t.MeleeSwingMoveScale);
|
||||||
|
float knockSpeed = math.max(0f, t.MeleeKnockbackSpeed);
|
||||||
|
float finisherMult = math.max(1f, t.MeleeFinisherMult);
|
||||||
|
byte comboLen = (byte)math.clamp((int)t.MeleeComboLength, 1, 3);
|
||||||
|
uint stamp = TickUtil.NonZero(now);
|
||||||
|
uint knockUntil = TickUtil.NonZero(now + (uint)math.max(1, Tuning.KnockbackDurationTicks));
|
||||||
|
|
||||||
|
bool isServer = state.WorldUnmanaged.IsServer();
|
||||||
|
|
||||||
|
// Server-only queue of cleaves to resolve after the player loop (so enemies are gathered ONCE, and only
|
||||||
|
// when at least one swing actually started — no per-tick enemy gather on idle/client ticks).
|
||||||
|
var cleaves = isServer ? new NativeList<PendingCleave>(Allocator.Temp) : default;
|
||||||
|
|
||||||
|
foreach (var (mc, control, input, facing, xform, owner, ds) in
|
||||||
|
SystemAPI.Query<RefRW<MeleeCombo>, RefRW<CharacterControl>, RefRO<PlayerInput>,
|
||||||
|
RefRO<PlayerFacing>, RefRO<LocalTransform>, RefRO<GhostOwner>, RefRO<DashState>>()
|
||||||
|
.WithAll<Simulate>().WithDisabled<Dead>())
|
||||||
|
{
|
||||||
|
// A dash window (i-frame OR recovery) active = dash owns movement + blocks a swing start (dash-cancel).
|
||||||
|
bool dashActive = ds.ValueRO.StartTick != 0u
|
||||||
|
&& !new NetworkTick(ds.ValueRO.StartTick).IsNewerThan(serverTick)
|
||||||
|
&& ds.ValueRO.RecoverUntilTick != 0u
|
||||||
|
&& new NetworkTick(ds.ValueRO.RecoverUntilTick).IsNewerThan(serverTick);
|
||||||
|
|
||||||
|
// Locked: mid swing / recovery (now < LockUntilTick).
|
||||||
|
bool locked = mc.ValueRO.LockUntilTick != 0u
|
||||||
|
&& new NetworkTick(mc.ValueRO.LockUntilTick).IsNewerThan(serverTick);
|
||||||
|
|
||||||
|
// --- ADVANCE (idempotent ABSOLUTE writes; dash wins same-tick ties via !Dash.IsSet) ---
|
||||||
|
bool swingStarted = false;
|
||||||
|
byte swingStep = 0;
|
||||||
|
if (input.ValueRO.Attack.IsSet && !locked && !dashActive && !input.ValueRO.Dash.IsSet)
|
||||||
|
{
|
||||||
|
bool inChainWindow = false;
|
||||||
|
if (mc.ValueRO.LockUntilTick != 0u)
|
||||||
|
{
|
||||||
|
var deadline = new NetworkTick(TickUtil.NonZero(mc.ValueRO.LockUntilTick + graceTicks));
|
||||||
|
inChainWindow = deadline.IsValid && deadline.IsNewerThan(serverTick); // now < lock+grace (now >= lock since !locked)
|
||||||
|
}
|
||||||
|
byte prev = mc.ValueRO.Step;
|
||||||
|
swingStep = (inChainWindow && prev >= 1 && prev < comboLen) ? (byte)(prev + 1) : (byte)1;
|
||||||
|
|
||||||
|
bool isFin = swingStep >= comboLen;
|
||||||
|
uint stepRecover = isFin ? (uint)math.max(1f, math.round(recoverTicks * finisherMult)) : recoverTicks;
|
||||||
|
|
||||||
|
mc.ValueRW.Step = swingStep;
|
||||||
|
mc.ValueRW.SwingStartTick = TickUtil.NonZero(now);
|
||||||
|
mc.ValueRW.LockUntilTick = TickUtil.NonZero(now + stepRecover);
|
||||||
|
swingStarted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- MOVEMENT COMMIT (every pass; lower-bounded on SwingStartTick; DashSystem overrides later) ---
|
||||||
|
bool inSwingWindow = mc.ValueRO.SwingStartTick != 0u
|
||||||
|
&& !new NetworkTick(mc.ValueRO.SwingStartTick).IsNewerThan(serverTick)
|
||||||
|
&& mc.ValueRO.LockUntilTick != 0u
|
||||||
|
&& new NetworkTick(mc.ValueRO.LockUntilTick).IsNewerThan(serverTick);
|
||||||
|
if (inSwingWindow && !dashActive)
|
||||||
|
control.ValueRW.MoveVelocity *= moveScale;
|
||||||
|
|
||||||
|
// --- SERVER: queue the cleave (resolved after the loop). Damage/cone are SERVER-authoritative. ---
|
||||||
|
if (swingStarted && isServer)
|
||||||
|
{
|
||||||
|
bool isFin = swingStep >= comboLen;
|
||||||
|
float2 face = facing.ValueRO.Direction;
|
||||||
|
face = math.lengthsq(face) < 1e-6f ? new float2(0f, 1f) : math.normalize(face);
|
||||||
|
cleaves.Add(new PendingCleave
|
||||||
|
{
|
||||||
|
From = xform.ValueRO.Position,
|
||||||
|
Face = face,
|
||||||
|
Damage = isFin ? baseDamage * finisherMult : baseDamage,
|
||||||
|
Range = isFin ? baseRange * finisherMult : baseRange,
|
||||||
|
KnockSpeed = isFin ? knockSpeed * finisherMult : knockSpeed,
|
||||||
|
OwnerId = owner.ValueRO.NetworkId,
|
||||||
|
Stamp = stamp,
|
||||||
|
KnockUntil = knockUntil,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve queued cleaves SERVER-ONLY and only when one actually fired: gather living enemies ONCE, then
|
||||||
|
// append a SourceTick-stamped DamageEvent + stamp KnockbackState for each enemy in each swing's cone.
|
||||||
|
if (isServer && cleaves.IsCreated && cleaves.Length > 0)
|
||||||
|
{
|
||||||
|
var enemyEntities = new NativeList<Entity>(Allocator.Temp);
|
||||||
|
var enemyPositions = new NativeList<float3>(Allocator.Temp);
|
||||||
|
foreach (var (xform, health, enemyEntity) in
|
||||||
|
SystemAPI.Query<RefRO<LocalTransform>, RefRO<Health>>()
|
||||||
|
.WithAny<EnemyTag, TrainingDummyTag>()
|
||||||
|
.WithEntityAccess())
|
||||||
|
{
|
||||||
|
if (health.ValueRO.Current <= 0f)
|
||||||
|
continue; // skip already-dead enemies (about to despawn)
|
||||||
|
enemyEntities.Add(enemyEntity);
|
||||||
|
enemyPositions.Add(xform.ValueRO.Position);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_KnockbackLookup.Update(ref state);
|
||||||
|
var ecb = new EntityCommandBuffer(Allocator.Temp);
|
||||||
|
for (int s = 0; s < cleaves.Length; s++)
|
||||||
|
{
|
||||||
|
var c = cleaves[s];
|
||||||
|
for (int i = 0; i < enemyEntities.Length; i++)
|
||||||
|
{
|
||||||
|
if (!MeleeConeMath.InCone(c.From, c.Face, c.Range, cosHalf, enemyPositions[i]))
|
||||||
|
continue;
|
||||||
|
var target = enemyEntities[i];
|
||||||
|
ecb.AppendToBuffer(target, new DamageEvent
|
||||||
|
{
|
||||||
|
Amount = c.Damage,
|
||||||
|
SourceNetworkId = c.OwnerId,
|
||||||
|
SourceTick = c.Stamp,
|
||||||
|
});
|
||||||
|
if (c.KnockSpeed > 0f && m_KnockbackLookup.HasComponent(target))
|
||||||
|
{
|
||||||
|
float3 d3 = enemyPositions[i] - c.From;
|
||||||
|
float2 kdir = new float2(d3.x, d3.z);
|
||||||
|
kdir = math.lengthsq(kdir) > 1e-6f ? math.normalize(kdir) : c.Face;
|
||||||
|
m_KnockbackLookup[target] = new KnockbackState { Dir = kdir, Speed = c.KnockSpeed, UntilTick = c.KnockUntil };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ecb.Playback(state.EntityManager);
|
||||||
|
ecb.Dispose();
|
||||||
|
enemyEntities.Dispose();
|
||||||
|
enemyPositions.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleaves.IsCreated)
|
||||||
|
cleaves.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 815ef85fd906e3640b787de314638027
|
||||||
@@ -42,6 +42,9 @@ namespace ProjectM.Simulation
|
|||||||
// no stale i-frames / stuck-fast on respawn (DashSystem skips dead players via .WithDisabled<Dead>()).
|
// no stale i-frames / stuck-fast on respawn (DashSystem skips dead players via .WithDisabled<Dead>()).
|
||||||
if (SystemAPI.HasComponent<DashState>(entity))
|
if (SystemAPI.HasComponent<DashState>(entity))
|
||||||
SystemAPI.SetComponent(entity, default(DashState));
|
SystemAPI.SetComponent(entity, default(DashState));
|
||||||
|
// MC-4: clear any in-flight combo so a death mid-combo leaves no stale lock/step on respawn.
|
||||||
|
if (SystemAPI.HasComponent<MeleeCombo>(entity))
|
||||||
|
SystemAPI.SetComponent(entity, default(MeleeCombo));
|
||||||
if (SystemAPI.HasComponent<CharacterComponent>(entity))
|
if (SystemAPI.HasComponent<CharacterComponent>(entity))
|
||||||
{
|
{
|
||||||
var cc = SystemAPI.GetComponent<CharacterComponent>(entity);
|
var cc = SystemAPI.GetComponent<CharacterComponent>(entity);
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ namespace ProjectM.Simulation
|
|||||||
/// so one press dashes exactly once; read by the predicted DashSystem (MC-1).</summary>
|
/// so one press dashes exactly once; read by the predicted DashSystem (MC-1).</summary>
|
||||||
[GhostField] public InputEvent Dash;
|
[GhostField] public InputEvent Dash;
|
||||||
|
|
||||||
|
/// <summary>Melee combo attack (MC-4) - the player's PRIMARY verb. InputEvent twin of Fire/Dash: one press =
|
||||||
|
/// one swing attempt across the frame->tick->rollback boundary; read by the predicted MeleeComboSystem.</summary>
|
||||||
|
[GhostField] public InputEvent Attack;
|
||||||
|
|
||||||
/// <summary>Active input scheme this tick (<see cref="InputSchemeId"/>: 0 = mouse/keyboard, 1 = gamepad).
|
/// <summary>Active input scheme this tick (<see cref="InputSchemeId"/>: 0 = mouse/keyboard, 1 = gamepad).
|
||||||
/// The server reads it so the auto-target assist applies only to gamepad shots; precise mouse aim is left
|
/// The server reads it so the auto-target assist applies only to gamepad shots; precise mouse aim is left
|
||||||
/// exact. A byte (not an enum): it is compared inside the Burst-compiled <c>AbilityFireSystem</c>.</summary>
|
/// exact. A byte (not an enum): it is compared inside the Burst-compiled <c>AbilityFireSystem</c>.</summary>
|
||||||
@@ -35,7 +39,7 @@ namespace ProjectM.Simulation
|
|||||||
var s = new FixedString512Bytes();
|
var s = new FixedString512Bytes();
|
||||||
s.Append(Move.x); s.Append(','); s.Append(Move.y); s.Append(';');
|
s.Append(Move.x); s.Append(','); s.Append(Move.y); s.Append(';');
|
||||||
s.Append(Aim.x); s.Append(','); s.Append(Aim.y); s.Append(';');
|
s.Append(Aim.x); s.Append(','); s.Append(Aim.y); s.Append(';');
|
||||||
s.Append(Fire.Count); s.Append(';'); s.Append(Scheme); s.Append(';'); s.Append(Dash.Count);
|
s.Append(Fire.Count); s.Append(';'); s.Append(Scheme); s.Append(';'); s.Append(Dash.Count); s.Append(';'); s.Append(Attack.Count);
|
||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user