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());
}
}
}
}