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>
This commit is contained in:
@@ -7,62 +7,66 @@ using Unity.Transforms;
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Predicted aim/facing: writes <see cref="PlayerFacing"/> from twin-stick Aim, falling back to
|
||||
/// the movement direction when Aim is zero (controller-first directional aim). Also turns the
|
||||
/// ghost transform toward the facing direction for top-down presentation. When there is no input
|
||||
/// this tick the previous facing is held. Deterministic (pure math); filtered to
|
||||
/// <see cref="Simulate"/> so it runs only for predicted ghosts.
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[UpdateInGroup(typeof(PredictedSimulationSystemGroup))]
|
||||
[UpdateAfter(typeof(MeleeComboSystem))]
|
||||
[BurstCompile]
|
||||
public partial struct PlayerAimSystem : ISystem
|
||||
{
|
||||
/// <summary>Cast-window turn rate (rad/s): snappy turn toward the aim mid-swing/fire (1080 deg/s).</summary>
|
||||
public const float DefaultCastTurnRateRadiansPerSec = 18.8495559f;
|
||||
|
||||
/// <summary>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).</summary>
|
||||
public const uint CastFacingTicks = 13;
|
||||
|
||||
[BurstCompile]
|
||||
public void OnUpdate(ref SystemState state)
|
||||
{
|
||||
float dt = SystemAPI.Time.DeltaTime;
|
||||
foreach (var (facing, transform, input, stats) in
|
||||
SystemAPI.Query<RefRW<PlayerFacing>, RefRW<LocalTransform>, RefRO<PlayerInput>, RefRO<EffectiveCharacterStats>>()
|
||||
.WithAll<Simulate>().WithDisabled<Dead>())
|
||||
NetworkTick serverTick = SystemAPI.TryGetSingleton<NetworkTime>(out var nt) ? nt.ServerTick : default;
|
||||
var abilityDb = SystemAPI.TryGetSingleton<AbilityDatabase>(out var adbSingleton) ? adbSingleton.Value : default;
|
||||
var tc = SystemAPI.TryGetSingleton<TuningConfig>(out var tcs) ? tcs : TuningConfig.Defaults();
|
||||
var effSocketLookup = SystemAPI.GetBufferLookup<EffectiveSocketStats>(true);
|
||||
|
||||
foreach (var (facing, transform, input, stats, melee, socketCd, sockets, entity) in
|
||||
SystemAPI.Query<RefRW<PlayerFacing>, RefRW<LocalTransform>, RefRO<PlayerInput>,
|
||||
RefRO<EffectiveCharacterStats>, RefRO<MeleeCombo>, RefRO<SocketCooldown>,
|
||||
DynamicBuffer<AbilitySocket>>()
|
||||
.WithAll<Simulate>().WithDisabled<Dead>().WithEntityAccess())
|
||||
{
|
||||
float2 aim = input.ValueRO.Aim;
|
||||
if (math.lengthsq(aim) < 1e-6f)
|
||||
aim = input.ValueRO.Move; // fall back to movement heading
|
||||
if (math.lengthsq(aim) < 1e-6f)
|
||||
continue; // no input this tick: keep last facing
|
||||
// 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 _);
|
||||
|
||||
aim = math.normalize(aim);
|
||||
if (!FacingMath.SelectTarget(castActive, input.ValueRO.Aim, input.ValueRO.Move, out float2 target))
|
||||
continue; // no target this tick: keep last facing
|
||||
|
||||
// Rate-limited turn: rotate the current facing toward the aim target by at most
|
||||
// TurnRateRadiansPerSec * dt this tick. Deterministic (pure planar math, fixed-step dt)
|
||||
// so it replays correctly on rollback; the first tick (uninitialized facing) snaps.
|
||||
float2 cur = facing.ValueRO.Direction;
|
||||
float2 dir;
|
||||
if (math.lengthsq(cur) < 1e-6f)
|
||||
{
|
||||
dir = aim; // uninitialized facing -> snap to target
|
||||
}
|
||||
else
|
||||
{
|
||||
cur = math.normalize(cur);
|
||||
float maxStep = stats.ValueRO.TurnRateRadiansPerSec * dt;
|
||||
float angle = math.acos(math.clamp(math.dot(cur, aim), -1f, 1f));
|
||||
if (angle <= maxStep)
|
||||
{
|
||||
dir = aim; // within reach this tick
|
||||
}
|
||||
else
|
||||
{
|
||||
float sign = (cur.x * aim.y - cur.y * aim.x) >= 0f ? 1f : -1f;
|
||||
math.sincos(maxStep * sign, out float sn, out float cs);
|
||||
dir = math.normalize(new float2(cur.x * cs - cur.y * sn, cur.x * sn + cur.y * cs));
|
||||
}
|
||||
}
|
||||
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;
|
||||
|
||||
float3 forward = new float3(dir.x, 0f, dir.y);
|
||||
transform.ValueRW.Rotation = quaternion.LookRotationSafe(forward, math.up());
|
||||
transform.ValueRW.Rotation = quaternion.LookRotationSafe(new float3(dir.x, 0f, dir.y), math.up());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user