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:
@@ -171,8 +171,7 @@ namespace ProjectM.Simulation
|
||||
{
|
||||
if (isServer)
|
||||
{
|
||||
float2 cFace = facing.ValueRO.Direction;
|
||||
cFace = math.lengthsq(cFace) < 1e-6f ? new float2(0f, 1f) : math.normalize(cFace);
|
||||
float2 cFace = FacingMath.ResolveAim(input.ValueRO.Aim, facing.ValueRO.Direction); // manual-aim (07-15): cursor wins; facing fallback = resting gamepad stick
|
||||
float cRange = math.max(0.1f, es.Range);
|
||||
float cCosHalf = math.cos(math.clamp(es.AutoTargetConeRadians, 0.01f, 3.14159f));
|
||||
uint cStamp = TickUtil.NonZero(serverTick.TickIndexForValidTick);
|
||||
@@ -206,8 +205,7 @@ namespace ProjectM.Simulation
|
||||
if (abilityPrefabs[i].Id == sparkId) { aoePrefab = abilityPrefabs[i].Prefab; break; }
|
||||
if (aoePrefab != Entity.Null)
|
||||
{
|
||||
float2 aFace = facing.ValueRO.Direction;
|
||||
aFace = math.lengthsq(aFace) < 1e-6f ? new float2(0f, 1f) : math.normalize(aFace);
|
||||
float2 aFace = FacingMath.ResolveAim(input.ValueRO.Aim, facing.ValueRO.Direction); // manual-aim (07-15)
|
||||
float3 spawnPos = xform.ValueRO.Position + new float3(aFace.x, 0f, aFace.y) * k_AoeCastAhead;
|
||||
spawnPos.y = xform.ValueRO.Position.y;
|
||||
uint expire = adef.DurationTicks > 0
|
||||
@@ -239,8 +237,7 @@ namespace ProjectM.Simulation
|
||||
{
|
||||
if (isServer)
|
||||
{
|
||||
float2 hFace = facing.ValueRO.Direction;
|
||||
hFace = math.lengthsq(hFace) < 1e-6f ? new float2(0f, 1f) : math.normalize(hFace);
|
||||
float2 hFace = FacingMath.ResolveAim(input.ValueRO.Aim, facing.ValueRO.Direction); // manual-aim (07-15)
|
||||
float hRange = math.max(0.1f, es.Range);
|
||||
uint hStamp = TickUtil.NonZero(serverTick.TickIndexForValidTick);
|
||||
for (int hi = 0; hi < coneTargets.Length; hi++)
|
||||
@@ -276,8 +273,9 @@ namespace ProjectM.Simulation
|
||||
|
||||
uint socketFireCount = applied.InternalInput.GetSocket(sk).Count;
|
||||
|
||||
float2 rawAim = facing.ValueRO.Direction;
|
||||
rawAim = math.lengthsq(rawAim) < 1e-6f ? new float2(0f, 1f) : math.normalize(rawAim);
|
||||
// Manual-aim (07-15): the projectile (and the server's auto-target seed below) fires along the
|
||||
// CURRENT tick's replicated Aim — PlayerFacing is body-yaw only under the SoD facing model.
|
||||
float2 rawAim = FacingMath.ResolveAim(input.ValueRO.Aim, facing.ValueRO.Direction);
|
||||
|
||||
// Client fires along raw aim; only the server applies the gamepad auto-target assist.
|
||||
float2 dir = rawAim;
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
using Unity.Collections;
|
||||
using Unity.Entities;
|
||||
using Unity.Mathematics;
|
||||
using Unity.NetCode;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Pure tick-window predicates over replicated combat state (MeleeCombo swing window, per-socket
|
||||
/// cooldown-derived fire windows). Extracted VERBATIM from PlayerAnimationDriveSystem (MC-4/B7) so the
|
||||
/// predicted facing path (PlayerAimSystem) and the client animation path share one implementation.
|
||||
/// Window lengths stay CALLER parameters (the anim path passes its k_AttackAnimTicks; the facing path
|
||||
/// passes its own) — never baked constants. Movement-archetype sockets (dash/blink) NEVER count as
|
||||
/// firing: a blink is a dodge, not a cast (this also kills the latent cooldown-tail phantom IsFiring
|
||||
/// pulse the old include-all check produced — the blink socket's stamped cooldown row used to open a
|
||||
/// fake window ~2.5s after each blink). Burst-safe statics (byte archetypes, no enums), EditMode-tested.
|
||||
/// </summary>
|
||||
public static class TickWindowMath
|
||||
{
|
||||
/// <summary>True while now is within [SwingStartTick, SwingStartTick + animTicks) — a per-swing pulse
|
||||
/// that re-triggers on each chained swing. NetworkTick arithmetic (wrap-safe).</summary>
|
||||
public static bool SwingActive(in MeleeCombo mc, NetworkTick serverTick, uint animTicks)
|
||||
{
|
||||
if (mc.SwingStartTick == 0u || !serverTick.IsValid) return false;
|
||||
var start = new NetworkTick(mc.SwingStartTick);
|
||||
var end = new NetworkTick(TickUtil.NonZero(mc.SwingStartTick + animTicks));
|
||||
return start.IsValid && end.IsValid && !start.IsNewerThan(serverTick) && end.IsNewerThan(serverTick);
|
||||
}
|
||||
|
||||
/// <summary>True while now is within the stateless fire window [NextFireTick - CooldownTicks, +animTicks)
|
||||
/// reconstructed from the replicated per-socket cooldown stamp minus the locally-derived CooldownTicks
|
||||
/// (recomputes identically on every world; no cached edges — edge-caches false-fire on
|
||||
/// relevancy/join/rollback). Window start shifts if cooldown modifiers change mid-window — documented
|
||||
/// acceptable (B7).</summary>
|
||||
public static bool FireActive(uint nextFireRaw, int cooldownTicks, NetworkTick serverTick, uint animTicks)
|
||||
{
|
||||
if (nextFireRaw == 0u || cooldownTicks <= 0 || !serverTick.IsValid) return false;
|
||||
uint startRaw = TickUtil.NonZero(nextFireRaw - (uint)cooldownTicks);
|
||||
var start = new NetworkTick(startRaw);
|
||||
var end = new NetworkTick(TickUtil.NonZero(startRaw + animTicks));
|
||||
return start.IsValid && end.IsValid && !start.IsNewerThan(serverTick) && end.IsNewerThan(serverTick);
|
||||
}
|
||||
|
||||
/// <summary>LANTERN 4-socket fire/cone resolution (any-socket model): firing if ANY socketed,
|
||||
/// NON-Movement Spark's per-socket window is mid-fire; cone if any such active socket holds a
|
||||
/// Cone-archetype Spark. Movement sockets are skipped OUTRIGHT (blink = dodge, not cast). Without a
|
||||
/// blob the archetype is unknown — the socket is included (matches the old fallback behavior).</summary>
|
||||
public static void SocketFireAndCone(in SocketCooldown cd, DynamicBuffer<AbilitySocket> sockets,
|
||||
DynamicBuffer<EffectiveSocketStats> effSockets, BlobAssetReference<AbilityDatabaseBlob> abilityDb,
|
||||
NetworkTick serverTick, uint animTicks, out bool firing, out bool cone)
|
||||
{
|
||||
firing = false; cone = false;
|
||||
int n = math.min(SocketId.Count, math.min(sockets.Length, effSockets.Length));
|
||||
bool haveDb = abilityDb.IsCreated;
|
||||
for (int sk = 0; sk < n; sk++)
|
||||
{
|
||||
byte sid = sockets[sk].SparkId;
|
||||
if (sid == 0) continue;
|
||||
bool isCone = false;
|
||||
if (haveDb)
|
||||
{
|
||||
ref var adb = ref abilityDb.Value;
|
||||
if (adb.TryGetAbility(sid, out var d))
|
||||
{
|
||||
if (d.Archetype == (byte)AbilityArchetype.Movement) continue; // dodge, never a cast
|
||||
isCone = d.Archetype == (byte)AbilityArchetype.Cone;
|
||||
}
|
||||
}
|
||||
if (!FireActive(cd.Get(sk), effSockets[sk].CooldownTicks, serverTick, animTicks)) continue;
|
||||
firing = true;
|
||||
if (isCone) cone = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 573dd7baac36e8142a3c746b488b8c74
|
||||
@@ -56,6 +56,13 @@ namespace ProjectM.Simulation
|
||||
public float StaggerKnockbackSpeed;
|
||||
public float SeparationMaxSpeed;
|
||||
|
||||
// Facing/underwater feel (07-15): DEV OVERRIDES with a 0 = NO-OVERRIDE sentinel (0 falls back to the
|
||||
// deterministic source: authored stat / compile-time const / authored sharpness). Unlike other knobs
|
||||
// these default to 0 and clamp >= 0 — consumers substitute their source, a 0 never reaches an integrator.
|
||||
public float TurnRateDeg; // locomotion body-turn (deg/s); 0 = EffectiveCharacterStats.TurnRateRadiansPerSec
|
||||
public float CastTurnRateDeg; // cast-window body-turn (deg/s); 0 = PlayerAimSystem's cast-turn const
|
||||
public float MoveSharpness; // grounded accel sharpness; 0 = CharacterComponent.DefaultGroundedSharpness
|
||||
|
||||
/// <summary>The baked feel defaults == the pre-MC-0 consts. Single source of truth for the fallback path.</summary>
|
||||
public static TuningConfig Defaults() => new TuningConfig
|
||||
{
|
||||
@@ -81,6 +88,10 @@ namespace ProjectM.Simulation
|
||||
StructureAggroWeight = 0.7f, // EB-1: <1 prefers structures (fortress aggro); live-tunable
|
||||
StaggerKnockbackSpeed = 7f, // B2 poise: kb.Speed >= this interrupts windups/lunges; below = nudge only
|
||||
SeparationMaxSpeed = 3f, // B1: max separation push (units/s) so soft-collision can't fling
|
||||
TurnRateDeg = 0f, // 07-15 facing feel: 0 = no override (authored stat wins)
|
||||
CastTurnRateDeg = 0f, // 0 = no override (PlayerAimSystem cast-turn const wins)
|
||||
MoveSharpness = 0f, // 0 = no override (DefaultGroundedSharpness wins)
|
||||
|
||||
};
|
||||
|
||||
/// <summary>Clamp a knob to its safe floor: tick knobs >= 1, value knobs >= 0. Used by every write path
|
||||
@@ -101,6 +112,9 @@ namespace ProjectM.Simulation
|
||||
case TuningKnob.MeleeFinisherMult:
|
||||
case TuningKnob.StructureAggroWeight:
|
||||
case TuningKnob.StaggerKnockbackSpeed:
|
||||
case TuningKnob.TurnRateDeg: // 07-15: 0 = no-override sentinel (never reaches an integrator)
|
||||
case TuningKnob.CastTurnRateDeg:
|
||||
case TuningKnob.MoveSharpness:
|
||||
case TuningKnob.SeparationMaxSpeed:
|
||||
return math.max(0f, value);
|
||||
// tick knobs: >= 1 (a 0 tick count is degenerate; a 0 i-frame window divides-by-zero in DashSystem).
|
||||
@@ -138,6 +152,10 @@ namespace ProjectM.Simulation
|
||||
case TuningKnob.StructureAggroWeight: c.StructureAggroWeight = value; break;
|
||||
case TuningKnob.StaggerKnockbackSpeed: c.StaggerKnockbackSpeed = value; break;
|
||||
case TuningKnob.SeparationMaxSpeed: c.SeparationMaxSpeed = value; break;
|
||||
case TuningKnob.TurnRateDeg: c.TurnRateDeg = value; break;
|
||||
case TuningKnob.CastTurnRateDeg: c.CastTurnRateDeg = value; break;
|
||||
case TuningKnob.MoveSharpness: c.MoveSharpness = value; break;
|
||||
|
||||
// unknown index -> no-op (matches the no-default switch convention in DebugCommandReceiveSystem)
|
||||
}
|
||||
}
|
||||
@@ -169,6 +187,10 @@ namespace ProjectM.Simulation
|
||||
case TuningKnob.StructureAggroWeight: return c.StructureAggroWeight;
|
||||
case TuningKnob.StaggerKnockbackSpeed: return c.StaggerKnockbackSpeed;
|
||||
case TuningKnob.SeparationMaxSpeed: return c.SeparationMaxSpeed;
|
||||
case TuningKnob.TurnRateDeg: return c.TurnRateDeg;
|
||||
case TuningKnob.CastTurnRateDeg: return c.CastTurnRateDeg;
|
||||
case TuningKnob.MoveSharpness: return c.MoveSharpness;
|
||||
|
||||
default: return 0f;
|
||||
}
|
||||
}
|
||||
@@ -198,6 +220,10 @@ namespace ProjectM.Simulation
|
||||
StructureAggroWeight = c.StructureAggroWeight,
|
||||
StaggerKnockbackSpeed = c.StaggerKnockbackSpeed,
|
||||
SeparationMaxSpeed = c.SeparationMaxSpeed,
|
||||
TurnRateDeg = c.TurnRateDeg,
|
||||
CastTurnRateDeg = c.CastTurnRateDeg,
|
||||
MoveSharpness = c.MoveSharpness,
|
||||
|
||||
};
|
||||
|
||||
/// <summary>Reconstruct the full config from a wire snapshot (FULL state, not a delta).</summary>
|
||||
@@ -225,6 +251,10 @@ namespace ProjectM.Simulation
|
||||
StructureAggroWeight = r.StructureAggroWeight,
|
||||
StaggerKnockbackSpeed = r.StaggerKnockbackSpeed,
|
||||
SeparationMaxSpeed = r.SeparationMaxSpeed,
|
||||
TurnRateDeg = r.TurnRateDeg,
|
||||
CastTurnRateDeg = r.CastTurnRateDeg,
|
||||
MoveSharpness = r.MoveSharpness,
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
@@ -255,9 +285,13 @@ namespace ProjectM.Simulation
|
||||
// 20 = CoreDamagePerHusk · 21 = CoreRegenIntervalTicks · 22 = CoreOverrunDrainPct · 23 = FinalSiegeMultiplier
|
||||
public const byte StaggerKnockbackSpeed = 24;
|
||||
public const byte SeparationMaxSpeed = 25;
|
||||
// 07-15 facing/underwater feel dev-overrides (0 = no-override sentinel; see TuningConfig fields):
|
||||
public const byte TurnRateDeg = 26;
|
||||
public const byte CastTurnRateDeg = 27;
|
||||
public const byte MoveSharpness = 28;
|
||||
|
||||
/// <summary>Knob count (overlay iteration bound).</summary>
|
||||
public const byte Count = 26;
|
||||
public const byte Count = 29;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -290,5 +324,11 @@ namespace ProjectM.Simulation
|
||||
public float StructureAggroWeight;
|
||||
public float StaggerKnockbackSpeed;
|
||||
public float SeparationMaxSpeed;
|
||||
public float TurnRateDeg;
|
||||
public float CastTurnRateDeg;
|
||||
public float MoveSharpness;
|
||||
}
|
||||
|
||||
// NOTE: appending fields = a DEV-PROTOCOL BUMP (RpcCollection hash) — rebuild both peers together.
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
using Unity.Mathematics;
|
||||
|
||||
namespace ProjectM.Simulation
|
||||
{
|
||||
/// <summary>
|
||||
/// Pure facing/aim math for the Shape-of-Dreams facing model (body yaw follows movement; turns toward
|
||||
/// the aim only inside a cast window; holds when idle; the cursor is never passively tracked).
|
||||
/// RotateToward is the verbatim extraction of PlayerAimSystem's rate-limited planar turn so it stays
|
||||
/// the tested-in-play math. ResolveAim is THE gameplay fire-direction resolver — every damage/spawn
|
||||
/// direction (AbilityFireSystem archetypes, MeleeComboSystem cleave) and the aim-readout presentation
|
||||
/// (reticle, local slash arcs) must route through it so sim and FX can never diverge. EditMode-tested,
|
||||
/// Burst-safe, no World needed.
|
||||
/// </summary>
|
||||
public static class FacingMath
|
||||
{
|
||||
/// <summary>Gameplay fire direction: raw replicated Aim when meaningful, else the current body facing
|
||||
/// (resting gamepad right stick — preserves controller-first "zero aim = movement heading" because
|
||||
/// facing tracks Move under the SoD model), else world +Z. Always normalized.</summary>
|
||||
public static float2 ResolveAim(float2 aim, float2 facing)
|
||||
{
|
||||
if (math.lengthsq(aim) > 1e-6f) return math.normalize(aim);
|
||||
if (math.lengthsq(facing) > 1e-6f) return math.normalize(facing);
|
||||
return new float2(0f, 1f);
|
||||
}
|
||||
|
||||
/// <summary>The shared Aim→Move→hold cascade. castActive grants Aim PRIORITY only — a zero Aim
|
||||
/// (resting gamepad stick mid-cast) falls through to Move, then to "no target" (hold previous
|
||||
/// facing). Returns false when there is no target this tick.</summary>
|
||||
public static bool SelectTarget(bool castActive, float2 aim, float2 move, out float2 target)
|
||||
{
|
||||
if (castActive && math.lengthsq(aim) > 1e-6f)
|
||||
{
|
||||
target = math.normalize(aim);
|
||||
return true;
|
||||
}
|
||||
if (math.lengthsq(move) > 1e-6f)
|
||||
{
|
||||
target = math.normalize(move);
|
||||
return true;
|
||||
}
|
||||
target = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>Rate-limited planar rotate toward a normalized target: snaps when uninitialized or within
|
||||
/// reach this step, else rotates by maxStepRadians toward the target. Deterministic pure math
|
||||
/// (fixed-step dt at the caller) so it replays identically on rollback re-simulation.</summary>
|
||||
public static float2 RotateToward(float2 current, float2 target, float maxStepRadians)
|
||||
{
|
||||
if (math.lengthsq(current) < 1e-6f)
|
||||
return target; // uninitialized facing -> snap to target
|
||||
|
||||
float2 cur = math.normalize(current);
|
||||
float angle = math.acos(math.clamp(math.dot(cur, target), -1f, 1f));
|
||||
if (angle <= maxStepRadians)
|
||||
return target; // within reach this step
|
||||
|
||||
float sign = (cur.x * target.y - cur.y * target.x) >= 0f ? 1f : -1f;
|
||||
math.sincos(maxStepRadians * sign, out float sn, out float cs);
|
||||
return math.normalize(new float2(cur.x * cs - cur.y * sn, cur.x * sn + cur.y * cs));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 00ded88e4e66e02469b8f67c4864634f
|
||||
@@ -61,6 +61,10 @@ namespace ProjectM.Simulation
|
||||
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>,
|
||||
@@ -97,8 +101,11 @@ namespace ProjectM.Simulation
|
||||
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 : facing.ValueRO.Direction;
|
||||
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;
|
||||
@@ -124,7 +131,7 @@ namespace ProjectM.Simulation
|
||||
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 = DefaultSharpness;
|
||||
character.ValueRW.GroundedMovementSharpness = baseSharpness;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,8 +17,10 @@ namespace ProjectM.Simulation
|
||||
public struct CharacterComponent : IComponentData
|
||||
{
|
||||
/// <summary>The CC's default grounded-movement smoothing sharpness — the single source for GetDefault, the
|
||||
/// authoring default, DashSystem's base, and the death-state reset.</summary>
|
||||
public const float DefaultGroundedSharpness = 15f;
|
||||
/// authoring default, DashSystem's base, and the death-state reset. 07-15 underwater feel: 15 → 6 (slow
|
||||
/// build-up + glidey stop — the seabed drag read); Player.prefab's serialized value must match (serialized
|
||||
/// wins over this initializer at bake). Dev-override: TuningKnob.MoveSharpness (0 = this const).</summary>
|
||||
public const float DefaultGroundedSharpness = 6f;
|
||||
|
||||
/// <summary>How quickly RelativeVelocity is lerped toward the target velocity on the ground.</summary>
|
||||
public float GroundedMovementSharpness;
|
||||
|
||||
@@ -48,6 +48,9 @@ namespace ProjectM.Simulation
|
||||
uint cooldownTicks = (uint)math.max(1f, t.DashCooldownTicks);
|
||||
float dashSpeed = t.DashDistance / (iFrameTicks / SimTickRate); // iFrameTicks>=1 -> never div-by-0 (review F1)
|
||||
float dashSharpness = t.DashSharpness;
|
||||
// 07-15 underwater feel: the restore target honors the MoveSharpness dev-override (0 = authored const).
|
||||
float baseSharpness = t.MoveSharpness > 0f ? t.MoveSharpness : DefaultSharpness;
|
||||
|
||||
|
||||
foreach (var (ds, cd, control, character, input, facing) in
|
||||
SystemAPI.Query<RefRW<DashState>, RefRW<DashCooldown>, RefRW<CharacterControl>,
|
||||
@@ -61,10 +64,13 @@ namespace ProjectM.Simulation
|
||||
&& new NetworkTick(ds.ValueRO.RecoverUntilTick).IsNewerThan(serverTick);
|
||||
if (input.ValueRO.Dash.IsSet && ready && !inWindow)
|
||||
{
|
||||
// C1: dash toward MOVEMENT input when moving (a panic 'dash away' while aiming at the threat now
|
||||
// works), else toward facing (a stationary aim-and-dash). Pure fn of replicated input -> idempotent.
|
||||
// C1 + 07-15 fork: dash toward MOVEMENT input when moving (a panic 'dash away' still works); else
|
||||
// toward the CURSOR aim (stationary aim-and-dash keeps today's KBM feel under move-facing —
|
||||
// operator fork 07-15); else last facing (resting gamepad stick). Pure replicated input -> idempotent.
|
||||
float2 mv = input.ValueRO.Move;
|
||||
float2 dir = math.lengthsq(mv) > 1e-4f ? mv : facing.ValueRO.Direction;
|
||||
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);
|
||||
ds.ValueRW.Dir = dir;
|
||||
@@ -94,12 +100,12 @@ namespace ProjectM.Simulation
|
||||
else if (recoverActive)
|
||||
{
|
||||
control.ValueRW.MoveVelocity = float3.zero; // movement locked during the punishable tail
|
||||
character.ValueRW.GroundedMovementSharpness = DefaultSharpness;
|
||||
character.ValueRW.GroundedMovementSharpness = baseSharpness;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (character.ValueRO.GroundedMovementSharpness != DefaultSharpness)
|
||||
character.ValueRW.GroundedMovementSharpness = DefaultSharpness; // restore after the dash
|
||||
if (character.ValueRO.GroundedMovementSharpness != baseSharpness)
|
||||
character.ValueRW.GroundedMovementSharpness = baseSharpness; // restore after the dash
|
||||
|
||||
// Window-close edge: score a wasted dash (negated nothing) ONCE, then clear the window.
|
||||
// SERVER-only — the DevTelemetry singleton exists only in the (editor) server world; the
|
||||
|
||||
@@ -160,8 +160,9 @@ namespace ProjectM.Simulation
|
||||
bool hasMods = m_StatModLookup.HasBuffer(entity);
|
||||
float pDamage = math.max(0f, hasMods ? StatMath.Apply(baseDamage, StatTarget.MeleeDamage, m_StatModLookup[entity]) : baseDamage);
|
||||
float pRange = math.max(0f, hasMods ? StatMath.Apply(baseRange, StatTarget.MeleeRange, m_StatModLookup[entity]) : baseRange);
|
||||
float2 face = facing.ValueRO.Direction;
|
||||
face = math.lengthsq(face) < 1e-6f ? new float2(0f, 1f) : math.normalize(face);
|
||||
// Manual-aim cleave (07-15): the cone follows the CURRENT tick's replicated Aim (SoD skillshot
|
||||
// grammar) — PlayerFacing is body-yaw only; the facing fallback covers a resting gamepad stick.
|
||||
float2 face = FacingMath.ResolveAim(input.ValueRO.Aim, facing.ValueRO.Direction);
|
||||
cleaves.Add(new PendingCleave
|
||||
{
|
||||
From = xform.ValueRO.Position,
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,9 @@ namespace ProjectM.Simulation
|
||||
if (SystemAPI.HasComponent<CharacterComponent>(entity))
|
||||
{
|
||||
var cc = SystemAPI.GetComponent<CharacterComponent>(entity);
|
||||
cc.GroundedMovementSharpness = CharacterComponent.DefaultGroundedSharpness;
|
||||
// 07-15: honor the MoveSharpness dev-override (0 = authored const) like the dash/blink restores.
|
||||
var tdc = SystemAPI.TryGetSingleton<TuningConfig>(out var tdcv) ? tdcv : TuningConfig.Defaults();
|
||||
cc.GroundedMovementSharpness = tdc.MoveSharpness > 0f ? tdc.MoveSharpness : CharacterComponent.DefaultGroundedSharpness;
|
||||
SystemAPI.SetComponent(entity, cc);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user