LANTERN P1 Group B: Blink Spark (Movement archetype) + BlinkSystem
Per Phase1_Combat_Gym_Build_Spec §6 (the Blink Spark = a second, socketed dash): - AbilityArchetype += Movement (falls through AbilityFireSystem; a sibling system drives it). - AbilityId += the 5 Phase-1 Sparks (DecoyWisp/HookPull/Vortex/Blink/LightZone). - BlinkState: predicted, non-replicated blink window (DashState sibling). - BlinkSystem: velocity blink (never a LocalTransform write), triggered by the Movement- archetype socket, dash-wins precedence ([UpdateAfter(DashSystem)]), sole writer of its socket's SocketCooldown row; reads socket loadout/cooldown by entity via lookups (query at 6 args, under the 7-cap). Baked BlinkState on the player. L1 clean; L2 494/494. Dead until a Blink Spark def is socketed (ability-database SO = next). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -87,6 +87,7 @@ 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 });
|
||||||
|
AddComponent<BlinkState>(entity); // LANTERN Blink: predicted blink window (Movement-archetype Spark), baked idle
|
||||||
// MC-4 melee combo: predicted, owner-replicated combo anchor (Step/SwingStartTick/LockUntilTick), baked idle/zero.
|
// MC-4 melee combo: predicted, owner-replicated combo anchor (Step/SwingStartTick/LockUntilTick), baked idle/zero.
|
||||||
AddComponent<MeleeCombo>(entity);
|
AddComponent<MeleeCombo>(entity);
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ namespace ProjectM.Simulation
|
|||||||
Hitscan = 1, // MC-6
|
Hitscan = 1, // MC-6
|
||||||
Cone = 2, // MC-6 (melee-style cone via the ability system)
|
Cone = 2, // MC-6 (melee-style cone via the ability system)
|
||||||
Aoe = 3, // MC-6
|
Aoe = 3, // MC-6
|
||||||
|
Movement = 4, // LANTERN: NOT dispatched by AbilityFireSystem (falls through); a sibling predicted system (BlinkSystem) drives it
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>One authored ability definition, baked immutable into the AbilityDatabase blob.</summary>
|
/// <summary>One authored ability definition, baked immutable into the AbilityDatabase blob.</summary>
|
||||||
|
|||||||
@@ -8,6 +8,12 @@ namespace ProjectM.Simulation
|
|||||||
FastLight = 2,
|
FastLight = 2,
|
||||||
SlowHeavy = 3,
|
SlowHeavy = 3,
|
||||||
WarriorCone = 4, // Slice 2: the Warrior's aim-directed short-range cone secondary (Fire slot)
|
WarriorCone = 4, // Slice 2: the Warrior's aim-directed short-range cone secondary (Fire slot)
|
||||||
|
// LANTERN Phase 1 Sparks (the gym's first socketables):
|
||||||
|
DecoyWisp = 5, // Aoe/spawn: a decoy ghost that draws aggro
|
||||||
|
HookPull = 6, // Projectile + pull: the Harpooner reel (yanks the hit target in)
|
||||||
|
Vortex = 7, // Aoe/zone: a timed pulling/slowing area
|
||||||
|
Blink = 8, // Movement: a second dash (BlinkSystem)
|
||||||
|
LightZone = 9, // Aoe/zone: a persistent light/damage zone
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Stable key for an authored character-stats definition in the AbilityDatabase blob.</summary>
|
/// <summary>Stable key for an authored character-stats definition in the AbilityDatabase blob.</summary>
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
using Unity.Entities;
|
||||||
|
using Unity.Mathematics;
|
||||||
|
|
||||||
|
namespace ProjectM.Simulation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Predicted, NON-replicated blink window on the owner-predicted player — the Movement-archetype Spark (a
|
||||||
|
/// second dash, socketed). A shape-sibling of <see cref="DashState"/>: re-simulated from the replicated
|
||||||
|
/// blink-socket InputEvent every predicted tick, so its own window (never <see cref="DashState"/>, which a
|
||||||
|
/// same-tick dash+blink would clobber). BlinkSystem drives a VELOCITY blink over the half-open
|
||||||
|
/// [StartTick, UntilTick) window and NEVER writes <c>LocalTransform</c> — the CC collide-and-slide processor
|
||||||
|
/// owns transform integration; a raw write tunnels the subscene-only Environment colliders and resets Scale.
|
||||||
|
/// Ticks routed through <c>TickUtil.NonZero</c>; compared via <see cref="Unity.NetCode.NetworkTick"/> only.
|
||||||
|
/// Baked all-zero (idle). NOT a <c>[GhostField]</c> (no player-ghost re-bake).
|
||||||
|
/// </summary>
|
||||||
|
public struct BlinkState : IComponentData
|
||||||
|
{
|
||||||
|
/// <summary>Planar XZ blink heading, captured at blink-start.</summary>
|
||||||
|
public float2 Dir;
|
||||||
|
|
||||||
|
/// <summary>Raw ServerTick at blink-start (NonZero). Lower (inclusive) bound of the blink window.</summary>
|
||||||
|
public uint StartTick;
|
||||||
|
|
||||||
|
/// <summary>StartTick + blink window (NonZero). Velocity override covers the HALF-OPEN [StartTick, UntilTick).</summary>
|
||||||
|
public uint UntilTick;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 8fe3e2f11409580468040c0d75090a05
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
using Unity.Burst;
|
||||||
|
using Unity.Entities;
|
||||||
|
using Unity.Mathematics;
|
||||||
|
using Unity.NetCode;
|
||||||
|
|
||||||
|
namespace ProjectM.Simulation
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// LANTERN Blink — the Movement-archetype Spark: a second, socketed dash. A behaviour-clone of
|
||||||
|
/// <see cref="DashSystem"/> (velocity blink, NOT a LocalTransform teleport — the CC processor lerps
|
||||||
|
/// RelativeVelocity toward MoveVelocity at a high GroundedMovementSharpness so it reads as a blink; a raw
|
||||||
|
/// transform write would tunnel Environment colliders and reset Scale). Triggered by the ability socket
|
||||||
|
/// whose Spark has <see cref="AbilityArchetype.Movement"/>: AbilityFireSystem skips movement sockets
|
||||||
|
/// (falls through), and BlinkSystem is the SOLE writer of that socket's <see cref="SocketCooldown"/> row
|
||||||
|
/// (so the HUD's per-socket bar stays uniform). Reads the socket loadout / cooldown by entity via lookups
|
||||||
|
/// (keeps the query under the 7-type cap).
|
||||||
|
/// <para>
|
||||||
|
/// Runs in <see cref="PredictedSimulationSystemGroup"/> AFTER <see cref="PlayerControlSystem"/> (overrides
|
||||||
|
/// the input-derived MoveVelocity) and AFTER <see cref="DashSystem"/> — dash wins precedence: while a dash
|
||||||
|
/// window is active this tick, blink neither starts nor overrides. START is an idempotent pure function of
|
||||||
|
/// the replicated socket input + tick (no IsFirstTimeFullyPredictingTick guard); the OVERRIDE re-applies on
|
||||||
|
/// EVERY predicted pass, lower-bounded on the half-open window, so rollback re-simulation converges. All
|
||||||
|
/// ticks routed through <c>TickUtil.NonZero</c>; compared via <see cref="NetworkTick"/> only.
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
|
||||||
|
[UpdateAfter(typeof(PlayerControlSystem))]
|
||||||
|
[UpdateAfter(typeof(DashSystem))]
|
||||||
|
[BurstCompile]
|
||||||
|
public partial struct BlinkSystem : ISystem
|
||||||
|
{
|
||||||
|
const float DefaultSharpness = CharacterComponent.DefaultGroundedSharpness;
|
||||||
|
const float SimTickRate = 60f;
|
||||||
|
// Blink feel (consts for v1; promote to TuningConfig with the rest of the gym knobs later).
|
||||||
|
const float k_BlinkDistance = 6.5f;
|
||||||
|
const uint k_BlinkWindowTicks = 5;
|
||||||
|
const uint k_BlinkCooldownTicks = 150;
|
||||||
|
const float k_BlinkSharpness = 200f;
|
||||||
|
|
||||||
|
BufferLookup<AbilitySocket> m_SocketLookup;
|
||||||
|
ComponentLookup<SocketCooldown> m_SocketCdLookup;
|
||||||
|
|
||||||
|
[BurstCompile]
|
||||||
|
public void OnCreate(ref SystemState state)
|
||||||
|
{
|
||||||
|
state.RequireForUpdate<AbilityDatabase>();
|
||||||
|
state.RequireForUpdate<NetworkTime>();
|
||||||
|
m_SocketLookup = state.GetBufferLookup<AbilitySocket>(isReadOnly: true);
|
||||||
|
m_SocketCdLookup = state.GetComponentLookup<SocketCooldown>(isReadOnly: false);
|
||||||
|
}
|
||||||
|
|
||||||
|
[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 abilityDb = SystemAPI.GetSingleton<AbilityDatabase>();
|
||||||
|
ref var adb = ref abilityDb.Value.Value;
|
||||||
|
m_SocketLookup.Update(ref state);
|
||||||
|
m_SocketCdLookup.Update(ref state);
|
||||||
|
float blinkSpeed = k_BlinkDistance / (k_BlinkWindowTicks / SimTickRate); // window>=1 -> no div-by-0
|
||||||
|
|
||||||
|
foreach (var (blink, control, character, input, facing, dash, entity) in
|
||||||
|
SystemAPI.Query<RefRW<BlinkState>, RefRW<CharacterControl>, RefRW<CharacterComponent>,
|
||||||
|
RefRO<PlayerInput>, RefRO<PlayerFacing>, RefRO<DashState>>()
|
||||||
|
.WithAll<Simulate>().WithDisabled<Dead>().WithEntityAccess())
|
||||||
|
{
|
||||||
|
if (!m_SocketLookup.HasBuffer(entity) || !m_SocketCdLookup.HasComponent(entity))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Which socket holds the Movement (Blink) Spark?
|
||||||
|
var sockets = m_SocketLookup[entity];
|
||||||
|
int blinkSk = -1;
|
||||||
|
int n = math.min(SocketId.Count, sockets.Length);
|
||||||
|
for (int sk = 0; sk < n; sk++)
|
||||||
|
{
|
||||||
|
byte sid = sockets[sk].SparkId;
|
||||||
|
if (sid == 0) continue;
|
||||||
|
if (adb.TryGetAbility(sid, out var d) && d.Archetype == (byte)AbilityArchetype.Movement) { blinkSk = sk; break; }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dash wins: while a dash window is active this tick, blink neither starts nor overrides.
|
||||||
|
bool dashActive = dash.ValueRO.StartTick != 0u
|
||||||
|
&& !new NetworkTick(dash.ValueRO.StartTick).IsNewerThan(serverTick)
|
||||||
|
&& dash.ValueRO.RecoverUntilTick != 0u
|
||||||
|
&& new NetworkTick(dash.ValueRO.RecoverUntilTick).IsNewerThan(serverTick);
|
||||||
|
|
||||||
|
var cd = m_SocketCdLookup[entity];
|
||||||
|
|
||||||
|
// START (idempotent): blink socket fired, cooldown ready, not mid-blink, no dash active.
|
||||||
|
if (blinkSk >= 0 && !dashActive && input.ValueRO.GetSocket(blinkSk).IsSet)
|
||||||
|
{
|
||||||
|
uint nextRaw = cd.Get(blinkSk);
|
||||||
|
bool ready = nextRaw == 0u || !new NetworkTick(nextRaw).IsNewerThan(serverTick);
|
||||||
|
bool inWindow = blink.ValueRO.UntilTick != 0u && new NetworkTick(blink.ValueRO.UntilTick).IsNewerThan(serverTick);
|
||||||
|
if (ready && !inWindow)
|
||||||
|
{
|
||||||
|
float2 mv = input.ValueRO.Move;
|
||||||
|
float2 dir = math.lengthsq(mv) > 1e-4f ? mv : facing.ValueRO.Direction;
|
||||||
|
if (math.lengthsq(dir) < 1e-6f) dir = new float2(0f, 1f);
|
||||||
|
dir = math.normalize(dir);
|
||||||
|
blink.ValueRW.Dir = dir;
|
||||||
|
blink.ValueRW.StartTick = TickUtil.NonZero(now);
|
||||||
|
blink.ValueRW.UntilTick = TickUtil.NonZero(now + k_BlinkWindowTicks);
|
||||||
|
cd.Set(blinkSk, TickUtil.NonZero(now + k_BlinkCooldownTicks));
|
||||||
|
m_SocketCdLookup[entity] = cd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OVERRIDE (every predicted pass; lower-bounded on the half-open window). Dash wins.
|
||||||
|
bool inBlink = !dashActive
|
||||||
|
&& blink.ValueRO.StartTick != 0u
|
||||||
|
&& !new NetworkTick(blink.ValueRO.StartTick).IsNewerThan(serverTick)
|
||||||
|
&& blink.ValueRO.UntilTick != 0u
|
||||||
|
&& new NetworkTick(blink.ValueRO.UntilTick).IsNewerThan(serverTick);
|
||||||
|
if (inBlink)
|
||||||
|
{
|
||||||
|
float2 d = blink.ValueRO.Dir;
|
||||||
|
control.ValueRW.MoveVelocity = new float3(d.x, 0f, d.y) * blinkSpeed;
|
||||||
|
character.ValueRW.GroundedMovementSharpness = k_BlinkSharpness;
|
||||||
|
}
|
||||||
|
else if (!dashActive && character.ValueRO.GroundedMovementSharpness == k_BlinkSharpness)
|
||||||
|
{
|
||||||
|
// restore only what WE raised (dash owns its own restore); never stomp an active dash.
|
||||||
|
character.ValueRW.GroundedMovementSharpness = DefaultSharpness;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
fileFormatVersion: 2
|
||||||
|
guid: 862bf6b2abc84c045a845de0faf103b1
|
||||||
Reference in New Issue
Block a user