using Unity.Burst; using Unity.Entities; using Unity.Mathematics; using Unity.NetCode; using Unity.Transforms; namespace ProjectM.Simulation { /// /// Predicted body facing — the Shape-of-Dreams model (07-15): while MOVING the body turns toward the /// movement direction; during a CAST window (melee swing, any non-Movement socket fire window) it turns /// toward the aim at a snappier rate; idle holds the last facing. The cursor is never passively tracked. /// PlayerFacing is presentation/body-yaw ONLY — every gameplay fire direction reads /// FacingMath.ResolveAim(PlayerInput.Aim, facing) at its own site (AbilityFireSystem, MeleeComboSystem). /// The rate-limited turn is an incremental integrator over the snapshot-restored [GhostField] /// PlayerFacing — NO IsFirstTimeFullyPredictingTick guard; it must re-integrate on EVERY predicted pass /// so rollback re-simulation converges (gating it freezes facing at the rollback-tick value and diverges /// from the server). Partial-tick writes are discarded by the prediction restore before the next full /// tick. [UpdateAfter(MeleeComboSystem)] is a hygiene pin (sorter tie-breaks are deterministic and /// cross-world-identical) so the melee cast window opens the same tick the swing starts; socket windows /// open 1 tick late by construction (AbilityFireSystem stamps AFTER this system) — cosmetic-only. /// Turn rates: locomotion = EffectiveCharacterStats.TurnRateRadiansPerSec; cast = a const — both /// dev-overridable via TuningKnob.TurnRateDeg / CastTurnRateDeg (0 = no override; Defaults() fallback /// keeps release worlds server==client). Deterministic (pure math, fixed-step dt); Simulate-filtered. /// [UpdateInGroup(typeof(PredictedSimulationSystemGroup))] [UpdateAfter(typeof(MeleeComboSystem))] [BurstCompile] public partial struct PlayerAimSystem : ISystem { /// Cast-window turn rate (rad/s): snappy turn toward the aim mid-swing/fire (1080 deg/s). public const float DefaultCastTurnRateRadiansPerSec = 18.8495559f; /// Ticks facing stays aimed after a swing/fire starts (~0.22s @60Hz — matches the anim pulse /// k_AttackAnimTicks so body yaw and the swing animation agree). public const uint CastFacingTicks = 13; [BurstCompile] public void OnUpdate(ref SystemState state) { float dt = SystemAPI.Time.DeltaTime; NetworkTick serverTick = SystemAPI.TryGetSingleton(out var nt) ? nt.ServerTick : default; var abilityDb = SystemAPI.TryGetSingleton(out var adbSingleton) ? adbSingleton.Value : default; var tc = SystemAPI.TryGetSingleton(out var tcs) ? tcs : TuningConfig.Defaults(); var effSocketLookup = SystemAPI.GetBufferLookup(true); foreach (var (facing, transform, input, stats, melee, socketCd, sockets, entity) in SystemAPI.Query, RefRW, RefRO, RefRO, RefRO, RefRO, DynamicBuffer>() .WithAll().WithDisabled().WithEntityAccess()) { // Cast window: melee swing first (cheap), else any non-Movement socket fire window // (TickWindowMath skips Movement sockets — a blink is a dodge, never a cast). bool castActive = TickWindowMath.SwingActive(melee.ValueRO, serverTick, CastFacingTicks); if (!castActive && effSocketLookup.HasBuffer(entity)) TickWindowMath.SocketFireAndCone(socketCd.ValueRO, sockets, effSocketLookup[entity], abilityDb, serverTick, CastFacingTicks, out castActive, out _); if (!FacingMath.SelectTarget(castActive, input.ValueRO.Aim, input.ValueRO.Move, out float2 target)) continue; // no target this tick: keep last facing float rate = castActive ? (tc.CastTurnRateDeg > 0f ? math.radians(tc.CastTurnRateDeg) : DefaultCastTurnRateRadiansPerSec) : (tc.TurnRateDeg > 0f ? math.radians(tc.TurnRateDeg) : stats.ValueRO.TurnRateRadiansPerSec); float2 dir = FacingMath.RotateToward(facing.ValueRO.Direction, target, rate * dt); facing.ValueRW.Direction = dir; transform.ValueRW.Rotation = quaternion.LookRotationSafe(new float3(dir.x, 0f, dir.y), math.up()); } } } }