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:
@@ -108,6 +108,11 @@ namespace ProjectM.Client
|
||||
TuningRow("Melee combo len", TuningKnob.MeleeComboLength, 1f, "0");
|
||||
GUILayout.Space(4);
|
||||
TuningRow("Struct aggro w", TuningKnob.StructureAggroWeight, 0.1f, "0.00"); // EB-1: <1 prefers structures
|
||||
// 07-15 facing/underwater feel (0 = no override: authored stat / cast const / authored sharpness)
|
||||
TuningRow("Turn rate deg", TuningKnob.TurnRateDeg, 30f, "0");
|
||||
TuningRow("Cast turn deg", TuningKnob.CastTurnRateDeg, 60f, "0");
|
||||
TuningRow("Move sharp", TuningKnob.MoveSharpness, 0.5f, "0.0");
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -61,17 +61,19 @@ namespace ProjectM.Client
|
||||
EntityManager.CompleteDependencyBeforeRO<LocalTransform>();
|
||||
EntityManager.CompleteDependencyBeforeRO<PlayerFacing>();
|
||||
EntityManager.CompleteDependencyBeforeRO<Health>();
|
||||
foreach (var (xform, facing) in
|
||||
SystemAPI.Query<RefRO<LocalTransform>, RefRO<PlayerFacing>>()
|
||||
EntityManager.CompleteDependencyBeforeRO<PlayerInput>();
|
||||
|
||||
foreach (var (xform, facing, input) in
|
||||
SystemAPI.Query<RefRO<LocalTransform>, RefRO<PlayerFacing>, RefRO<PlayerInput>>()
|
||||
.WithAll<GhostOwnerIsLocal, PlayerTag>())
|
||||
{
|
||||
float3 playerPos = xform.ValueRO.Position;
|
||||
|
||||
if (scheme == InputSchemeId.Gamepad)
|
||||
{
|
||||
float2 dir = facing.ValueRO.Direction;
|
||||
if (math.lengthsq(dir) < 1e-6f) dir = new float2(0f, 1f);
|
||||
dir = math.normalize(dir);
|
||||
// 07-15 move-facing model: the ring shows the FIRE direction (raw right stick), not the body
|
||||
// yaw — facing tracks movement outside casts and would lie about where a cast goes.
|
||||
float2 dir = FacingMath.ResolveAim(input.ValueRO.Aim, facing.ValueRO.Direction);
|
||||
ringPos = playerPos + new float3(dir.x, 0f, dir.y) * ReticleDistance;
|
||||
}
|
||||
else
|
||||
@@ -94,7 +96,9 @@ namespace ProjectM.Client
|
||||
|
||||
ringPos.y += ReticleLiftY;
|
||||
lpPos = playerPos;
|
||||
lpFacing = facing.ValueRO.Direction;
|
||||
// Tether cone keys off the same resolved fire direction (matches the server assist seed, which
|
||||
// also reads ResolveAim(Aim, facing) since 07-15).
|
||||
lpFacing = FacingMath.ResolveAim(input.ValueRO.Aim, facing.ValueRO.Direction);
|
||||
haveTarget = true;
|
||||
break;
|
||||
}
|
||||
@@ -111,9 +115,7 @@ namespace ProjectM.Client
|
||||
if (_tetherLine != null && haveTarget && FeelConfig.LockOnEnabled
|
||||
&& (!FeelConfig.LockOnGamepadOnly || scheme == InputSchemeId.Gamepad))
|
||||
{
|
||||
float2 fdir = lpFacing;
|
||||
if (math.lengthsq(fdir) < 1e-6f) fdir = new float2(0f, 1f);
|
||||
fdir = math.normalize(fdir);
|
||||
float2 fdir = lpFacing; // ResolveAim output: always normalized, never zero
|
||||
float rangeSq = FeelConfig.LockOnRange * FeelConfig.LockOnRange;
|
||||
float cone = math.cos(math.radians(FeelConfig.LockOnArcDegrees));
|
||||
float bestSq = float.MaxValue;
|
||||
|
||||
@@ -25,11 +25,15 @@ namespace ProjectM.Client
|
||||
int _lastCountdownSec = -1;
|
||||
bool _bossRoared;
|
||||
|
||||
const float AmbientBaseVolume = 0.10f; // low ambient bed
|
||||
// 07-16 underwater re-voice: the bed volume + groan cadence live on FeelConfig (live-tunable).
|
||||
AudioSource _groanSrc;
|
||||
AudioClip _groanClip;
|
||||
float _nextGroanIn = 12f; // first distant groan ~12s into a session
|
||||
|
||||
protected override void OnCreate()
|
||||
{
|
||||
_ambientClip = MakeDrone();
|
||||
_groanClip = MakeGroan();
|
||||
_stingBeep = MakeSting(880f, 880f, 0.09f, 0.30f); // countdown tick
|
||||
_stingRoar = MakeSting(90f, 38f, 0.90f, 0.60f); // boss-arrival roar (low falling growl)
|
||||
}
|
||||
@@ -43,8 +47,12 @@ namespace ProjectM.Client
|
||||
_ambient.loop = true;
|
||||
_ambient.playOnAwake = false;
|
||||
_ambient.spatialBlend = 0f; // 2D bed
|
||||
_ambient.volume = AmbientBaseVolume * GameVolume.Music;
|
||||
_ambient.volume = FeelConfig.AmbienceVolume * GameVolume.Music;
|
||||
_ambient.Play();
|
||||
_groanSrc = _root.AddComponent<AudioSource>();
|
||||
_groanSrc.playOnAwake = false;
|
||||
_groanSrc.spatialBlend = 0f; // distant, directionless
|
||||
_groanSrc.loop = false;
|
||||
}
|
||||
|
||||
protected override void OnDestroy()
|
||||
@@ -56,7 +64,21 @@ namespace ProjectM.Client
|
||||
{
|
||||
if (_ambient == null) return;
|
||||
|
||||
_ambient.volume = Mathf.MoveTowards(_ambient.volume, AmbientBaseVolume * GameVolume.Music, SystemAPI.Time.DeltaTime * 0.25f);
|
||||
float dt = SystemAPI.Time.DeltaTime;
|
||||
_ambient.volume = Mathf.MoveTowards(_ambient.volume, FeelConfig.AmbienceVolume * GameVolume.Music, dt * 0.25f);
|
||||
|
||||
// Distant groans (07-16 gap-list): a slow low sweep with a soft attack, jittered interval + pitch —
|
||||
// the murk answering back. Presentation-only wall-clock randomness is fine here.
|
||||
if (_groanSrc != null && FeelConfig.GroanIntervalSec > 0f)
|
||||
{
|
||||
_nextGroanIn -= dt;
|
||||
if (_nextGroanIn <= 0f)
|
||||
{
|
||||
_nextGroanIn = FeelConfig.GroanIntervalSec * (0.6f + Random.value * 0.8f);
|
||||
_groanSrc.pitch = 0.85f + Random.value * 0.3f;
|
||||
_groanSrc.PlayOneShot(_groanClip, FeelConfig.GroanVolume * GameVolume.Music);
|
||||
}
|
||||
}
|
||||
|
||||
// Launch countdown beeps (3-2-1) + the boss-arrival roar — replicated-state observations only.
|
||||
if (SystemAPI.TryGetSingleton<RunInfo>(out var runAudio))
|
||||
@@ -94,27 +116,54 @@ namespace ProjectM.Client
|
||||
|
||||
// A low, seamless-looping pad: each partial completes an integer number of cycles over the buffer
|
||||
// (freq snapped to k/duration) so the loop point has no click. A slow tremolo adds motion.
|
||||
// A low, seamless-looping UNDERWATER pressure bed (07-16 re-voice): sub-heavy partials + a pair of
|
||||
// detuned subs whose beat completes exactly once per loop (Snap keeps every partial click-free at the
|
||||
// loop point) — reads as slow water-column pressure swell rather than a musical pad.
|
||||
static AudioClip MakeDrone()
|
||||
{
|
||||
const int rate = 44100;
|
||||
const float dur = 4f;
|
||||
const float dur = 8f;
|
||||
int len = (int)(dur * rate);
|
||||
var clip = AudioClip.Create("ambient_drone", len, 1, rate, false);
|
||||
var clip = AudioClip.Create("underwater_bed", len, 1, rate, false);
|
||||
var data = new float[len];
|
||||
float f0 = Snap(55f, dur); // sub
|
||||
float f1 = Snap(110f, dur); // root
|
||||
float f2 = Snap(164.81f, dur); // fifth-ish
|
||||
float f3 = Snap(220f, dur);
|
||||
float trem = Snap(0.5f, dur); // slow amplitude wobble
|
||||
float f0 = Snap(38f, dur); // deep sub
|
||||
float f0b = f0 + 1f / dur; // detuned sub: beats exactly once per loop (seamless)
|
||||
float f1 = Snap(55f, dur);
|
||||
float f2 = Snap(82.4f, dur); // faint upper body
|
||||
float swell = Snap(0.25f, dur); // very slow amplitude swell
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
float t = i / (float)rate;
|
||||
float s = 0.50f * Mathf.Sin(2f * Mathf.PI * f0 * t)
|
||||
+ 0.35f * Mathf.Sin(2f * Mathf.PI * f1 * t)
|
||||
+ 0.18f * Mathf.Sin(2f * Mathf.PI * f2 * t)
|
||||
+ 0.10f * Mathf.Sin(2f * Mathf.PI * f3 * t);
|
||||
float amp = 0.75f + 0.25f * Mathf.Sin(2f * Mathf.PI * trem * t);
|
||||
data[i] = s * amp * 0.5f; // peak ~0.57, no clipping
|
||||
float s = 0.46f * Mathf.Sin(2f * Mathf.PI * f0 * t)
|
||||
+ 0.34f * Mathf.Sin(2f * Mathf.PI * f0b * t)
|
||||
+ 0.22f * Mathf.Sin(2f * Mathf.PI * f1 * t)
|
||||
+ 0.07f * Mathf.Sin(2f * Mathf.PI * f2 * t);
|
||||
float amp = 0.7f + 0.3f * Mathf.Sin(2f * Mathf.PI * swell * t);
|
||||
data[i] = s * amp * 0.5f;
|
||||
}
|
||||
clip.SetData(data, 0);
|
||||
return clip;
|
||||
}
|
||||
|
||||
// A distant whale-adjacent groan: slow 58->34Hz sweep with a soft sin^2 attack/decay window and a
|
||||
// subtle wobble — never a jump-scare, just the deep being large somewhere off-screen.
|
||||
static AudioClip MakeGroan()
|
||||
{
|
||||
const int rate = 44100;
|
||||
const float dur = 3.4f;
|
||||
int len = (int)(dur * rate);
|
||||
var clip = AudioClip.Create("distant_groan", len, 1, rate, false);
|
||||
var data = new float[len];
|
||||
float phase = 0f;
|
||||
for (int i = 0; i < len; i++)
|
||||
{
|
||||
float t01 = i / (float)len;
|
||||
float t = i / (float)rate;
|
||||
float freq = Mathf.Lerp(58f, 34f, t01) * (1f + 0.03f * Mathf.Sin(2f * Mathf.PI * 1.7f * t));
|
||||
phase += 2f * Mathf.PI * freq / rate;
|
||||
float window = Mathf.Sin(Mathf.PI * t01);
|
||||
float env = window * window; // soft attack AND release
|
||||
data[i] = (0.8f * Mathf.Sin(phase) + 0.2f * Mathf.Sin(2f * phase)) * env * 0.55f;
|
||||
}
|
||||
clip.SetData(data, 0);
|
||||
return clip;
|
||||
|
||||
@@ -85,8 +85,12 @@ namespace ProjectM.Client
|
||||
AudioClip _telegraphClip;
|
||||
AudioClip _dashClip;
|
||||
AudioClip _swingClip;
|
||||
AudioClip _meleeConnectClip, _footstepClip; // combat feel pass: connect thunk / footstep
|
||||
Vector3 _lastFootPos; float _footTimer; bool _footInit; // footstep edge-detect (local player locomotion)
|
||||
AudioClip _meleeConnectClip; // combat feel pass: connect thunk
|
||||
readonly AudioClip[] _footstepClips = new AudioClip[3]; // 07-15: heavy underwater thud variants (random pick per step)
|
||||
ParticleSystem _stepFx; // 07-15: silt puff per footstep
|
||||
ParticleSystem _bubbleFx; float _bubbleTimer; // 07-16: dome bubble exhaust (+1 per footstep, loose sync)
|
||||
Light _lampLight; // 07-16e: the shoulder lamp CASTS — a warm spot beam riding the body yaw
|
||||
Vector3 _lastFootPos; float _footDistAccum; float _footStepGap; bool _footInit; // stride-distance footsteps (local player)
|
||||
|
||||
Entity _localPlayer = Entity.Null;
|
||||
uint[] _lastSocketFire = new uint[SocketId.Count]; // LANTERN per-socket fire-edge cache (was _lastLocalFireTick)
|
||||
@@ -110,7 +114,11 @@ namespace ProjectM.Client
|
||||
_dashClip = MakeClip("dash", 950f, 240f, 0.12f, 0.50f, noise: false);
|
||||
_swingClip = MakeClip("swing", 720f, 200f, 0.09f, 0.42f, noise: false);
|
||||
_meleeConnectClip = MakeClip("melee_thunk", 180f, 60f, 0.13f, 0.55f, noise: true); // meaty low connect
|
||||
_footstepClip = MakeClip("step", 200f, 110f, 0.06f, 0.18f, noise: true); // soft footfall
|
||||
// 07-15 underwater feel: three deep muffled thud variants (noise burst, low sweep, long decay) —
|
||||
// a random pick + volume jitter per step reads as weight (PlayClipAtPoint has no pitch control).
|
||||
_footstepClips[0] = MakeClip("step_a", 95f, 38f, 0.13f, 0.5f, noise: true, decay: 7f);
|
||||
_footstepClips[1] = MakeClip("step_b", 108f, 42f, 0.12f, 0.5f, noise: true, decay: 7f);
|
||||
_footstepClips[2] = MakeClip("step_c", 120f, 45f, 0.11f, 0.5f, noise: true, decay: 7f);
|
||||
}
|
||||
|
||||
protected override void OnStartRunning()
|
||||
@@ -124,6 +132,12 @@ namespace ProjectM.Client
|
||||
_muzzleFx = MakeBurst(_fxRoot, "Muzzle", mat, new Color(0.6f, 2.4f, 3.2f), 0.12f, 5f, 0.20f, 128);
|
||||
_dashFx = MakeBurst(_fxRoot, "DashWhoosh", mat, new Color(0.7f, 2.6f, 3.0f), 0.16f, 4f, 0.30f, 256);
|
||||
_swingFx = MakeBurst(_fxRoot, "MeleeSwing", mat, new Color(3.0f, 2.6f, 0.9f), 0.14f, 6f, 0.28f, 256);
|
||||
// 07-15: silt puff per footstep — dark, slow, hangs briefly (underwater weight read; dark = no bloom).
|
||||
_stepFx = MakeBurst(_fxRoot, "SiltPuff", mat, new Color(0.06f, 0.10f, 0.11f, 0.85f), 0.20f, 0.8f, 1.1f, 128, gravity: 0.03f, shapeRadius: 0.18f, sizeTail: 1.5f);
|
||||
// 07-16: bubble exhaust — tiny pale spheres that RISE (negative gravity) and drift from the dome.
|
||||
_bubbleFx = MakeBurst(_fxRoot, "Bubbles", mat, new Color(0.75f, 0.9f, 1.0f, 0.85f), 0.055f, 0.25f, 2.4f, 128, gravity: -0.45f, shapeRadius: 0.06f, sizeTail: 0.35f);
|
||||
|
||||
|
||||
BuildSlash();
|
||||
|
||||
for (int i = 0; i < NumberPoolSize; i++)
|
||||
@@ -160,6 +174,8 @@ namespace ProjectM.Client
|
||||
EntityManager.CompleteDependencyBeforeRO<DashState>();
|
||||
EntityManager.CompleteDependencyBeforeRO<DashCooldown>();
|
||||
EntityManager.CompleteDependencyBeforeRO<MeleeCombo>();
|
||||
EntityManager.CompleteDependencyBeforeRO<PlayerInput>(); // 07-15: local FX read the fire direction
|
||||
|
||||
|
||||
// Resolve the local player (for hit colouring + fire feedback).
|
||||
_localPlayer = Entity.Null;
|
||||
@@ -326,13 +342,15 @@ namespace ProjectM.Client
|
||||
var socks = EntityManager.GetBuffer<AbilitySocket>(_localPlayer, true);
|
||||
var effs = EntityManager.GetBuffer<EffectiveSocketStats>(_localPlayer, true);
|
||||
ref var fireAdb = ref fireDb.Value.Value;
|
||||
Vector3 sface = Vector3.forward;
|
||||
if (EntityManager.HasComponent<PlayerFacing>(_localPlayer))
|
||||
// 07-15: the cue must match the DAMAGE direction — ResolveAim(Aim, facing), the same source the
|
||||
// sim fire sites read; PlayerFacing alone is body-yaw and can be up to 180° off under move-facing.
|
||||
float2 sfdir = new float2(0f, 1f);
|
||||
if (EntityManager.HasComponent<PlayerFacing>(_localPlayer) && EntityManager.HasComponent<PlayerInput>(_localPlayer))
|
||||
{
|
||||
var sfd = EntityManager.GetComponentData<PlayerFacing>(_localPlayer).Direction;
|
||||
if (math.lengthsq(sfd) > 1e-6f) sface = new Vector3(sfd.x, 0f, sfd.y).normalized;
|
||||
sfdir = FacingMath.ResolveAim(
|
||||
EntityManager.GetComponentData<PlayerInput>(_localPlayer).Aim,
|
||||
EntityManager.GetComponentData<PlayerFacing>(_localPlayer).Direction);
|
||||
}
|
||||
float2 sfdir = new float2(sface.x, sface.z);
|
||||
int sn = math.min(SocketId.Count, math.min(socks.Length, effs.Length));
|
||||
for (int sk = 0; sk < sn; sk++)
|
||||
{
|
||||
@@ -342,7 +360,11 @@ namespace ProjectM.Client
|
||||
if (!edge) continue;
|
||||
byte sid = socks[sk].SparkId;
|
||||
if (sid == 0) continue;
|
||||
bool coneSpark = fireAdb.TryGetAbility(sid, out var sdef) && sdef.Archetype == (byte)AbilityArchetype.Cone;
|
||||
bool haveDef = fireAdb.TryGetAbility(sid, out var sdef);
|
||||
// 07-16 review: a blink (Movement archetype) stamps its cooldown row too — it's a dodge,
|
||||
// not a cast; no muzzle/fire cue (mirrors TickWindowMath's Movement skip).
|
||||
if (haveDef && sdef.Archetype == (byte)AbilityArchetype.Movement) continue;
|
||||
bool coneSpark = haveDef && sdef.Archetype == (byte)AbilityArchetype.Cone;
|
||||
if (!coneSpark)
|
||||
{
|
||||
Burst(_muzzleFx, cfg != null ? cfg.Muzzle : null, (Vector3)localPos + Vector3.up * 0.9f, 8);
|
||||
@@ -405,11 +427,14 @@ namespace ProjectM.Client
|
||||
if (_swingTickInit && mc.SwingStartTick != 0 && mc.SwingStartTick != _lastLocalSwingTick)
|
||||
{
|
||||
int step = math.max(1, (int)mc.Step);
|
||||
// 07-15: match the cleave's DAMAGE direction (ResolveAim(Aim, facing) — same as MeleeComboSystem).
|
||||
Vector3 face = Vector3.forward;
|
||||
if (EntityManager.HasComponent<PlayerFacing>(_localPlayer))
|
||||
if (EntityManager.HasComponent<PlayerFacing>(_localPlayer) && EntityManager.HasComponent<PlayerInput>(_localPlayer))
|
||||
{
|
||||
var d = EntityManager.GetComponentData<PlayerFacing>(_localPlayer).Direction;
|
||||
if (math.lengthsq(d) > 1e-6f) face = new Vector3(d.x, 0f, d.y).normalized;
|
||||
var d = FacingMath.ResolveAim(
|
||||
EntityManager.GetComponentData<PlayerInput>(_localPlayer).Aim,
|
||||
EntityManager.GetComponentData<PlayerFacing>(_localPlayer).Direction);
|
||||
face = new Vector3(d.x, 0f, d.y);
|
||||
}
|
||||
EmitAt(_swingFx, (Vector3)localPos + Vector3.up * 0.9f + face * 0.8f, 6 + (step - 1) * 5);
|
||||
PlayClip(_swingClip, (Vector3)localPos, 0.45f);
|
||||
@@ -458,18 +483,86 @@ namespace ProjectM.Client
|
||||
}
|
||||
|
||||
|
||||
// Footsteps (combat feel): edge-detect local locomotion from the position delta; a soft step at a cadence.
|
||||
// Shoulder-lamp beam (07-16e, operator: "make the lamp actually light up" — LANTERN's light-is-
|
||||
// territory read starts on the suit). A warm STEADY spot (steady = true light) mounted at the kit
|
||||
// lamp's clavicle offset, riding the BODY yaw (PlayerFacing — the lamp is bolted to the suit, so it
|
||||
// sweeps with the body, not the cursor), tilted down onto the seabed ahead. Local player only.
|
||||
if (_localPlayer != Entity.Null && FeelConfig.ShoulderLampIntensity > 0f)
|
||||
{
|
||||
if (_lampLight == null)
|
||||
{
|
||||
var lampGo = new GameObject("~ShoulderLamp");
|
||||
lampGo.transform.SetParent(_fxRoot, false);
|
||||
_lampLight = lampGo.AddComponent<Light>();
|
||||
_lampLight.type = LightType.Spot;
|
||||
_lampLight.color = new Color(1f, 0.78f, 0.45f); // warm gold — ours, steady
|
||||
_lampLight.shadows = LightShadows.None;
|
||||
_lampLight.spotAngle = 58f;
|
||||
_lampLight.innerSpotAngle = 26f;
|
||||
}
|
||||
_lampLight.intensity = FeelConfig.ShoulderLampIntensity;
|
||||
_lampLight.range = FeelConfig.ShoulderLampRange;
|
||||
float2 lampFace = EntityManager.HasComponent<PlayerFacing>(_localPlayer)
|
||||
? EntityManager.GetComponentData<PlayerFacing>(_localPlayer).Direction
|
||||
: new float2(0f, 1f);
|
||||
if (math.lengthsq(lampFace) < 1e-6f) lampFace = new float2(0f, 1f);
|
||||
var lampYaw = Quaternion.LookRotation(new Vector3(lampFace.x, 0f, lampFace.y));
|
||||
_lampLight.transform.SetPositionAndRotation(
|
||||
(Vector3)localPos + lampYaw * new Vector3(0.27f, 0.55f, 0.05f), // the kit lamp's shoulder offset
|
||||
lampYaw * Quaternion.Euler(26f, 0f, 0f)); // down-tilt puts the hotspot ~3m ahead (14° landed ~6m out — too diffuse)
|
||||
}
|
||||
else if (_lampLight != null && _lampLight.intensity > 0f)
|
||||
{
|
||||
_lampLight.intensity = 0f;
|
||||
}
|
||||
|
||||
|
||||
// Bubble exhaust (07-16 gap-list): a periodic trickle from the dome (entity origin ≈ chest; dome
|
||||
// sits ~0.78 up), jittered so it never reads as a metronome. One extra bubble rides each footstep.
|
||||
if (_localPlayer != Entity.Null && FeelConfig.BubbleIntervalSec > 0f && FeelConfig.BubbleBurstCount > 0)
|
||||
{
|
||||
_bubbleTimer -= dt;
|
||||
if (_bubbleTimer <= 0f)
|
||||
{
|
||||
_bubbleTimer = FeelConfig.BubbleIntervalSec * (0.8f + UnityEngine.Random.value * 0.4f);
|
||||
EmitAt(_bubbleFx, (Vector3)localPos + Vector3.up * 0.78f, FeelConfig.BubbleBurstCount);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Footsteps (07-15 underwater feel): stride-DISTANCE stepping — accumulate planar travel and step
|
||||
// every FootstepStrideMeters, so cadence tracks the real (drifting) velocity instead of a fixed
|
||||
// timer. Heavier read: deep thud variant + volume jitter + a silt puff at the feet.
|
||||
if (_localPlayer != Entity.Null)
|
||||
{
|
||||
Vector3 lp = (Vector3)localPos;
|
||||
if (_footInit)
|
||||
{
|
||||
float sp = dt > 1e-4f ? new Vector2(lp.x - _lastFootPos.x, lp.z - _lastFootPos.z).magnitude / dt : 0f;
|
||||
_footTimer -= dt;
|
||||
if (sp >= FeelConfig.FootstepMinSpeed && _footTimer <= 0f)
|
||||
float planar = new Vector2(lp.x - _lastFootPos.x, lp.z - _lastFootPos.z).magnitude;
|
||||
float sp = dt > 1e-4f ? planar / dt : 0f;
|
||||
_footStepGap -= dt;
|
||||
if (sp >= FeelConfig.FootstepMinSpeed)
|
||||
{
|
||||
PlayClip(_footstepClip, lp, FeelConfig.FootstepVolume);
|
||||
_footTimer = FeelConfig.FootstepIntervalSec;
|
||||
_footDistAccum += planar;
|
||||
// 07-16 review: the seconds floor stops a dash/blink from machine-gunning 2-4 thuds in ~0.1s
|
||||
// (the old fixed-interval timer capped this implicitly).
|
||||
if (_footDistAccum >= FeelConfig.FootstepStrideMeters && _footStepGap <= 0f)
|
||||
{
|
||||
_footDistAccum = 0f;
|
||||
_footStepGap = 0.18f;
|
||||
float j = FeelConfig.FootstepJitter;
|
||||
var stepClip = _footstepClips[UnityEngine.Random.Range(0, _footstepClips.Length)];
|
||||
PlayClip(stepClip, lp, FeelConfig.FootstepVolume * (1f + UnityEngine.Random.Range(-j, j)));
|
||||
if (FeelConfig.FootstepPuffCount > 0)
|
||||
EmitAt(_stepFx, lp + Vector3.down * 0.85f, FeelConfig.FootstepPuffCount); // entity origin = capsule center -> feet
|
||||
if (FeelConfig.BubbleBurstCount > 0)
|
||||
EmitAt(_bubbleFx, lp + Vector3.up * 0.78f, 1); // exertion bubble, loosely step-synced
|
||||
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_footDistAccum = FeelConfig.FootstepStrideMeters * 0.6f; // primed: the first step lands quickly on move-start
|
||||
}
|
||||
}
|
||||
_lastFootPos = lp; _footInit = true;
|
||||
@@ -805,6 +898,9 @@ void TriggerSlash(Vector3 pos, float2 facing, float range, float halfAngle, int
|
||||
// renders via _slashMr; here each REMOTE player (interpolated, GhostOwnerIsLocal DISABLED) gets a pooled
|
||||
// slash arc edge-detected from its replicated MeleeCombo.SwingStartTick + PlayerFacing. Observe-only client
|
||||
// presentation; no sim, no new [GhostField]. Anchored to the moving teammate while it sweeps open + fades.
|
||||
// NOTE (07-15 SoD facing, accepted asymmetry): remote arcs deliberately do NOT use FacingMath.ResolveAim —
|
||||
// PlayerInput.Aim is owner-only (never replicated to non-owners), so body-yaw PlayerFacing is the closest
|
||||
// replicated proxy; the cast-turn converges facing onto the aim within a few ticks. Do not "fix" this to Aim.
|
||||
void UpdateRemoteSwings(float dt)
|
||||
{
|
||||
if (!FeelConfig.RemoteSwingEnabled || _fxRoot == null) return;
|
||||
|
||||
@@ -116,12 +116,44 @@ namespace ProjectM.Client
|
||||
public static float MeleeConnectVolume;
|
||||
/// <summary>Kill-pop colored flash density on an enemy death.</summary>
|
||||
public static int KillFlashBurstCount;
|
||||
/// <summary>Soft footstep SFX volume.</summary>
|
||||
/// <summary>Footstep SFX volume (07-15: deep muffled underwater thud).</summary>
|
||||
public static float FootstepVolume;
|
||||
/// <summary>Seconds between footsteps while the local player is moving.</summary>
|
||||
public static float FootstepIntervalSec;
|
||||
/// <summary>Planar meters travelled per footstep — stride-DISTANCE stepping, so cadence tracks the
|
||||
/// real (drifting) velocity instead of a fixed timer (07-15 underwater feel).</summary>
|
||||
public static float FootstepStrideMeters;
|
||||
/// <summary>Local player speed (u/s) above which footsteps play.</summary>
|
||||
public static float FootstepMinSpeed;
|
||||
/// <summary>Per-step random volume jitter (±fraction) — breaks the metronome read.</summary>
|
||||
public static float FootstepJitter;
|
||||
/// <summary>Silt-puff particles per footstep (0 = off).</summary>
|
||||
public static int FootstepPuffCount;
|
||||
|
||||
// ---- 07-16 underwater feel (gap-list): bubbles, ambience, camera weight, banking ----
|
||||
/// <summary>Seconds between dome bubble-exhaust puffs (0 = off).</summary>
|
||||
public static float BubbleIntervalSec;
|
||||
/// <summary>Bubbles per exhaust puff.</summary>
|
||||
public static int BubbleBurstCount;
|
||||
/// <summary>Underwater ambience bed volume (Music bus).</summary>
|
||||
public static float AmbienceVolume;
|
||||
/// <summary>Mean seconds between distant groans (0 = off; ±40% jitter).</summary>
|
||||
public static float GroanIntervalSec;
|
||||
/// <summary>Distant-groan volume (Music bus).</summary>
|
||||
public static float GroanVolume;
|
||||
/// <summary>Camera follow-sharpness multiplier (<1 = the camera drags through the water).</summary>
|
||||
public static float CameraDragMult;
|
||||
/// <summary>The facing turn rate (deg/s) at which the additive body bank reaches full (±1).</summary>
|
||||
public static float BankFullTurnDegPerSec;
|
||||
/// <summary>The WALK ring's natural travel speed (u/s; measured off the pack's root-motion variant).
|
||||
/// StrideScale fine-corrects playback against the walk→run blended natural (07-16b/e).</summary>
|
||||
public static float TrudgeNaturalSpeed;
|
||||
/// <summary>The RUN ring's natural travel speed (u/s; measured).</summary>
|
||||
public static float RunNaturalSpeed;
|
||||
/// <summary>Shoulder-lamp spot intensity (0 = lamp off). LANTERN: light is territory — the suit lamp CASTS.</summary>
|
||||
public static float ShoulderLampIntensity;
|
||||
/// <summary>Shoulder-lamp beam range (m).</summary>
|
||||
public static float ShoulderLampRange;
|
||||
|
||||
|
||||
/// <summary>Master gate for gamepad rumble (no-op on KBM).</summary>
|
||||
public static bool RumbleEnabled;
|
||||
/// <summary>Rumble strength on a local hit taken / dealt.</summary>
|
||||
@@ -228,9 +260,24 @@ namespace ProjectM.Client
|
||||
MeleeConnectFovKick = 0.8f;
|
||||
MeleeConnectVolume = 0.55f;
|
||||
KillFlashBurstCount = 20;
|
||||
FootstepVolume = 0.16f;
|
||||
FootstepIntervalSec = 0.32f;
|
||||
FootstepMinSpeed = 1.5f;
|
||||
FootstepVolume = 0.30f; // 07-15: heavier, more present underwater thud
|
||||
FootstepStrideMeters = 1.5f; // one step per ~1.5m of planar travel
|
||||
FootstepMinSpeed = 1.0f;
|
||||
FootstepJitter = 0.12f;
|
||||
FootstepPuffCount = 5;
|
||||
BubbleIntervalSec = 1.6f; // 07-16: dome bubble exhaust cadence
|
||||
BubbleBurstCount = 2;
|
||||
AmbienceVolume = 0.14f; // underwater bed (Music bus)
|
||||
GroanIntervalSec = 24f; // distant groans, ±40% jitter
|
||||
GroanVolume = 0.35f;
|
||||
CameraDragMult = 0.8f; // heavier camera through the water (multiplies FollowSharpness)
|
||||
BankFullTurnDegPerSec = 270f;
|
||||
TrudgeNaturalSpeed = 1.46f; // measured: A_Walk_FwdStrafeF_RootMotion_Masc averageSpeed (walk ring)
|
||||
RunNaturalSpeed = 2.6f; // measured: A_Run_FwdStrafeF_RootMotion_Masc averageSpeed (run ring)
|
||||
ShoulderLampIntensity = 8f; // 07-16e: the suit lamp actually lights (3 got lost in the murk ambient)
|
||||
ShoulderLampRange = 12f;
|
||||
|
||||
|
||||
RumbleEnabled = true;
|
||||
RumbleHit = 0.25f;
|
||||
RumbleKill = 0.45f;
|
||||
|
||||
@@ -46,17 +46,26 @@ namespace ProjectM.Client
|
||||
static readonly FastAnimatorParameter k_IsAttacking = new FastAnimatorParameter("IsAttacking");
|
||||
static readonly FastAnimatorParameter k_IsFiring = new FastAnimatorParameter("IsFiring"); // Blender-authored A_Fire_OneHand (B7)
|
||||
static readonly FastAnimatorParameter k_IsDashing = new FastAnimatorParameter("IsDashing"); // Blender-authored A_Dash_Lean (local player only - DashState is not replicated)
|
||||
static readonly FastAnimatorParameter k_ComboStep = new FastAnimatorParameter("ComboStep"); // replicated MeleeCombo.Step -> per-step swing clips (Swing1/2/3)
|
||||
static readonly FastAnimatorParameter k_Bank = new FastAnimatorParameter("Bank");
|
||||
static readonly FastAnimatorParameter k_StrideScale = new FastAnimatorParameter("StrideScale"); // 07-16b: Locomotion playback = planarSpeed / TrudgeNaturalSpeed (foot-skate fix) // 07-16: additive Posture layer — signed facing turn rate, local player only (subtle; not derived for remotes)
|
||||
|
||||
static readonly FastAnimatorParameter k_ComboStep = new FastAnimatorParameter("ComboStep"); // replicated MeleeCombo.Step -> per-step swing clips (Swing1/2/3)
|
||||
static readonly FastAnimatorParameter k_IsCone = new FastAnimatorParameter("IsCone"); // ability archetype (replicated AbilityRef + blob): Cone -> two-hand slam, else the shot
|
||||
|
||||
// Ticks after a swing-start that IsAttacking stays true (drives the MeleeSwing state). Kept < the swing lock
|
||||
// (MeleeRecoverTicks ~16) so a CHAINED swing re-pulses the bool false->true and re-triggers the Any State
|
||||
// transition per hit. ~0.22s @ 60Hz. Presentation-only.
|
||||
const uint k_AttackAnimTicks = 13;
|
||||
const uint k_AttackAnimTicks = PlayerAimSystem.CastFacingTicks; // structurally tied (07-16 review): body-yaw cast-turn and the swing anim pulse must agree
|
||||
|
||||
// Remote prevPos cache (per ghost Entity). Pruned every frame (a vanished remote = a despawn).
|
||||
NativeParallelHashMap<Entity, float3> _prevPos;
|
||||
|
||||
// 07-16 banking: previous local-player facing + the smoothed bank value (main-thread state; one entity).
|
||||
float2 _prevLocalFacing;
|
||||
bool _bankInit;
|
||||
float _bankSmoothed;
|
||||
|
||||
|
||||
protected override void OnCreate()
|
||||
{
|
||||
_prevPos = new NativeParallelHashMap<Entity, float3>(16, Allocator.Persistent);
|
||||
@@ -77,13 +86,37 @@ namespace ProjectM.Client
|
||||
// Ability blob for the per-class special read (Cone -> slam anim); default(BlobAssetReference) if absent.
|
||||
var abilityBlob = SystemAPI.TryGetSingleton<AbilityDatabase>(out var adbSingleton) ? adbSingleton.Value : default;
|
||||
|
||||
// --- LOCAL owner (CC velocity) ---
|
||||
// 07-16 banking: signed facing turn rate -> Bank (-1..1, + = bank right, leaning INTO the turn).
|
||||
// Sampled main-thread (a single local player), smoothed so the additive pose eases in and out.
|
||||
float bankTarget = 0f;
|
||||
foreach (var facingRO in SystemAPI.Query<RefRO<PlayerFacing>>().WithAll<GhostOwnerIsLocal>())
|
||||
{
|
||||
float2 f = facingRO.ValueRO.Direction;
|
||||
if (_bankInit && math.lengthsq(f) > 1e-6f && math.lengthsq(_prevLocalFacing) > 1e-6f)
|
||||
{
|
||||
float2 a2 = math.normalize(_prevLocalFacing);
|
||||
float2 b2 = math.normalize(f);
|
||||
float signed = math.atan2(a2.x * b2.y - a2.y * b2.x, math.clamp(math.dot(a2, b2), -1f, 1f));
|
||||
float degPerSec = math.degrees(signed) / dt;
|
||||
bankTarget = math.clamp(-degPerSec / math.max(30f, FeelConfig.BankFullTurnDegPerSec), -1f, 1f);
|
||||
}
|
||||
_prevLocalFacing = f;
|
||||
_bankInit = true;
|
||||
break;
|
||||
}
|
||||
_bankSmoothed = math.lerp(_bankSmoothed, bankTarget, 1f - math.exp(-6f * dt));
|
||||
|
||||
|
||||
// --- LOCAL owner (CC velocity) ---
|
||||
var localJob = new LocalDriveJob
|
||||
{
|
||||
moveX = k_MoveX, moveZ = k_MoveZ, speed = k_Speed, isDead = k_IsDead,
|
||||
isAttacking = k_IsAttacking, isFiring = k_IsFiring, isDashing = k_IsDashing,
|
||||
comboStep = k_ComboStep, isCone = k_IsCone, abilityDb = abilityBlob,
|
||||
serverTick = serverTick, attackTicks = k_AttackAnimTicks,
|
||||
bank = k_Bank, bankValue = _bankSmoothed,
|
||||
strideScale = k_StrideScale, trudgeNaturalSpeed = math.max(0.5f, FeelConfig.TrudgeNaturalSpeed),
|
||||
runNaturalSpeed = math.max(0.5f, FeelConfig.RunNaturalSpeed),
|
||||
};
|
||||
Dependency = localJob.ScheduleParallel(Dependency);
|
||||
|
||||
@@ -96,6 +129,8 @@ namespace ProjectM.Client
|
||||
comboStep = k_ComboStep, isCone = k_IsCone, abilityDb = abilityBlob,
|
||||
serverTick = serverTick, attackTicks = k_AttackAnimTicks,
|
||||
dt = dt,
|
||||
strideScale = k_StrideScale, trudgeNaturalSpeed = math.max(0.5f, FeelConfig.TrudgeNaturalSpeed),
|
||||
runNaturalSpeed = math.max(0.5f, FeelConfig.RunNaturalSpeed),
|
||||
prevPos = _prevPos,
|
||||
seen = seen,
|
||||
};
|
||||
@@ -113,53 +148,9 @@ namespace ProjectM.Client
|
||||
if (!seen.Contains(keys[i])) _prevPos.Remove(keys[i]);
|
||||
}
|
||||
|
||||
// True while now is within [SwingStartTick, SwingStartTick + animTicks) -- a per-swing pulse that re-triggers
|
||||
// on each chained swing. NetworkTick arithmetic (wrap-safe). Presentation-only, Burst-safe.
|
||||
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);
|
||||
}
|
||||
|
||||
// B7: the class FIRE ability finally plays a body animation - a stateless window derived from the
|
||||
// replicated AbilityCooldown ([GhostField] NextFireTick) minus the locally-derived CooldownTicks
|
||||
// (EffectiveAbilityStats recomputes identically on every world), so it works for the predicted local
|
||||
// player AND interpolated remotes with no cached edges (review B7: edge-caches false-fire on
|
||||
// relevancy/join/rollback). Reuses the swing clip until a dedicated fire clip exists.
|
||||
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);
|
||||
}
|
||||
|
||||
// LANTERN 4-socket fire/cone anim resolution (any-socket model): firing if ANY socketed Spark's
|
||||
// per-socket cooldown window is mid-fire; cone if any such active socket holds a Cone-archetype Spark.
|
||||
// Replaces the single-ability FireActive + IsCone reads (AbilityCooldown/EffectiveAbilityStats/AbilityRef).
|
||||
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;
|
||||
if (!FireActive(cd.Get(sk), effSockets[sk].CooldownTicks, serverTick, animTicks)) continue;
|
||||
firing = true;
|
||||
if (haveDb)
|
||||
{
|
||||
ref var adb = ref abilityDb.Value;
|
||||
if (adb.TryGetAbility(sid, out var d) && d.Archetype == (byte)AbilityArchetype.Cone) cone = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// The swing/fire tick-window predicates (SwingActive/FireActive/SocketFireAndCone) moved to
|
||||
// TickWindowMath (Simulation/Combat) 07-15 — shared verbatim with the predicted facing path
|
||||
// (PlayerAimSystem); this system passes k_AttackAnimTicks at the call sites.
|
||||
|
||||
// LOCAL: GhostOwnerIsLocal ENABLED -> exactly the owned player. WithPresent<Dead> so alive
|
||||
// (Dead-disabled) players are visited. NOTE: GhostOwnerIsLocal as a WithAll filter respects the
|
||||
@@ -169,7 +160,13 @@ namespace ProjectM.Client
|
||||
[WithPresent(typeof(Dead))]
|
||||
partial struct LocalDriveJob : IJobEntity
|
||||
{
|
||||
public FastAnimatorParameter moveX, moveZ, speed, isDead, isAttacking, isFiring, isDashing, comboStep, isCone;
|
||||
public FastAnimatorParameter bank;
|
||||
public float bankValue;
|
||||
public FastAnimatorParameter strideScale;
|
||||
public float trudgeNaturalSpeed;
|
||||
public float runNaturalSpeed;
|
||||
|
||||
public FastAnimatorParameter moveX, moveZ, speed, isDead, isAttacking, isFiring, isDashing, comboStep, isCone;
|
||||
public BlobAssetReference<AbilityDatabaseBlob> abilityDb;
|
||||
public NetworkTick serverTick;
|
||||
public uint attackTicks;
|
||||
@@ -190,8 +187,8 @@ namespace ProjectM.Client
|
||||
var a = new AnimatorParametersAspect(parametersArr, indexTable);
|
||||
float3 p = AnimParamMath.LocomotionParams(body.RelativeVelocity, facing.Direction, stats.MoveSpeed);
|
||||
Write(ref a, p, dead.ValueRO, moveX, moveZ, speed, isDead);
|
||||
if (a.HasParameter(isAttacking)) a.SetParameterValue(isAttacking, SwingActive(melee, serverTick, attackTicks));
|
||||
SocketFireAndCone(socketCd, sockets, effSockets, abilityDb, serverTick, attackTicks, out bool firingL, out bool coneL);
|
||||
if (a.HasParameter(isAttacking)) a.SetParameterValue(isAttacking, TickWindowMath.SwingActive(melee, serverTick, attackTicks));
|
||||
TickWindowMath.SocketFireAndCone(socketCd, sockets, effSockets, abilityDb, serverTick, attackTicks, out bool firingL, out bool coneL);
|
||||
if (a.HasParameter(isFiring)) a.SetParameterValue(isFiring, firingL);
|
||||
// Dash lean: the LOCAL player's predicted DashState window [StartTick, IFrameUntilTick+tail).
|
||||
bool dashActive = false;
|
||||
@@ -202,6 +199,10 @@ namespace ProjectM.Client
|
||||
dashActive = dStart.IsValid && dEnd.IsValid && !dStart.IsNewerThan(serverTick) && dEnd.IsNewerThan(serverTick);
|
||||
}
|
||||
if (a.HasParameter(isDashing)) a.SetParameterValue(isDashing, dashActive);
|
||||
if (a.HasParameter(bank)) a.SetParameterValue(bank, bankValue);
|
||||
if (a.HasParameter(strideScale))
|
||||
a.SetParameterValue(strideScale, StrideScaleValue(math.length(body.RelativeVelocity.xz), stats.MoveSpeed, trudgeNaturalSpeed, runNaturalSpeed));
|
||||
|
||||
if (a.HasParameter(comboStep)) a.SetParameterValue(comboStep, (int)melee.Step);
|
||||
if (a.HasParameter(isCone)) a.SetParameterValue(isCone, coneL);
|
||||
}
|
||||
@@ -219,6 +220,9 @@ namespace ProjectM.Client
|
||||
public NetworkTick serverTick;
|
||||
public uint attackTicks;
|
||||
public float dt;
|
||||
public FastAnimatorParameter strideScale;
|
||||
public float trudgeNaturalSpeed;
|
||||
public float runNaturalSpeed;
|
||||
public NativeParallelHashMap<Entity, float3> prevPos;
|
||||
public NativeParallelHashSet<Entity> seen;
|
||||
|
||||
@@ -245,16 +249,30 @@ namespace ProjectM.Client
|
||||
var a = new AnimatorParametersAspect(parametersArr, indexTable);
|
||||
float3 p = AnimParamMath.LocomotionParams(vel, facing.Direction, stats.MoveSpeed);
|
||||
Write(ref a, p, dead.ValueRO, moveX, moveZ, speed, isDead);
|
||||
if (a.HasParameter(isAttacking)) a.SetParameterValue(isAttacking, SwingActive(melee, serverTick, attackTicks));
|
||||
SocketFireAndCone(socketCd, sockets, effSockets, abilityDb, serverTick, attackTicks, out bool firingR, out bool coneR);
|
||||
if (a.HasParameter(isAttacking)) a.SetParameterValue(isAttacking, TickWindowMath.SwingActive(melee, serverTick, attackTicks));
|
||||
TickWindowMath.SocketFireAndCone(socketCd, sockets, effSockets, abilityDb, serverTick, attackTicks, out bool firingR, out bool coneR);
|
||||
if (a.HasParameter(isFiring)) a.SetParameterValue(isFiring, firingR);
|
||||
if (a.HasParameter(isDashing)) a.SetParameterValue(isDashing, false); // DashState is not replicated to remotes
|
||||
if (a.HasParameter(strideScale))
|
||||
a.SetParameterValue(strideScale, StrideScaleValue(math.length(vel.xz), stats.MoveSpeed, trudgeNaturalSpeed, runNaturalSpeed));
|
||||
if (a.HasParameter(comboStep)) a.SetParameterValue(comboStep, (int)melee.Step);
|
||||
if (a.HasParameter(isCone)) a.SetParameterValue(isCone, coneR);
|
||||
}
|
||||
}
|
||||
|
||||
// ParameterValue has implicit float/bool operators -> SetParameterValue(key, float) / (key, bool) compile.
|
||||
// 07-16e: playback correction against the walk→run BLENDED natural — the two-ring gait tree picks
|
||||
// the clip; this keeps residual cadence honest between/beyond the rings. Pure, Burst-safe.
|
||||
static float StrideScaleValue(float planarSpeed, float maxSpeed, float walkNatural, float runNatural)
|
||||
{
|
||||
float maxSp = math.max(0.5f, maxSpeed);
|
||||
float walkRing = walkNatural / maxSp;
|
||||
float t = math.saturate((planarSpeed / maxSp - walkRing) / math.max(0.05f, 1f - walkRing));
|
||||
float natural = math.lerp(walkNatural, runNatural, t);
|
||||
return math.clamp(planarSpeed / math.max(0.3f, natural), 0.6f, 2.2f);
|
||||
}
|
||||
|
||||
|
||||
// ParameterValue has implicit float/bool operators -> SetParameterValue(key, float) / (key, bool) compile.
|
||||
static void Write(ref AnimatorParametersAspect a, float3 p, bool isDeadVal,
|
||||
FastAnimatorParameter moveX, FastAnimatorParameter moveZ,
|
||||
FastAnimatorParameter speed, FastAnimatorParameter isDead)
|
||||
|
||||
@@ -96,6 +96,15 @@ namespace ProjectM.Client
|
||||
Camera _cam;
|
||||
Vector3 _leadOffset; // smoothed look-ahead offset (world units), eased toward the desired lead each frame
|
||||
|
||||
// 07-16c DEV ZOOM (LoL/SoD-style scroll-to-inspect; may not ship): the wheel moves a smoothed
|
||||
// distance target between ZoomMinDistance and the rig's authored Distance — zoom IN only, scroll
|
||||
// back out returns to the gameplay framing. Windows wheels report ±120/notch, some devices ±1.
|
||||
const float ZoomMinDistance = 3.5f;
|
||||
const float ZoomStepPerNotch = 1.1f;
|
||||
const float ZoomSharpness = 10f;
|
||||
float _zoomTarget = -1f;
|
||||
float _zoomDist = -1f;
|
||||
|
||||
void Awake() => _cam = GetComponent<Camera>();
|
||||
|
||||
void LateUpdate()
|
||||
@@ -127,10 +136,26 @@ namespace ProjectM.Client
|
||||
_leadOffset = Vector3.Lerp(_leadOffset, desiredLead, leadK);
|
||||
target += _leadOffset;
|
||||
|
||||
var rot = Quaternion.Euler(Pitch, Yaw, 0f);
|
||||
Vector3 desired = target - (rot * Vector3.forward) * Distance;
|
||||
// Dev zoom: scroll up = closer; clamped [ZoomMinDistance, authored Distance].
|
||||
if (_zoomTarget < 0f) { _zoomTarget = Distance; _zoomDist = Distance; }
|
||||
var scrollMouse = UnityEngine.InputSystem.Mouse.current;
|
||||
if (scrollMouse != null)
|
||||
{
|
||||
float raw = scrollMouse.scroll.ReadValue().y;
|
||||
float notches = Mathf.Abs(raw) >= 100f ? raw / 120f : raw;
|
||||
if (Mathf.Abs(notches) > 0.01f)
|
||||
_zoomTarget = Mathf.Clamp(_zoomTarget - notches * ZoomStepPerNotch, ZoomMinDistance, Distance);
|
||||
}
|
||||
_zoomTarget = Mathf.Min(_zoomTarget, Distance); // authored Distance can change live in the inspector
|
||||
_zoomDist = Mathf.Lerp(_zoomDist, _zoomTarget, 1f - Mathf.Exp(-ZoomSharpness * Time.deltaTime));
|
||||
|
||||
float k = FollowSharpness <= 0f ? 1f : 1f - Mathf.Exp(-FollowSharpness * Time.deltaTime);
|
||||
var rot = Quaternion.Euler(Pitch, Yaw, 0f);
|
||||
Vector3 desired = target - (rot * Vector3.forward) * _zoomDist;
|
||||
|
||||
// 07-16 underwater feel: CameraDragMult (<1) makes the follow DRAG through the water (0 = knob
|
||||
// uninitialized -> no change). Multiplies the serialized FollowSharpness so scenes stay untouched.
|
||||
float dragSharp = FollowSharpness * Mathf.Max(0.05f, FeelConfig.CameraDragMult <= 0f ? 1f : FeelConfig.CameraDragMult);
|
||||
float k = FollowSharpness <= 0f ? 1f : 1f - Mathf.Exp(-dragSharp * Time.deltaTime);
|
||||
Vector3 basePos;
|
||||
if (s_holdFrames > 0) { s_holdFrames--; basePos = transform.position; } // C4: brief hit-stop hold (freeze the follow a couple frames)
|
||||
else basePos = Vector3.Lerp(transform.position, desired, k);
|
||||
|
||||
Reference in New Issue
Block a user