Slice Combat Depth (MC-3 + wiring + review fixes): Spitter aim-line + player-hit punch, rigged enemies, in-band gate (DR-041)
Completes the Combat Depth slice on top of the MC-2 server spine (56cf60cce):
MC-3 impact juice (client, observe-only):
- 7 FeelConfig fields + ResetDefaults; magnitude-scaled player-dealt-hit camera
PunchFov on the enemy-Health-decrease edge (camera-only hit-stop, never timeScale).
- Spitter Kind==2 aim-LANE telegraph (BuildLaneMesh) — reads baked SpitterState
client-side, falls back to a fixed length. True freeze + material flash deferred.
Content / wiring:
- SpitterProjectilePrefabAuthoring (the SpitterProjectilePrefab singleton).
- Both directors rebuilt to a 4-entry KIND-INDEXED roster [Grunt,Charger,Spitter,
Swarmer] + mix/MaxAlive config + the SpitterProjectileConfig singleton in the subscene.
- Real rigged models: EnemySpitter (re-skinned Kaiju, ranged poker) + EnemySwarmerUndead
(Undead-Werewolf, fast/low-HP); grunt/charger keep Werewolf/ChargerMuscle. EnemySpit =
ownerless interpolated ghost (no Health, no collider).
Post-impl adversarial review fixes (wf_febdcfdb-665):
- [MED] in-band fire gate: the Spitter committed its telegraph from ANY range (fired while
advancing from far). Now commits only when sInBand || sCornered (gives CorneredRange a
real read site) — a Spitter out-of-band holds fire and repositions.
- [LOW] EnemyProjectileDamageSystem early-returns on !ServerTick.IsValid (sibling parity).
- [LOW] EnemyAuthoring bake-time guard: errors if a prefab composes both Charger+Spitter
(would match zero AI passes -> never move).
- [LOW] tests: Spitter brain fires from Expedition (kills the Base==0 region false-green);
a direct partition-exclusion test replaces the order-masked claim; added out-of-band +
cornered negative tests.
388/388 EditMode green + two Play smokes (clean boot, fire, swept-hit, region, server==
client; rigged Kaiju spitter bakes + fires with zero console errors). Accepted as-is
(documented in DR-041): global spit soft-cap, co-op punch attribution.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -217,7 +217,15 @@ namespace ProjectM.Client
|
||||
PlayClip(_hitClip, (Vector3)p, FeelConfig.HitSfxVolume);
|
||||
PrototypeCameraRig.AddShake(isLocalPlayer ? FeelConfig.HitShakeLocal : FeelConfig.HitShakeRemote);
|
||||
if (isLocalPlayer) PrototypeCameraRig.PunchFov(FeelConfig.HitStopFovKick, FeelConfig.HitStopDurationMs);
|
||||
if (isEnemy) ShowHealthBar(entity); // Feature B: arm/refresh this enemy's bar on a damage edge
|
||||
if (isEnemy)
|
||||
{
|
||||
// MC-3: net-new player-dealt-hit camera punch — scales with the bite size
|
||||
// (saturate(delta / RefDamage)) so a chip reads soft and a heavy connect snaps.
|
||||
// Camera-only hit-stop (NEVER Time.timeScale); keys on the enemy Health-decrease edge.
|
||||
float hitMag = math.saturate((prev.Hp - cur) / math.max(1f, FeelConfig.HitStopRefDamage));
|
||||
PrototypeCameraRig.PunchFov(math.lerp(FeelConfig.HitStopFovKickMin, FeelConfig.HitStopFovKickMax, hitMag), FeelConfig.HitStopDurationMs);
|
||||
ShowHealthBar(entity); // Feature B: arm/refresh this enemy's bar on a damage edge
|
||||
}
|
||||
}
|
||||
|
||||
// Respawn recovery: the LOCAL player's Health rising from <=0 back to positive. No healing
|
||||
@@ -739,7 +747,20 @@ namespace ProjectM.Client
|
||||
}
|
||||
float coneRange = math.max(1f, stats.ValueRO.AttackRange + 0.6f);
|
||||
if (lunging) coneRange += 1.5f; // forward-stretch the wedge to read the committed travel
|
||||
BuildDangerMesh(go.GetComponent<MeshFilter>().sharedMesh, coneRange, 0.7f, intensity);
|
||||
if (tele.ValueRO.Kind == ZoneEnemyMath.KindSpitter)
|
||||
{
|
||||
// MC-3: a Spitter is a RANGED threat — a melee wedge at its feet is useless. Paint a thin aim
|
||||
// LANE along its (face-locked) facing out to projectile reach during wind-up, brightening as the
|
||||
// shot nears so the player reads the line to dodge/dash across it.
|
||||
float laneLen = 12f;
|
||||
if (SystemAPI.HasComponent<SpitterState>(entity))
|
||||
{
|
||||
var ss = SystemAPI.GetComponent<SpitterState>(entity);
|
||||
laneLen = math.max(4f, ss.PreferredRange + ss.RangeTolerance + 2f);
|
||||
}
|
||||
BuildLaneMesh(go.GetComponent<MeshFilter>().sharedMesh, laneLen, 0.28f, intensity);
|
||||
}
|
||||
else BuildDangerMesh(go.GetComponent<MeshFilter>().sharedMesh, coneRange, 0.7f, intensity);
|
||||
float2 fwd = AnimParamMath.PlanarForward(xf.ValueRO.Rotation);
|
||||
var tr = go.transform;
|
||||
tr.position = (Vector3)xf.ValueRO.Position + Vector3.up * 0.06f;
|
||||
@@ -892,6 +913,33 @@ namespace ProjectM.Client
|
||||
mesh.RecalculateBounds();
|
||||
}
|
||||
|
||||
// MC-3: a thin forward LANE (filled quad in local +Z) for a Spitter's ranged aim telegraph, vertex-alpha
|
||||
// ramped by `intensity` (brightening toward the shot). Built into the same pooled danger mesh; the GO is
|
||||
// already rotated to the enemy facing, so +Z is "toward the locked target".
|
||||
static void BuildLaneMesh(Mesh mesh, float length, float halfWidth, float intensity)
|
||||
{
|
||||
float a = 0.18f + 0.62f * intensity;
|
||||
var verts = new Vector3[4]
|
||||
{
|
||||
new Vector3(-halfWidth, 0f, 0.2f),
|
||||
new Vector3( halfWidth, 0f, 0.2f),
|
||||
new Vector3(-halfWidth, 0f, length),
|
||||
new Vector3( halfWidth, 0f, length),
|
||||
};
|
||||
var cols = new Color[4]
|
||||
{
|
||||
new Color(1f, 1f, 1f, a),
|
||||
new Color(1f, 1f, 1f, a),
|
||||
new Color(1f, 1f, 1f, a * 0.12f),
|
||||
new Color(1f, 1f, 1f, a * 0.12f),
|
||||
};
|
||||
var uvs = new Vector2[4] { new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f), new Vector2(0.5f, 0.5f) };
|
||||
var tris = new int[6] { 0, 2, 1, 1, 2, 3 };
|
||||
mesh.Clear();
|
||||
mesh.vertices = verts; mesh.colors = cols; mesh.uv = uvs; mesh.triangles = tris;
|
||||
mesh.RecalculateBounds();
|
||||
}
|
||||
|
||||
static void EmitAt(ParticleSystem ps, Vector3 pos, int count)
|
||||
{
|
||||
if (ps == null) return;
|
||||
|
||||
@@ -34,6 +34,22 @@ namespace ProjectM.Client
|
||||
/// <summary>Milliseconds the FOV kick eases back to base.</summary>
|
||||
public static float HitStopDurationMs;
|
||||
|
||||
// ---- MC-3: player-dealt-hit punch (magnitude-scaled) + deferred enemy flash ----
|
||||
/// <summary>Min FOV kick (deg) when the LOCAL player lands a hit on an enemy (chip damage).</summary>
|
||||
public static float HitStopFovKickMin;
|
||||
/// <summary>Max FOV kick (deg) on a heavy player-dealt hit (delta >= HitStopRefDamage).</summary>
|
||||
public static float HitStopFovKickMax;
|
||||
/// <summary>Reserved cap for the (deferred) true freeze-frame, in fixed frames.</summary>
|
||||
public static int HitStopMaxFrames;
|
||||
/// <summary>Damage delta that saturates the player-dealt punch to HitStopFovKickMax.</summary>
|
||||
public static float HitStopRefDamage;
|
||||
/// <summary>Tint for the (deferred) enemy material hit-flash — exposed now, wired in the ShaderGraph slice.</summary>
|
||||
public static Color HitFlashColor;
|
||||
/// <summary>Duration (ms) of the deferred enemy hit-flash.</summary>
|
||||
public static float HitFlashDurationMs;
|
||||
/// <summary>Master gate for the (deferred) true freeze-frame hit-stop. FALSE for v1 (camera-punch only).</summary>
|
||||
public static bool HitStopFreezeEnabled;
|
||||
|
||||
// ---- Feature 1/2: death camera punch ----
|
||||
/// <summary>Camera shake on LOCAL player death (loudest event by design).</summary>
|
||||
public static float PlayerDeathShake;
|
||||
@@ -103,6 +119,13 @@ namespace ProjectM.Client
|
||||
HitSfxVolume = 0.70f;
|
||||
HitStopFovKick = 1.5f;
|
||||
HitStopDurationMs = 90f;
|
||||
HitStopFovKickMin = 0.6f;
|
||||
HitStopFovKickMax = 2.2f;
|
||||
HitStopMaxFrames = 3;
|
||||
HitStopRefDamage = 30f;
|
||||
HitFlashColor = new Color(1f, 0.85f, 0.55f, 1f);
|
||||
HitFlashDurationMs = 80f;
|
||||
HitStopFreezeEnabled = false;
|
||||
|
||||
// Feature 1/2 death
|
||||
PlayerDeathShake = 0.50f;
|
||||
|
||||
Reference in New Issue
Block a user