Files
Project-M/Assets/_Project/Scripts/Simulation/Player/BlinkSystem.cs
T
kronic 7571091394 LANTERN feel pass (DR-052 + gap list): SoD facing, underwater feel, Bathynaut kit, walk/run gait, suit lamp + new Synty anim packs
- SoD facing: PlayerFacing = body-yaw only (move-facing / cast-turn / idle-hold); every fire
  direction re-sourced to FacingMath.ResolveAim (pre-code review blocking catch); TickWindowMath
  shared windows with Movement-skip; reticle/FX coupled to the damage direction; cursor-dash kept.
- Underwater feel: sharpness 15->6, turn 720->360, MoveSpeed 6->4.2; TuningKnob 26-28
  (0 = no-override sentinels, dev-protocol bump on DebugTuningReport); stride footsteps + silt +
  cadence floor; bubbles; underwater ambience bed + distant groans; camera drag + dev scroll zoom.
- Bathynaut kit in-engine: dome/tank/shoulder-lamp + bare head grafted (GraftSmr rigid rebase,
  RecalculateTangents); EmissiveGloamSkinned shader (Rukhanka deformation); shoulder lamp CASTS
  (warm steady spot on body yaw).
- Gait: two-ring walk/run FreeformDirectional tree (walk @0.35, run @1.0) + blended-natural
  StrideScale; additive Posture(Bank) + Lead(chest-lead) layers; idle = AnimationIdles Base;
  banking driven from facing turn rate; flat terrain (Env_SeabedKit seabed squashed - CC is planar).
- New packs: Synty AnimationIdles + AnimationSwordCombat (combat pass queued) + SyntyPropBoneTool;
  four authored clips (sway/trudge/banks/lean) + Anim_Player_Underwater.blend + suit-kit FBX.
- Validation: 411/411 EditMode green; Play smokes (server==client facing, Aim-true projectile,
  bank/stride live-sampled, lamp beam verified); pre-code + post-impl adversarial reviews applied.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-16 13:56:02 -07:00

140 lines
7.7 KiB
C#

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
// 07-15 underwater feel: the restore target honors the MoveSharpness dev-override (0 = authored const).
var t = SystemAPI.TryGetSingleton<TuningConfig>(out var tcfg) ? tcfg : TuningConfig.Defaults();
float baseSharpness = t.MoveSharpness > 0f ? t.MoveSharpness : DefaultSharpness;
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)
{
// 07-15 fork: Move → cursor Aim → last facing (stationary blink keeps going toward the cursor).
float2 mv = input.ValueRO.Move;
float2 dir = math.lengthsq(mv) > 1e-4f ? mv
: math.lengthsq(input.ValueRO.Aim) > 1e-6f ? input.ValueRO.Aim
: 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 = baseSharpness;
}
}
}
}
}