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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user