Melee feel overhaul: SwordCombat anims at heavy cadence, salvage axe, damage-at-contact + buffer + dash law (guidelines forks 1-6)
07-18/19/20 arc: LightCombo01/HeavyCombo01 clips onto Swing1-3/Slam at ~natural speed (26-tick window, 30-tick recover, DPS-held damage retune), Menacing01 combat idle (InCombat), SM_Wep_Axe_Large_01 rigid-skinned to Hand_R at x1.25, LANTERN FX retone + blade-smear ribbon. Forks 1-6 per Combat_Attack_Feel_Guidelines (design review wf_000bc247): cleave resolves at the CONTACT tick (MeleeCleavePending schedule-and-consume, knob 31, 0=legacy; connect cues moved to contact), MeleeRange 2.2 + reach-only finisher mult 1.25 (knob 30), BufferedAttackTick GhostField input buffer (knob 29, unlock-edge validity), dash refused pre-contact (lookup-based). Tests: MeleeComboTests pin legacy knobs; +3 fork tests (414 green); live server proof: HP drop exactly at swing+16 under tick batching. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,28 @@
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>07-20 melee feel forks (guidelines G2.1): the per-step CONTACT tick — how many ticks after
|
||||
/// SwingStartTick the blade actually lands. The server cleave resolves THEN (not at swing start), so the
|
||||
/// damage cone is a true record of the visible hit. Fractions derive from the AC_PlayerTopDown clip contact
|
||||
/// seconds at the wired state speeds (PlayerRigTools swaps table: A hit@0.267s, B hit@0.167s, C hit@0.333s
|
||||
/// @60Hz): step2 = x0.625 (10t at the default), step3+ (finisher) = x1.25 (20t). Knob = TuningConfig
|
||||
/// .MeleeContactTicks (default 16); <b>0 = IMMEDIATE</b> (legacy same-tick resolve; the mechanics tests pin 0).
|
||||
/// COUPLING (documented, not clamped): keep contact(step3) < CastFacingTicks (26) and < MeleeRecoverTicks —
|
||||
/// the cast-turn/anim window and chain cadence assume the hit lands inside both. The cleave itself is safe at
|
||||
/// ANY knob value (MeleeComboSystem flushes a still-armed pending cleave before overwriting the swing).</summary>
|
||||
public static class MeleeTiming
|
||||
{
|
||||
/// <summary>Server tick-batching slack: the buffered-attack validity window past the unlock edge must
|
||||
/// cover Netcode's default MaxSimulationStepBatchSize (4) or an edge press drops under load (design
|
||||
/// review wf_000bc247 C1).</summary>
|
||||
public const uint BatchSlackTicks = 4;
|
||||
|
||||
public static uint ContactTicks(byte step, float contactKnob)
|
||||
{
|
||||
if (contactKnob <= 0f) return 0u; // 0 = immediate (legacy same-tick resolve)
|
||||
float f = step == 2 ? 0.625f : step >= 3 ? 1.25f : 1f;
|
||||
return (uint)math.max(0f, math.round(contactKnob * f));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4354c9c60079e0a44a0985ef7065d574
|
||||
@@ -46,6 +46,14 @@ namespace ProjectM.Simulation
|
||||
public float MeleeFinisherMult;
|
||||
public float MeleeComboLength;
|
||||
|
||||
// 07-20 melee feel forks (guidelines G2/G7, design review wf_000bc247):
|
||||
/// <summary>Finisher REACH multiplier (reach honesty G2.2) — MeleeFinisherMult keeps damage/recover/knockback only.</summary>
|
||||
public float MeleeFinisherRangeMult;
|
||||
/// <summary>Attack input-buffer window (ticks before unlock a press is remembered). 0 = off.</summary>
|
||||
public float MeleeBufferTicks;
|
||||
/// <summary>Base contact delay (ticks from swing start to the blade landing; per-step via MeleeTiming). 0 = IMMEDIATE (legacy same-tick cleave).</summary>
|
||||
public float MeleeContactTicks;
|
||||
|
||||
// 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;
|
||||
@@ -76,15 +84,18 @@ namespace ProjectM.Simulation
|
||||
ChargerLungeDurationTicks = 18f,
|
||||
ChargerWhiffStaggerTicks = 36f,
|
||||
GruntWindupTicks = Tuning.AttackWindupTicks, // canonical Grunt-windup source (TelegraphTests couples to it)
|
||||
MeleeDamage = 18f,
|
||||
MeleeRange = 2.6f,
|
||||
MeleeDamage = 30f, // 07-19 heavy-weapon retime: bigger hit, slower cadence (DPS ~held: 18/0.27s -> 30/0.50s)
|
||||
MeleeRange = 2.2f, // 07-20 G2.2 reach honesty: axe tip ~1.8m + a 0.4m player-favoring margin (was 2.6)
|
||||
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)
|
||||
MeleeRecoverTicks = 30f, // heavy-swing lock/recovery (~0.50s; 07-19 weighty-melee retime)
|
||||
MeleeChainGraceTicks = 24f, // window after the lock to chain the next hit (~0.4s)
|
||||
MeleeSwingMoveScale = 0.35f, // movement-commit while swinging (0..1)
|
||||
MeleeKnockbackSpeed = 6f,
|
||||
MeleeFinisherMult = 1.8f, // finisher (last hit) scales dmg/range/recover/knockback
|
||||
MeleeFinisherMult = 1.5f, // finisher (last hit) scales dmg/range/recover/knockback (07-19: 1.8 -> 1.5, recover was ballooning at the heavy cadence)
|
||||
MeleeComboLength = 3f, // light, light, finisher
|
||||
MeleeFinisherRangeMult = 1.25f, // 07-20 G2.2: finisher REACH (damage/recover/knockback keep MeleeFinisherMult)
|
||||
MeleeBufferTicks = 8f, // 07-20 G7: attack press remembered in the last ~0.13s of a lock (0 = off)
|
||||
MeleeContactTicks = 16f, // 07-20 G2.1: blade lands ~0.27s after swing start (per-step via MeleeTiming; 0 = legacy immediate)
|
||||
StructureAggroWeight = 0.7f, // EB-1: <1 prefers structures (fortress aggro); live-tunable
|
||||
StaggerKnockbackSpeed = 7f, // B2 poise: kb.Speed >= this interrupts windups/lunges; below = nudge only
|
||||
SeparationMaxSpeed = 3f, // B1: max separation push (units/s) so soft-collision can't fling
|
||||
@@ -116,6 +127,9 @@ namespace ProjectM.Simulation
|
||||
case TuningKnob.CastTurnRateDeg:
|
||||
case TuningKnob.MoveSharpness:
|
||||
case TuningKnob.SeparationMaxSpeed:
|
||||
case TuningKnob.MeleeFinisherRangeMult:
|
||||
case TuningKnob.MeleeBufferTicks: // 07-20: 0 = buffer OFF sentinel (must survive ClampKnob — review C10)
|
||||
case TuningKnob.MeleeContactTicks: // 07-20: 0 = IMMEDIATE sentinel (legacy same-tick cleave)
|
||||
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.
|
||||
@@ -155,6 +169,9 @@ namespace ProjectM.Simulation
|
||||
case TuningKnob.TurnRateDeg: c.TurnRateDeg = value; break;
|
||||
case TuningKnob.CastTurnRateDeg: c.CastTurnRateDeg = value; break;
|
||||
case TuningKnob.MoveSharpness: c.MoveSharpness = value; break;
|
||||
case TuningKnob.MeleeBufferTicks: c.MeleeBufferTicks = value; break;
|
||||
case TuningKnob.MeleeFinisherRangeMult: c.MeleeFinisherRangeMult = value; break;
|
||||
case TuningKnob.MeleeContactTicks: c.MeleeContactTicks = value; break;
|
||||
|
||||
// unknown index -> no-op (matches the no-default switch convention in DebugCommandReceiveSystem)
|
||||
}
|
||||
@@ -190,6 +207,9 @@ namespace ProjectM.Simulation
|
||||
case TuningKnob.TurnRateDeg: return c.TurnRateDeg;
|
||||
case TuningKnob.CastTurnRateDeg: return c.CastTurnRateDeg;
|
||||
case TuningKnob.MoveSharpness: return c.MoveSharpness;
|
||||
case TuningKnob.MeleeBufferTicks: return c.MeleeBufferTicks;
|
||||
case TuningKnob.MeleeFinisherRangeMult: return c.MeleeFinisherRangeMult;
|
||||
case TuningKnob.MeleeContactTicks: return c.MeleeContactTicks;
|
||||
|
||||
default: return 0f;
|
||||
}
|
||||
@@ -223,6 +243,9 @@ namespace ProjectM.Simulation
|
||||
TurnRateDeg = c.TurnRateDeg,
|
||||
CastTurnRateDeg = c.CastTurnRateDeg,
|
||||
MoveSharpness = c.MoveSharpness,
|
||||
MeleeBufferTicks = c.MeleeBufferTicks,
|
||||
MeleeFinisherRangeMult = c.MeleeFinisherRangeMult,
|
||||
MeleeContactTicks = c.MeleeContactTicks,
|
||||
|
||||
};
|
||||
|
||||
@@ -254,6 +277,9 @@ namespace ProjectM.Simulation
|
||||
TurnRateDeg = r.TurnRateDeg,
|
||||
CastTurnRateDeg = r.CastTurnRateDeg,
|
||||
MoveSharpness = r.MoveSharpness,
|
||||
MeleeBufferTicks = r.MeleeBufferTicks,
|
||||
MeleeFinisherRangeMult = r.MeleeFinisherRangeMult,
|
||||
MeleeContactTicks = r.MeleeContactTicks,
|
||||
|
||||
};
|
||||
}
|
||||
@@ -289,9 +315,12 @@ namespace ProjectM.Simulation
|
||||
public const byte TurnRateDeg = 26;
|
||||
public const byte CastTurnRateDeg = 27;
|
||||
public const byte MoveSharpness = 28;
|
||||
public const byte MeleeBufferTicks = 29; // 07-20 G7 (0 = off sentinel)
|
||||
public const byte MeleeFinisherRangeMult = 30; // 07-20 G2.2
|
||||
public const byte MeleeContactTicks = 31; // 07-20 G2.1 (0 = immediate sentinel)
|
||||
|
||||
/// <summary>Knob count (overlay iteration bound).</summary>
|
||||
public const byte Count = 29;
|
||||
public const byte Count = 32;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -327,6 +356,9 @@ namespace ProjectM.Simulation
|
||||
public float TurnRateDeg;
|
||||
public float CastTurnRateDeg;
|
||||
public float MoveSharpness;
|
||||
public float MeleeBufferTicks; // 07-20 melee feel forks (dev-protocol bump: rebuild both peers)
|
||||
public float MeleeFinisherRangeMult;
|
||||
public float MeleeContactTicks;
|
||||
}
|
||||
|
||||
// NOTE: appending fields = a DEV-PROTOCOL BUMP (RpcCollection hash) — rebuild both peers together.
|
||||
|
||||
@@ -35,6 +35,15 @@ namespace ProjectM.Simulation
|
||||
const float DefaultSharpness = CharacterComponent.DefaultGroundedSharpness; // CharacterComponent.GetDefault() base
|
||||
const float SimTickRate = 60f;
|
||||
|
||||
// 07-20 G7 dash law (design review wf_000bc247 C4): a LOOKUP, not a query add -- melee-less entities
|
||||
// (dash test harnesses) must keep dashing; only entities that carry MeleeCombo get the commitment rule.
|
||||
ComponentLookup<MeleeCombo> m_MeleeLookup;
|
||||
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
m_MeleeLookup = state.GetComponentLookup<MeleeCombo>(isReadOnly: true);
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
@@ -50,19 +59,31 @@ namespace ProjectM.Simulation
|
||||
float dashSharpness = t.DashSharpness;
|
||||
// 07-15 underwater feel: the restore target honors the MoveSharpness dev-override (0 = authored const).
|
||||
float baseSharpness = t.MoveSharpness > 0f ? t.MoveSharpness : DefaultSharpness;
|
||||
float contactKnob = math.max(0f, t.MeleeContactTicks); // 07-20 G7: pre-contact swing = dash refused
|
||||
m_MeleeLookup.Update(ref state);
|
||||
|
||||
|
||||
foreach (var (ds, cd, control, character, input, facing) in
|
||||
foreach (var (ds, cd, control, character, input, facing, entity) in
|
||||
SystemAPI.Query<RefRW<DashState>, RefRW<DashCooldown>, RefRW<CharacterControl>,
|
||||
RefRW<CharacterComponent>, RefRO<PlayerInput>, RefRO<PlayerFacing>>()
|
||||
.WithAll<Simulate>().WithDisabled<Dead>())
|
||||
.WithAll<Simulate>().WithDisabled<Dead>().WithEntityAccess())
|
||||
{
|
||||
// --- START (idempotent: fresh press + cooldown ready + not already mid-dash) ---
|
||||
bool ready = cd.ValueRO.NextTick == 0u
|
||||
|| !new NetworkTick(cd.ValueRO.NextTick).IsNewerThan(serverTick);
|
||||
bool inWindow = ds.ValueRO.RecoverUntilTick != 0u
|
||||
&& new NetworkTick(ds.ValueRO.RecoverUntilTick).IsNewerThan(serverTick);
|
||||
if (input.ValueRO.Dash.IsSet && ready && !inWindow)
|
||||
// 07-20 G7 (the Bathynaut COMMITS): a dash start is refused while a swing is PRE-CONTACT -- the
|
||||
// blade hasn't landed; post-contact recovery dash-cancel stays allowed. Same wrap-safe window
|
||||
// family as the anim pulse (TickWindowMath.SwingActive over the per-step contact ticks);
|
||||
// contact knob 0 -> zero-length window -> no refusal (legacy). Deterministic both worlds.
|
||||
bool preContactCommit = false;
|
||||
if (m_MeleeLookup.HasComponent(entity))
|
||||
{
|
||||
var mcRO = m_MeleeLookup[entity];
|
||||
preContactCommit = TickWindowMath.SwingActive(mcRO, serverTick, MeleeTiming.ContactTicks(mcRO.Step, contactKnob));
|
||||
}
|
||||
if (input.ValueRO.Dash.IsSet && ready && !inWindow && !preContactCommit)
|
||||
{
|
||||
// C1 + 07-15 fork: dash toward MOVEMENT input when moving (a panic 'dash away' still works); else
|
||||
// toward the CURSOR aim (stationary aim-and-dash keeps today's KBM feel under move-facing —
|
||||
|
||||
@@ -28,5 +28,25 @@ namespace ProjectM.Simulation
|
||||
/// <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;
|
||||
|
||||
/// <summary>07-20 (guidelines G7 input buffer): raw ServerTick of an Attack press that landed during the
|
||||
/// swing lock's LAST MeleeBufferTicks (NonZero; 0 = empty). Consumed (zeroed) when it starts the chained
|
||||
/// swing; validity is anchored to the UNLOCK edge, not the press tick, so server tick-batching cannot
|
||||
/// drop an edge press (design review wf_000bc247 C1). Predicted + rollback-restored like its siblings.</summary>
|
||||
[GhostField] public uint BufferedAttackTick;
|
||||
}
|
||||
|
||||
/// <summary>07-20 (guidelines G2.1 damage-at-contact): SERVER-ONLY scheduled cleave — the AttackWindup
|
||||
/// schedule-and-consume idiom, entity-local (design review wf_000bc247 C11: no container, no disposal, no
|
||||
/// staleness window). Stamped when a swing starts (ResolveTick = start + MeleeTiming.ContactTicks), fired on
|
||||
/// a wrap-safe elapsed compare (tick-batch-proof), CONSUMED by zeroing. A still-armed pending is FLUSHED
|
||||
/// (fired early) before a chained swing overwrites it, so no knob combination can lose a cleave (C0/C12).
|
||||
/// Not replicated; the client's contact cues are presentation-side (CombatFeedbackSystem). Baked zeroed.</summary>
|
||||
public struct MeleeCleavePending : IComponentData
|
||||
{
|
||||
/// <summary>NonZero tick the armed swing's blade lands; 0 = nothing pending.</summary>
|
||||
public uint ResolveTick;
|
||||
/// <summary>Combo step of the ARMED swing (finisher scaling resolved against the live combo length).</summary>
|
||||
public byte Step;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,10 +58,38 @@ namespace ProjectM.Simulation
|
||||
BufferLookup<InventorySlot> m_InvLookup;
|
||||
BufferLookup<StatModifier> m_StatModLookup;
|
||||
ComponentLookup<BoonEffects> m_BoonEffectsLookup; // Phase 1.7 (player query is at the 7-type cap -> lookup)
|
||||
ComponentLookup<MeleeCleavePending> m_PendingLookup; // 07-20 G2.1 scheduled cleave (query at cap -> lookup)
|
||||
|
||||
/// <summary>Phase 1.7 Detonating Finisher blast radius (planar, tunable).</summary>
|
||||
const float k_DetonateRadius = 3.5f;
|
||||
|
||||
/// <summary>07-20 G2.1: build a cleave payload from LIVE player state at the moment it FIRES (contact tick,
|
||||
/// flush, or legacy same-tick) -- the cast-turn keeps steering the cone until the blade lands. Static with
|
||||
/// explicit params (no captures) so Burst lowering stays trivial. Reach uses the RANGE-only finisher mult
|
||||
/// (G2.2); damage/knockback keep the classic finisher mult.</summary>
|
||||
static PendingCleave BuildCleave(byte step, byte comboLen, float baseDamage, float baseRange, float knockSpeed,
|
||||
float finisherMult, float finisherRangeMult, uint stamp, uint knockUntil, float3 from, float2 aim,
|
||||
float2 facingDir, int ownerId, byte bflags, bool hasMods, DynamicBuffer<StatModifier> mods)
|
||||
{
|
||||
bool fin = step >= comboLen;
|
||||
float d = math.max(0f, hasMods ? StatMath.Apply(baseDamage, StatTarget.MeleeDamage, mods) : baseDamage);
|
||||
float r = math.max(0f, hasMods ? StatMath.Apply(baseRange, StatTarget.MeleeRange, mods) : baseRange);
|
||||
return new PendingCleave
|
||||
{
|
||||
From = from,
|
||||
Face = FacingMath.ResolveAim(aim, facingDir),
|
||||
Damage = fin ? d * finisherMult : d,
|
||||
Range = fin ? r * finisherRangeMult : r,
|
||||
KnockSpeed = fin ? knockSpeed * finisherMult : knockSpeed,
|
||||
OwnerId = ownerId,
|
||||
Stamp = stamp,
|
||||
KnockUntil = knockUntil,
|
||||
IsFinisher = fin,
|
||||
Detonate = (bflags & BoonFlag.FinisherDetonate) != 0,
|
||||
Pull = (bflags & BoonFlag.KnockToPull) != 0,
|
||||
};
|
||||
}
|
||||
|
||||
[BurstCompile]
|
||||
public void OnCreate(ref SystemState state)
|
||||
{
|
||||
@@ -72,6 +100,7 @@ namespace ProjectM.Simulation
|
||||
m_InvLookup = state.GetBufferLookup<InventorySlot>(isReadOnly: false);
|
||||
m_StatModLookup = state.GetBufferLookup<StatModifier>(isReadOnly: true);
|
||||
m_BoonEffectsLookup = state.GetComponentLookup<BoonEffects>(isReadOnly: true);
|
||||
m_PendingLookup = state.GetComponentLookup<MeleeCleavePending>(isReadOnly: false);
|
||||
state.RequireForUpdate<NetworkTime>();
|
||||
}
|
||||
|
||||
@@ -92,6 +121,9 @@ namespace ProjectM.Simulation
|
||||
float moveScale = math.max(0f, t.MeleeSwingMoveScale);
|
||||
float knockSpeed = math.max(0f, t.MeleeKnockbackSpeed);
|
||||
float finisherMult = math.max(1f, t.MeleeFinisherMult);
|
||||
float finisherRangeMult = math.max(0f, t.MeleeFinisherRangeMult); // 07-20 G2.2: REACH-only finisher scaling
|
||||
float bufferKnob = math.max(0f, t.MeleeBufferTicks); // 07-20 G7: 0 = buffer off
|
||||
float contactKnob = math.max(0f, t.MeleeContactTicks); // 07-20 G2.1: 0 = immediate (legacy)
|
||||
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));
|
||||
@@ -103,6 +135,7 @@ namespace ProjectM.Simulation
|
||||
var cleaves = isServer ? new NativeList<PendingCleave>(Allocator.Temp) : default;
|
||||
m_StatModLookup.Update(ref state);
|
||||
m_BoonEffectsLookup.Update(ref state); // Phase 1.7: per-player boon flags (read inside the player loop)
|
||||
m_PendingLookup.Update(ref state); // 07-20 G2.1: scheduled cleave slots (server-only writes)
|
||||
|
||||
foreach (var (mc, control, input, facing, xform, owner, ds, entity) in
|
||||
SystemAPI.Query<RefRW<MeleeCombo>, RefRW<CharacterControl>, RefRO<PlayerInput>,
|
||||
@@ -119,10 +152,48 @@ namespace ProjectM.Simulation
|
||||
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) ---
|
||||
// --- 07-20 G2.1: fire a DUE scheduled cleave BEFORE anything can overwrite the swing this tick
|
||||
// (wrap-safe elapsed compare = tick-batch-proof; consumed by zeroing -- the AttackWindup idiom,
|
||||
// design review wf_000bc247 C11). Server-only; the pending slot never replicates.
|
||||
bool hasPending = m_PendingLookup.HasComponent(entity);
|
||||
if (isServer && hasPending)
|
||||
{
|
||||
var pend = m_PendingLookup[entity];
|
||||
if (pend.ResolveTick != 0u && !new NetworkTick(pend.ResolveTick).IsNewerThan(serverTick))
|
||||
{
|
||||
cleaves.Add(BuildCleave(pend.Step, comboLen, baseDamage, baseRange, knockSpeed,
|
||||
finisherMult, finisherRangeMult, stamp, knockUntil, xform.ValueRO.Position,
|
||||
input.ValueRO.Aim, facing.ValueRO.Direction, owner.ValueRO.NetworkId,
|
||||
m_BoonEffectsLookup.HasComponent(entity) ? m_BoonEffectsLookup[entity].Flags : (byte)0,
|
||||
m_StatModLookup.HasBuffer(entity), m_StatModLookup.HasBuffer(entity) ? m_StatModLookup[entity] : default));
|
||||
m_PendingLookup[entity] = default;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 07-20 G7 buffer: a press in the lock's LAST MeleeBufferTicks is remembered; validity anchors
|
||||
// to the UNLOCK edge so server tick-batching can't drop an edge press (review wf_000bc247 C1). ---
|
||||
uint bufTicks = (uint)bufferKnob;
|
||||
if (bufTicks > 0u && input.ValueRO.Attack.IsSet && locked)
|
||||
{
|
||||
var lockEnd = new NetworkTick(mc.ValueRO.LockUntilTick);
|
||||
if (lockEnd.IsValid && lockEnd.TicksSince(serverTick) <= (int)bufTicks)
|
||||
mc.ValueRW.BufferedAttackTick = TickUtil.NonZero(now);
|
||||
}
|
||||
bool buffered = false;
|
||||
if (!locked && mc.ValueRO.BufferedAttackTick != 0u)
|
||||
{
|
||||
var lockEnd = new NetworkTick(mc.ValueRO.LockUntilTick);
|
||||
if (bufTicks > 0u && lockEnd.IsValid && serverTick.TicksSince(lockEnd) <= (int)MeleeTiming.BatchSlackTicks)
|
||||
buffered = true;
|
||||
else
|
||||
mc.ValueRW.BufferedAttackTick = 0u; // stale (missed the slack / buffer tuned off) -- drop
|
||||
}
|
||||
|
||||
// --- ADVANCE (idempotent ABSOLUTE writes; dash wins same-tick ties via !Dash.IsSet; a buffered
|
||||
// press counts as a press -- G7) ---
|
||||
bool swingStarted = false;
|
||||
byte swingStep = 0;
|
||||
if (input.ValueRO.Attack.IsSet && !locked && !dashActive && !input.ValueRO.Dash.IsSet)
|
||||
if ((input.ValueRO.Attack.IsSet || buffered) && !locked && !dashActive && !input.ValueRO.Dash.IsSet)
|
||||
{
|
||||
bool inChainWindow = false;
|
||||
if (mc.ValueRO.LockUntilTick != 0u)
|
||||
@@ -139,6 +210,7 @@ namespace ProjectM.Simulation
|
||||
mc.ValueRW.Step = swingStep;
|
||||
mc.ValueRW.SwingStartTick = TickUtil.NonZero(now);
|
||||
mc.ValueRW.LockUntilTick = TickUtil.NonZero(now + stepRecover);
|
||||
mc.ValueRW.BufferedAttackTick = 0u; // consumed (or superseded by a fresh press)
|
||||
swingStarted = true;
|
||||
}
|
||||
|
||||
@@ -150,33 +222,33 @@ namespace ProjectM.Simulation
|
||||
if (inSwingWindow && !dashActive)
|
||||
control.ValueRW.MoveVelocity *= moveScale;
|
||||
|
||||
// --- SERVER: queue the cleave (resolved after the loop). Damage/cone are SERVER-authoritative. ---
|
||||
// --- SERVER: schedule the cleave at the CONTACT tick (07-20 G2.1) -- or fire same-tick when the
|
||||
// knob is 0 (legacy) / the entity has no pending slot (plain test worlds). Damage stays SERVER-only;
|
||||
// payload is built from LIVE state at the moment it fires (the cast-turn steers until the blade lands).
|
||||
if (swingStarted && isServer)
|
||||
{
|
||||
bool isFin = swingStep >= comboLen;
|
||||
// Slice 2: fold the player's class/run StatModifiers onto the live-tunable melee base so the
|
||||
// PRIMARY verb scales with class identity (Warrior +MeleeDamage/+reach) + run augments.
|
||||
byte bflags = m_BoonEffectsLookup.HasComponent(entity) ? m_BoonEffectsLookup[entity].Flags : (byte)0;
|
||||
bool hasMods = m_StatModLookup.HasBuffer(entity);
|
||||
float pDamage = math.max(0f, hasMods ? StatMath.Apply(baseDamage, StatTarget.MeleeDamage, m_StatModLookup[entity]) : baseDamage);
|
||||
float pRange = math.max(0f, hasMods ? StatMath.Apply(baseRange, StatTarget.MeleeRange, m_StatModLookup[entity]) : baseRange);
|
||||
// Manual-aim cleave (07-15): the cone follows the CURRENT tick's replicated Aim (SoD skillshot
|
||||
// grammar) — PlayerFacing is body-yaw only; the facing fallback covers a resting gamepad stick.
|
||||
float2 face = FacingMath.ResolveAim(input.ValueRO.Aim, facing.ValueRO.Direction);
|
||||
cleaves.Add(new PendingCleave
|
||||
uint ct = MeleeTiming.ContactTicks(swingStep, contactKnob);
|
||||
if (ct > 0u && hasPending)
|
||||
{
|
||||
From = xform.ValueRO.Position,
|
||||
Face = face,
|
||||
Damage = isFin ? pDamage * finisherMult : pDamage,
|
||||
Range = isFin ? pRange * finisherMult : pRange,
|
||||
KnockSpeed = isFin ? knockSpeed * finisherMult : knockSpeed,
|
||||
OwnerId = owner.ValueRO.NetworkId,
|
||||
Stamp = stamp,
|
||||
KnockUntil = knockUntil,
|
||||
IsFinisher = isFin,
|
||||
Detonate = (bflags & BoonFlag.FinisherDetonate) != 0,
|
||||
Pull = (bflags & BoonFlag.KnockToPull) != 0,
|
||||
});
|
||||
// FLUSH a still-armed older cleave first (pathological knob combos can push contact past the
|
||||
// recover lock -- fire it early rather than lose it; review wf_000bc247 C0/C12).
|
||||
var pend = m_PendingLookup[entity];
|
||||
if (pend.ResolveTick != 0u)
|
||||
cleaves.Add(BuildCleave(pend.Step, comboLen, baseDamage, baseRange, knockSpeed,
|
||||
finisherMult, finisherRangeMult, stamp, knockUntil, xform.ValueRO.Position,
|
||||
input.ValueRO.Aim, facing.ValueRO.Direction, owner.ValueRO.NetworkId,
|
||||
m_BoonEffectsLookup.HasComponent(entity) ? m_BoonEffectsLookup[entity].Flags : (byte)0,
|
||||
m_StatModLookup.HasBuffer(entity), m_StatModLookup.HasBuffer(entity) ? m_StatModLookup[entity] : default));
|
||||
m_PendingLookup[entity] = new MeleeCleavePending { ResolveTick = TickUtil.NonZero(now + ct), Step = swingStep };
|
||||
}
|
||||
else
|
||||
{
|
||||
cleaves.Add(BuildCleave(swingStep, comboLen, baseDamage, baseRange, knockSpeed,
|
||||
finisherMult, finisherRangeMult, stamp, knockUntil, xform.ValueRO.Position,
|
||||
input.ValueRO.Aim, facing.ValueRO.Direction, owner.ValueRO.NetworkId,
|
||||
m_BoonEffectsLookup.HasComponent(entity) ? m_BoonEffectsLookup[entity].Flags : (byte)0,
|
||||
m_StatModLookup.HasBuffer(entity), m_StatModLookup.HasBuffer(entity) ? m_StatModLookup[entity] : default));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ namespace ProjectM.Simulation
|
||||
|
||||
/// <summary>Ticks facing stays aimed after a swing/fire starts (~0.22s @60Hz — matches the anim pulse
|
||||
/// k_AttackAnimTicks so body yaw and the swing animation agree).</summary>
|
||||
public const uint CastFacingTicks = 13;
|
||||
public const uint CastFacingTicks = 26; // 07-19 heavy-weapon retime: cast-turn + swing-anim window tracks the full ~0.43s swing (was 13; MUST stay < MeleeRecoverTicks so the anim pulse re-triggers between chained swings)
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
|
||||
Reference in New Issue
Block a user