Vault Re-Alignment
This commit is contained in:
@@ -16,5 +16,11 @@ namespace ProjectM.Simulation
|
||||
|
||||
/// <summary>NetworkId of the firing player that caused this hit (attribution / self-hit filtering upstream).</summary>
|
||||
public int SourceNetworkId;
|
||||
|
||||
/// <summary>Raw ServerTick at which this hit logically LANDS (the appending tick), stamped via
|
||||
/// <c>TickUtil.NonZero</c> at every append site (0 = unstamped). The dash i-frame negation compares it
|
||||
/// against the dashing player's <c>DashState</c> window, so a strike appended a tick before it is
|
||||
/// drained is judged against the tick it was AUTHORED, not the tick it was applied.</summary>
|
||||
public uint SourceTick;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// MC-1 — server-only Charger lunge state (a KnockbackState SHAPE-twin). Component PRESENCE is the Charger
|
||||
/// discriminator (no enum / brain byte — honours the Burst cross-assembly-enum rule; EnemyAISystem is Bursted):
|
||||
/// a Husk variant baked with LungeState is driven by the Charger branch, every other Husk by the Grunt branch
|
||||
/// (which excludes these via <c>.WithNone<LungeState>()</c>). On a wind-up commit the Charger LOCKS
|
||||
/// <see cref="Dir"/> toward the target and travels at <see cref="Speed"/> until <see cref="UntilTick"/> — dealing
|
||||
/// contact damage if it connects, or staggering into a punish window if it whiffs (wall-stop or overshoot).
|
||||
/// NOT a <c>[GhostField]</c> (the lunged position replicates via the stock LocalTransform variant, like
|
||||
/// KnockbackState). All ticks via <c>TickUtil.NonZero</c>; compared with <see cref="Unity.NetCode.NetworkTick"/> only.
|
||||
/// </summary>
|
||||
public struct LungeState : IComponentData
|
||||
{
|
||||
/// <summary>Fixed planar lunge heading, locked at commit (world XZ -> float2 x,y).</summary>
|
||||
public float2 Dir;
|
||||
|
||||
/// <summary>Lunge speed (world units/s); only meaningful while <see cref="UntilTick"/> is active.</summary>
|
||||
public float Speed;
|
||||
|
||||
/// <summary>Raw tick the lunge ends (NonZero). <c>0</c> = not lunging. Active while .IsNewerThan(serverTick).</summary>
|
||||
public uint UntilTick;
|
||||
|
||||
/// <summary>Raw tick the whiff-stagger punish window ends (NonZero; set at BOTH whiff sites). 0 = not
|
||||
/// staggered — or already punished: HealthApplyDamageSystem zeroes it when the first player-sourced hit
|
||||
/// lands so a window counts ONCE in DevTelemetry.ChargerWhiffPunishesLanded. The attack lockout itself
|
||||
/// rides EnemyAttackCooldown.NextAttackTick; this field only scores the punish.</summary>
|
||||
public uint StaggerUntilTick;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: cc65446b98bef1040bc5b9beaac094ba
|
||||
@@ -0,0 +1,49 @@
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// MC-0 — server-only dev-telemetry accumulator (a singleton). Counters are incremented at the
|
||||
/// combat stamp sites (wired in MC-1+) so the fun-gate is MEASURED, not argued. NOT a
|
||||
/// <c>[GhostField]</c> (no ghost-hash change); shipped to dev clients via a periodic
|
||||
/// <see cref="DebugTelemetryReport"/> RPC. The component type is unconditional (stable across
|
||||
/// release/dev peers); only the dev send/sample/receive SYSTEMS are <c>#if UNITY_EDITOR</c>.
|
||||
/// </summary>
|
||||
public struct DevTelemetry : IComponentData
|
||||
{
|
||||
/// <summary>Hits a dash i-frame window negated (incremented in HealthApplyDamageSystem, MC-1).</summary>
|
||||
public uint DashIFrameNegatedHits;
|
||||
|
||||
/// <summary>Dashes whose i-frame window negated nothing (spam signal, MC-1).</summary>
|
||||
public uint DashesWasted;
|
||||
|
||||
/// <summary>Charger lunges that whiffed and opened a punish window (EnemyAISystem, MC-1).</summary>
|
||||
public uint ChargerWhiffWindowsOpened;
|
||||
|
||||
/// <summary>Of those, the ones the player actually punished (MC-1).</summary>
|
||||
public uint ChargerWhiffPunishesLanded;
|
||||
|
||||
/// <summary>Living Husks, sampled each report — proof-of-life (changes during play, no MC-1 dep).</summary>
|
||||
public uint LiveEnemyCount;
|
||||
|
||||
/// <summary>Server tick at the last sample — proof-of-life that the pipe is live.</summary>
|
||||
public uint LastSampleTick;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// MC-0 — server → dev-client telemetry snapshot (sent periodically by the editor-only sampler).
|
||||
/// <b>Unconditional wire type</b> (like <see cref="DebugCommandRequest"/>) so the reflection-built
|
||||
/// RpcCollection hash matches across release/dev peers; only the send/receive SYSTEMS are
|
||||
/// <c>#if UNITY_EDITOR</c>. The dev overlay reads the latest snapshot to show live fun-gate counters.
|
||||
/// </summary>
|
||||
public struct DebugTelemetryReport : IRpcCommand
|
||||
{
|
||||
public uint DashIFrameNegatedHits;
|
||||
public uint DashesWasted;
|
||||
public uint ChargerWhiffWindowsOpened;
|
||||
public uint ChargerWhiffPunishesLanded;
|
||||
public uint LiveEnemyCount;
|
||||
public uint LastSampleTick;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6c5dfccd35c016940914a8357204f4e8
|
||||
@@ -0,0 +1,19 @@
|
||||
using Unity.Entities;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// MC-1 — predicted per-player dash cooldown gate (an <c>AbilityCooldown</c> twin). <c>[GhostField]</c> so the
|
||||
/// owning client does not mispredict the cooldown across rollback / reconnect: re-predicted ticks see the same
|
||||
/// authoritative gate the server applied and converge without a double-dash. <c>0</c> = ready; set to
|
||||
/// <c>serverTick + dashCooldownTicks</c> via <c>TickUtil.NonZero</c> on dash-start; compare by wrapping into a
|
||||
/// <see cref="NetworkTick"/> and using <see cref="NetworkTick.IsNewerThan"/> (raw uint subtraction is unsafe
|
||||
/// across tick wraparound). Baked <c>{NextTick = 0}</c>.
|
||||
/// </summary>
|
||||
public struct DashCooldown : IComponentData
|
||||
{
|
||||
/// <summary>Raw tick of the earliest tick the player may dash again. <c>0</c> = ready.</summary>
|
||||
[GhostField] public uint NextTick;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f6f32a690284a3a47bf02829323ed8f5
|
||||
@@ -0,0 +1,33 @@
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// MC-1 — predicted, NON-replicated dash window on the owner-predicted player. A SHAPE-clone of
|
||||
/// <c>KnockbackState</c>, but UNLIKE KnockbackState it lives on a PREDICTED player and is re-simulated from
|
||||
/// the replicated <see cref="PlayerInput.Dash"/> InputEvent every predicted tick — so it is authoritative on
|
||||
/// the server at the tick <c>HealthApplyDamageSystem</c> drains damage, even for a melee strike appended a
|
||||
/// tick earlier in the plain group. All ticks routed through <c>TickUtil.NonZero</c>; compared via
|
||||
/// <see cref="Unity.NetCode.NetworkTick"/> only (never raw uint). NOT a <c>[GhostField]</c> (no player-ghost
|
||||
/// re-bake). Baked all-zero (idle).
|
||||
/// </summary>
|
||||
public struct DashState : IComponentData
|
||||
{
|
||||
/// <summary>Planar XZ dash heading, captured at dash-start.</summary>
|
||||
public float2 Dir;
|
||||
|
||||
/// <summary>Raw ServerTick at dash-start (NonZero-coerced). Lower (inclusive) bound of the i-frame window.</summary>
|
||||
public uint StartTick;
|
||||
|
||||
/// <summary>StartTick + i-frame window (NonZero). I-frames cover the HALF-OPEN range [StartTick, IFrameUntilTick).</summary>
|
||||
public uint IFrameUntilTick;
|
||||
|
||||
/// <summary>IFrameUntilTick + recovery tail (NonZero). Movement-lock tail (no i-frames) so a panic-dash is punishable.</summary>
|
||||
public uint RecoverUntilTick;
|
||||
|
||||
/// <summary>Hits negated by THIS dash's i-frame window. SERVER-written (HealthApplyDamageSystem);
|
||||
/// 0 at window-close = a wasted dash (DevTelemetry.DashesWasted spam signal). The client copy stays 0.</summary>
|
||||
public uint NegatedCount;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 28b7317dff9952841a0fb4b66df54f90
|
||||
@@ -0,0 +1,113 @@
|
||||
using Unity.Burst;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// MC-1 — the predicted dodge dash. On a fresh <see cref="PlayerInput.Dash"/> press (cooldown ready and not
|
||||
/// already mid-dash) it captures the dash heading from <see cref="PlayerFacing"/> and opens a HALF-OPEN i-frame
|
||||
/// window [StartTick, IFrameUntilTick) plus a recovery tail. While the i-frame window is active it OVERRIDES
|
||||
/// <see cref="CharacterControl.MoveVelocity"/> with the dash velocity and raises
|
||||
/// <see cref="CharacterComponent.GroundedMovementSharpness"/> to ~200 so the move reads as a BLINK (the CC
|
||||
/// processor lerps RelativeVelocity toward MoveVelocity at that sharpness — no CharacterProcessor edit needed).
|
||||
/// During the recovery tail movement is locked to zero (no i-frames) so a panic-dash is punishable.
|
||||
/// <see cref="ProjectM.Server"/>'s HealthApplyDamageSystem reads the window to negate hits authored inside it.
|
||||
/// <para>
|
||||
/// Runs in <see cref="PredictedSimulationSystemGroup"/> AFTER <see cref="PlayerControlSystem"/> (it overrides
|
||||
/// the input-derived MoveVelocity that system wrote this tick) and is gated
|
||||
/// <c>.WithAll<Simulate>().WithDisabled<Dead>()</c>. The START is an idempotent pure function of
|
||||
/// replicated input + tick (no IsFirstTimeFullyPredictingTick guard); the OVERRIDE re-applies on EVERY predicted
|
||||
/// pass so rollback re-simulation converges. All ticks routed through <c>TickUtil.NonZero</c>; compared via
|
||||
/// <see cref="NetworkTick"/> only. DashSystem owns GroundedMovementSharpness on the player (base = the CC default
|
||||
/// 15); PlayerDeathStateSystem restores it + clears the window on death.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
|
||||
[UpdateAfter(typeof(PlayerControlSystem))]
|
||||
[BurstCompile]
|
||||
public partial struct DashSystem : ISystem
|
||||
{
|
||||
// Baked-first feel knobs (MC-1; promote to a live TuningConfig later). Sim runs at 60 ticks/sec.
|
||||
const float DashDistance = 4.0f; // world units covered during the i-frame window
|
||||
const uint IFrameWindowTicks = 12; // ~0.20 s of i-frames
|
||||
const uint RecoverTailTicks = 9; // ~0.15 s movement-locked tail (punishes spam)
|
||||
const uint DashCooldownTicks = 45; // ~0.75 s
|
||||
const float DashSharpness = 200f; // GroundedMovementSharpness during the dash -> blink
|
||||
const float DefaultSharpness = 15f; // CharacterComponent.GetDefault() base
|
||||
const float SimTickRate = 60f;
|
||||
|
||||
[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;
|
||||
float dashSpeed = DashDistance / (IFrameWindowTicks / SimTickRate);
|
||||
|
||||
foreach (var (ds, cd, control, character, input, facing) in
|
||||
SystemAPI.Query<RefRW<DashState>, RefRW<DashCooldown>, RefRW<CharacterControl>,
|
||||
RefRW<CharacterComponent>, RefRO<PlayerInput>, RefRO<PlayerFacing>>()
|
||||
.WithAll<Simulate>().WithDisabled<Dead>())
|
||||
{
|
||||
// --- 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)
|
||||
{
|
||||
float2 dir = facing.ValueRO.Direction;
|
||||
if (math.lengthsq(dir) < 1e-6f) dir = new float2(0f, 1f);
|
||||
dir = math.normalize(dir);
|
||||
ds.ValueRW.Dir = dir;
|
||||
ds.ValueRW.StartTick = TickUtil.NonZero(now);
|
||||
ds.ValueRW.IFrameUntilTick = TickUtil.NonZero(now + IFrameWindowTicks);
|
||||
ds.ValueRW.RecoverUntilTick = TickUtil.NonZero(now + IFrameWindowTicks + RecoverTailTicks);
|
||||
cd.ValueRW.NextTick = TickUtil.NonZero(now + DashCooldownTicks);
|
||||
}
|
||||
|
||||
// --- OVERRIDE (runs every predicted pass so rollback re-simulation re-applies it) ---
|
||||
// The lower bound matters: DashState is non-replicated, so prediction rollback does NOT restore
|
||||
// it — a re-simulated PRE-dash tick (serverTick < StartTick) still sees the post-press window and,
|
||||
// gated on the upper bound alone, would stomp dash velocity onto ticks that never had it
|
||||
// (dash-start overshoot under real latency). Membership = the half-open [StartTick, …) test.
|
||||
bool inDashWindow = ds.ValueRO.StartTick != 0u
|
||||
&& !new NetworkTick(ds.ValueRO.StartTick).IsNewerThan(serverTick);
|
||||
bool iFrameActive = inDashWindow && ds.ValueRO.IFrameUntilTick != 0u
|
||||
&& new NetworkTick(ds.ValueRO.IFrameUntilTick).IsNewerThan(serverTick);
|
||||
bool recoverActive = inDashWindow && ds.ValueRO.RecoverUntilTick != 0u
|
||||
&& new NetworkTick(ds.ValueRO.RecoverUntilTick).IsNewerThan(serverTick);
|
||||
if (iFrameActive)
|
||||
{
|
||||
float2 d = ds.ValueRO.Dir;
|
||||
control.ValueRW.MoveVelocity = new float3(d.x, 0f, d.y) * dashSpeed;
|
||||
character.ValueRW.GroundedMovementSharpness = DashSharpness;
|
||||
}
|
||||
else if (recoverActive)
|
||||
{
|
||||
control.ValueRW.MoveVelocity = float3.zero; // movement locked during the punishable tail
|
||||
character.ValueRW.GroundedMovementSharpness = DefaultSharpness;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (character.ValueRO.GroundedMovementSharpness != DefaultSharpness)
|
||||
character.ValueRW.GroundedMovementSharpness = DefaultSharpness; // restore after the dash
|
||||
|
||||
// Window-close edge: score a wasted dash (negated nothing) ONCE, then clear the window.
|
||||
// SERVER-only — the DevTelemetry singleton exists only in the (editor) server world; the
|
||||
// client keeps its copy un-zeroed so rollback re-simulation of the tail stays intact. All
|
||||
// in-window strikes drain >= 9 ticks before this edge, so clearing can't eat a negation.
|
||||
if (ds.ValueRO.RecoverUntilTick != 0u && SystemAPI.TryGetSingletonRW<DevTelemetry>(out var telem))
|
||||
{
|
||||
if (ds.ValueRO.NegatedCount == 0u)
|
||||
telem.ValueRW.DashesWasted++;
|
||||
ds.ValueRW = default;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 58536af899e5e9442b81c594c17bc034
|
||||
@@ -27,15 +27,28 @@ namespace ProjectM.Simulation
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
foreach (var (health, control, deadEnabled) in
|
||||
foreach (var (health, control, deadEnabled, entity) in
|
||||
SystemAPI.Query<RefRO<Health>, RefRW<CharacterControl>, EnabledRefRW<Dead>>()
|
||||
.WithAll<PlayerTag, Simulate>()
|
||||
.WithPresent<Dead>())
|
||||
.WithPresent<Dead>()
|
||||
.WithEntityAccess())
|
||||
{
|
||||
bool isDead = health.ValueRO.Current <= 0f;
|
||||
deadEnabled.ValueRW = isDead;
|
||||
if (isDead)
|
||||
{
|
||||
control.ValueRW.MoveVelocity = float3.zero;
|
||||
// MC-1: clear any in-flight dash window + restore base sharpness so a death mid-dash leaves
|
||||
// no stale i-frames / stuck-fast on respawn (DashSystem skips dead players via .WithDisabled<Dead>()).
|
||||
if (SystemAPI.HasComponent<DashState>(entity))
|
||||
SystemAPI.SetComponent(entity, default(DashState));
|
||||
if (SystemAPI.HasComponent<CharacterComponent>(entity))
|
||||
{
|
||||
var cc = SystemAPI.GetComponent<CharacterComponent>(entity);
|
||||
cc.GroundedMovementSharpness = 15f;
|
||||
SystemAPI.SetComponent(entity, cc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ namespace ProjectM.Simulation
|
||||
|
||||
/// <summary>Primary ability fire. InputEvent survives the frame→tick→rollback boundary so a press fires exactly once.</summary>
|
||||
[GhostField] public InputEvent Fire;
|
||||
/// <summary>Dodge dash. InputEvent twin of <see cref="Fire"/>: survives the frame-tick-rollback boundary
|
||||
/// so one press dashes exactly once; read by the predicted DashSystem (MC-1).</summary>
|
||||
[GhostField] public InputEvent Dash;
|
||||
|
||||
/// <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
|
||||
@@ -32,7 +35,7 @@ namespace ProjectM.Simulation
|
||||
var s = new FixedString512Bytes();
|
||||
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(Fire.Count); s.Append(';'); s.Append(Scheme);
|
||||
s.Append(Fire.Count); s.Append(';'); s.Append(Scheme); s.Append(';'); s.Append(Dash.Count);
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user