From 8f099c32ba5dc6c2ddeec58e220cbf5ffdf04ca5 Mon Sep 17 00:00:00 2001 From: Luis Gonzalez Date: Wed, 15 Jul 2026 08:35:20 -0700 Subject: [PATCH] LANTERN P1 step 6: manual-aim windups (SoD readable telegraphs) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AbilityDefBlob/AbilityDefinition gain WindupTicks. AbilityFireSystem's socket loop resolves each socket at the historical command (serverTick - WindupTicks) via GetDataAtTick — a pure fn of replicated data on both worlds (Build Spec §4/ DB-6: no local fireAtTick that would diverge under reprediction; fireCount from that command, spawn uses the current tick's aim/pos; history gap = no-fire). WindupTicks set on the 4 non-Blink Sparks (Blink is movement, resolved by BlinkSystem). 497 EditMode green (no regression). Client telegraph VFX + the reprediction L3 test are operator-eyes / netcode-world follow-ups. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Abilities/Ability_DecoyWisp.asset | 1 + .../_Project/Abilities/Ability_HookPull.asset | 1 + .../Abilities/Ability_LightZone.asset | 1 + .../_Project/Abilities/Ability_Vortex.asset | 1 + .../Combat/AbilityDatabaseAuthoring.cs | 1 + .../Authoring/Combat/AbilityDefinition.cs | 3 +++ .../Simulation/Combat/AbilityDatabaseBlob.cs | 1 + .../Simulation/Combat/AbilityFireSystem.cs | 25 +++++++++++++------ 8 files changed, 27 insertions(+), 7 deletions(-) diff --git a/Assets/_Project/Abilities/Ability_DecoyWisp.asset b/Assets/_Project/Abilities/Ability_DecoyWisp.asset index c526ff589..ca14d6d4e 100644 --- a/Assets/_Project/Abilities/Ability_DecoyWisp.asset +++ b/Assets/_Project/Abilities/Ability_DecoyWisp.asset @@ -23,4 +23,5 @@ MonoBehaviour: CooldownTicks: 360 Reel: 0 EffectDurationTicks: 480 + WindupTicks: 12 ProjectilePrefab: {fileID: 3885353946372160549, guid: a5b1532dc1acca045856fd653fe392b9, type: 3} diff --git a/Assets/_Project/Abilities/Ability_HookPull.asset b/Assets/_Project/Abilities/Ability_HookPull.asset index 95606e27d..235cd8fc0 100644 --- a/Assets/_Project/Abilities/Ability_HookPull.asset +++ b/Assets/_Project/Abilities/Ability_HookPull.asset @@ -23,4 +23,5 @@ MonoBehaviour: CooldownTicks: 120 Reel: 1 EffectDurationTicks: 0 + WindupTicks: 8 ProjectilePrefab: {fileID: 8857056134628386430, guid: 700fb9c3f7cf94830a0ee78c4e4f1290, type: 3} diff --git a/Assets/_Project/Abilities/Ability_LightZone.asset b/Assets/_Project/Abilities/Ability_LightZone.asset index 116aceb5a..9deb5cd98 100644 --- a/Assets/_Project/Abilities/Ability_LightZone.asset +++ b/Assets/_Project/Abilities/Ability_LightZone.asset @@ -23,4 +23,5 @@ MonoBehaviour: CooldownTicks: 480 Reel: 0 EffectDurationTicks: 420 + WindupTicks: 18 ProjectilePrefab: {fileID: 3885353946372160549, guid: e12b85c23fd562d49b27a3d2fdfd9cad, type: 3} diff --git a/Assets/_Project/Abilities/Ability_Vortex.asset b/Assets/_Project/Abilities/Ability_Vortex.asset index bc354c7d6..e88f58186 100644 --- a/Assets/_Project/Abilities/Ability_Vortex.asset +++ b/Assets/_Project/Abilities/Ability_Vortex.asset @@ -23,4 +23,5 @@ MonoBehaviour: CooldownTicks: 420 Reel: 0 EffectDurationTicks: 300 + WindupTicks: 20 ProjectilePrefab: {fileID: 3885353946372160549, guid: 6e31b46e818d7f348931e3d0ea73bf23, type: 3} diff --git a/Assets/_Project/Scripts/Authoring/Combat/AbilityDatabaseAuthoring.cs b/Assets/_Project/Scripts/Authoring/Combat/AbilityDatabaseAuthoring.cs index 6f681124c..7ec39bee0 100644 --- a/Assets/_Project/Scripts/Authoring/Combat/AbilityDatabaseAuthoring.cs +++ b/Assets/_Project/Scripts/Authoring/Combat/AbilityDatabaseAuthoring.cs @@ -52,6 +52,7 @@ namespace ProjectM.Authoring CooldownTicks = def.CooldownTicks, EffectFlags = (byte)(def.Reel ? ProjectileEffectFlag.Reel : (byte)0), DurationTicks = def.EffectDurationTicks, + WindupTicks = def.WindupTicks, Name = def.DisplayName, }; } diff --git a/Assets/_Project/Scripts/Authoring/Combat/AbilityDefinition.cs b/Assets/_Project/Scripts/Authoring/Combat/AbilityDefinition.cs index 5629e9211..84b2aac18 100644 --- a/Assets/_Project/Scripts/Authoring/Combat/AbilityDefinition.cs +++ b/Assets/_Project/Scripts/Authoring/Combat/AbilityDefinition.cs @@ -33,6 +33,9 @@ namespace ProjectM.Authoring public bool Reel = false; [Min(0), Tooltip("Aoe/zone + decoy lifetime in ticks (0 = system default).")] public int EffectDurationTicks = 0; + [Header("Manual-aim windup")] + [Min(0), Tooltip("Telegraph delay (ticks, ~60/s) before the ability resolves. 0 = instant. The SoD 'readable windup'.")] + public int WindupTicks = 0; [Header("Prefab (baked into the prefab buffer, not the blob)")] public GameObject ProjectilePrefab; diff --git a/Assets/_Project/Scripts/Simulation/Combat/AbilityDatabaseBlob.cs b/Assets/_Project/Scripts/Simulation/Combat/AbilityDatabaseBlob.cs index f773bed46..36ec3f5a1 100644 --- a/Assets/_Project/Scripts/Simulation/Combat/AbilityDatabaseBlob.cs +++ b/Assets/_Project/Scripts/Simulation/Combat/AbilityDatabaseBlob.cs @@ -27,6 +27,7 @@ namespace ProjectM.Simulation public int CooldownTicks; public byte EffectFlags; // ProjectileEffectFlag bits stamped at spawn (the Reel bit ⇒ HookPull homes the hit target to the caster). Config, not replicated. public int DurationTicks; // Aoe/zone + decoy lifetime, ticks (0 = system default). Reel leash is a system const. + public int WindupTicks; // manual-aim telegraph delay (ticks) before the ability resolves; 0 = instant (Build Spec §4/DB-6) public FixedString64Bytes Name; } diff --git a/Assets/_Project/Scripts/Simulation/Combat/AbilityFireSystem.cs b/Assets/_Project/Scripts/Simulation/Combat/AbilityFireSystem.cs index 1d540ea13..e072d210c 100644 --- a/Assets/_Project/Scripts/Simulation/Combat/AbilityFireSystem.cs +++ b/Assets/_Project/Scripts/Simulation/Combat/AbilityFireSystem.cs @@ -131,9 +131,8 @@ namespace ProjectM.Simulation BoonEffects bfx = m_BoonEffectsLookup.HasComponent(entity) ? m_BoonEffectsLookup[entity] : default; bool pull = (bfx.Flags & BoonFlag.KnockToPull) != 0; - // Replicated command buffer at this tick (per-socket fire count for the SpawnId + scheme for aim assist). + // Replicated command buffer (windup resolve + per-socket fire count for the SpawnId + scheme for aim assist). var inputBuffer = SystemAPI.GetBuffer>(entity); - bool haveApplied = inputBuffer.GetDataAtTick(serverTick, out var applied); bool cdDirty = false; int socketCount = math.min(SocketId.Count, sockets.Length); @@ -141,7 +140,23 @@ namespace ProjectM.Simulation { byte sparkId = sockets[sk].SparkId; if (sparkId == 0) continue; // empty socket - if (!input.ValueRO.GetSocket(sk).IsSet) continue; // this socket not fired this tick + + EffectiveSocketStats es = sk < effSockets.Length ? effSockets[sk] : default; + byte archetype = adb.TryGetAbility(sparkId, out var adef) ? adef.Archetype : (byte)AbilityArchetype.Projectile; + + // MANUAL-AIM WINDUP (Build Spec §4/DB-6): resolve when the command WindupTicks ago had this socket + // set, so the resolve tick is a pure fn of replicated data on both worlds (no local fireAtTick that + // diverges under reprediction). fireCount comes from THAT command; the spawn uses the CURRENT tick's + // aim/position. WindupTicks == 0 = instant. A history gap (older than the buffer window) = no-fire. + NetworkTick resolveTick = serverTick; + if (adef.WindupTicks > 0) + { + uint riNow = serverTick.TickIndexForValidTick; + if (riNow <= (uint)adef.WindupTicks) continue; // not enough history yet -> no-fire + resolveTick = new NetworkTick(riNow - (uint)adef.WindupTicks); + } + if (!inputBuffer.GetDataAtTick(resolveTick, out var applied)) continue; // history gap -> no-fire + if (!applied.InternalInput.GetSocket(sk).IsSet) continue; // socket not fired at the resolve tick // Per-socket cooldown gate (0 = ready). uint nextFireRaw = cd.Get(sk); @@ -151,9 +166,6 @@ namespace ProjectM.Simulation if (nextTick.IsValid && nextTick.IsNewerThan(serverTick)) continue; } - EffectiveSocketStats es = sk < effSockets.Length ? effSockets[sk] : default; - byte archetype = adb.TryGetAbility(sparkId, out var adef) ? adef.Archetype : (byte)AbilityArchetype.Projectile; - // CONE: no projectile ghost. Predict the cooldown on both worlds; apply server-only cleave. if (archetype == (byte)AbilityArchetype.Cone) { @@ -261,7 +273,6 @@ namespace ProjectM.Simulation if (abilityPrefabs[i].Id == sparkId) { prefab = abilityPrefabs[i].Prefab; break; } } if (prefab == Entity.Null) continue; - if (!haveApplied) continue; uint socketFireCount = applied.InternalInput.GetSocket(sk).Count;