From 52818d0dd6e865ce4273c2a566ad7300d1ce5c07 Mon Sep 17 00:00:00 2001 From: Luis Gonzalez Date: Tue, 14 Jul 2026 11:45:06 -0700 Subject: [PATCH] LANTERN P1 Group B: Blink Spark (Movement archetype) + BlinkSystem MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../Authoring/Player/PlayerAuthoring.cs | 1 + .../Simulation/Combat/AbilityDatabaseBlob.cs | 1 + .../Scripts/Simulation/Combat/StatIds.cs | 6 + .../Scripts/Simulation/Player/BlinkState.cs | 27 ++++ .../Simulation/Player/BlinkState.cs.meta | 2 + .../Scripts/Simulation/Player/BlinkSystem.cs | 132 ++++++++++++++++++ .../Simulation/Player/BlinkSystem.cs.meta | 2 + 7 files changed, 171 insertions(+) create mode 100644 Assets/_Project/Scripts/Simulation/Player/BlinkState.cs create mode 100644 Assets/_Project/Scripts/Simulation/Player/BlinkState.cs.meta create mode 100644 Assets/_Project/Scripts/Simulation/Player/BlinkSystem.cs create mode 100644 Assets/_Project/Scripts/Simulation/Player/BlinkSystem.cs.meta diff --git a/Assets/_Project/Scripts/Authoring/Player/PlayerAuthoring.cs b/Assets/_Project/Scripts/Authoring/Player/PlayerAuthoring.cs index 4f7f804ee..4c75c3892 100644 --- a/Assets/_Project/Scripts/Authoring/Player/PlayerAuthoring.cs +++ b/Assets/_Project/Scripts/Authoring/Player/PlayerAuthoring.cs @@ -87,6 +87,7 @@ namespace ProjectM.Authoring // MC-1 dash: predicted dash window (derived from PlayerInput.Dash) + cooldown gate, baked idle/ready. AddComponent(entity); AddComponent(entity, new DashCooldown { NextTick = 0 }); + AddComponent(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. AddComponent(entity); diff --git a/Assets/_Project/Scripts/Simulation/Combat/AbilityDatabaseBlob.cs b/Assets/_Project/Scripts/Simulation/Combat/AbilityDatabaseBlob.cs index 64dfec962..0445b13ab 100644 --- a/Assets/_Project/Scripts/Simulation/Combat/AbilityDatabaseBlob.cs +++ b/Assets/_Project/Scripts/Simulation/Combat/AbilityDatabaseBlob.cs @@ -11,6 +11,7 @@ namespace ProjectM.Simulation Hitscan = 1, // MC-6 Cone = 2, // MC-6 (melee-style cone via the ability system) Aoe = 3, // MC-6 + Movement = 4, // LANTERN: NOT dispatched by AbilityFireSystem (falls through); a sibling predicted system (BlinkSystem) drives it } /// One authored ability definition, baked immutable into the AbilityDatabase blob. diff --git a/Assets/_Project/Scripts/Simulation/Combat/StatIds.cs b/Assets/_Project/Scripts/Simulation/Combat/StatIds.cs index 9c798cbf6..42f5bff5b 100644 --- a/Assets/_Project/Scripts/Simulation/Combat/StatIds.cs +++ b/Assets/_Project/Scripts/Simulation/Combat/StatIds.cs @@ -8,6 +8,12 @@ namespace ProjectM.Simulation FastLight = 2, SlowHeavy = 3, 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 } /// Stable key for an authored character-stats definition in the AbilityDatabase blob. diff --git a/Assets/_Project/Scripts/Simulation/Player/BlinkState.cs b/Assets/_Project/Scripts/Simulation/Player/BlinkState.cs new file mode 100644 index 000000000..7673b1cd5 --- /dev/null +++ b/Assets/_Project/Scripts/Simulation/Player/BlinkState.cs @@ -0,0 +1,27 @@ +using Unity.Entities; +using Unity.Mathematics; + +namespace ProjectM.Simulation +{ + /// + /// Predicted, NON-replicated blink window on the owner-predicted player — the Movement-archetype Spark (a + /// second dash, socketed). A shape-sibling of : re-simulated from the replicated + /// blink-socket InputEvent every predicted tick, so its own window (never , which a + /// same-tick dash+blink would clobber). BlinkSystem drives a VELOCITY blink over the half-open + /// [StartTick, UntilTick) window and NEVER writes LocalTransform — the CC collide-and-slide processor + /// owns transform integration; a raw write tunnels the subscene-only Environment colliders and resets Scale. + /// Ticks routed through TickUtil.NonZero; compared via only. + /// Baked all-zero (idle). NOT a [GhostField] (no player-ghost re-bake). + /// + public struct BlinkState : IComponentData + { + /// Planar XZ blink heading, captured at blink-start. + public float2 Dir; + + /// Raw ServerTick at blink-start (NonZero). Lower (inclusive) bound of the blink window. + public uint StartTick; + + /// StartTick + blink window (NonZero). Velocity override covers the HALF-OPEN [StartTick, UntilTick). + public uint UntilTick; + } +} diff --git a/Assets/_Project/Scripts/Simulation/Player/BlinkState.cs.meta b/Assets/_Project/Scripts/Simulation/Player/BlinkState.cs.meta new file mode 100644 index 000000000..14081be2d --- /dev/null +++ b/Assets/_Project/Scripts/Simulation/Player/BlinkState.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 8fe3e2f11409580468040c0d75090a05 \ No newline at end of file diff --git a/Assets/_Project/Scripts/Simulation/Player/BlinkSystem.cs b/Assets/_Project/Scripts/Simulation/Player/BlinkSystem.cs new file mode 100644 index 000000000..b6c232808 --- /dev/null +++ b/Assets/_Project/Scripts/Simulation/Player/BlinkSystem.cs @@ -0,0 +1,132 @@ +using Unity.Burst; +using Unity.Entities; +using Unity.Mathematics; +using Unity.NetCode; + +namespace ProjectM.Simulation +{ + /// + /// LANTERN Blink — the Movement-archetype Spark: a second, socketed dash. A behaviour-clone of + /// (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 : AbilityFireSystem skips movement sockets + /// (falls through), and BlinkSystem is the SOLE writer of that socket's 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). + /// + /// Runs in AFTER (overrides + /// the input-derived MoveVelocity) and AFTER — 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 TickUtil.NonZero; compared via only. + /// + /// + [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 m_SocketLookup; + ComponentLookup m_SocketCdLookup; + + [BurstCompile] + public void OnCreate(ref SystemState state) + { + state.RequireForUpdate(); + state.RequireForUpdate(); + m_SocketLookup = state.GetBufferLookup(isReadOnly: true); + m_SocketCdLookup = state.GetComponentLookup(isReadOnly: false); + } + + [BurstCompile] + public void OnUpdate(ref SystemState state) + { + if (!SystemAPI.TryGetSingleton(out var netTime) || !netTime.ServerTick.IsValid) + return; + var serverTick = netTime.ServerTick; + uint now = serverTick.TickIndexForValidTick; + var abilityDb = SystemAPI.GetSingleton(); + 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, RefRW, + RefRO, RefRO, RefRO>() + .WithAll().WithDisabled().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; + } + } + } + } +} diff --git a/Assets/_Project/Scripts/Simulation/Player/BlinkSystem.cs.meta b/Assets/_Project/Scripts/Simulation/Player/BlinkSystem.cs.meta new file mode 100644 index 000000000..04d86527d --- /dev/null +++ b/Assets/_Project/Scripts/Simulation/Player/BlinkSystem.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 862bf6b2abc84c045a845de0faf103b1 \ No newline at end of file