Hygiene B4c: clean dedup helpers (knockback, bakers, editor rig)

- KnockbackUtil.Stamp: shared knockback-stamp used by AbilityFireSystem + MeleeComboSystem (guard + planar normalize + write; speed passed in).
- BakerStructureExt: AddPlacedStructure/AddDamageable extensions on Baker<T>; rewired Turret/Structure/Harvester/Conveyor/Fabricator bakers (baked data + serialized fields unchanged).
- EnemyPrefabBakeExt.AddEnemyPrefabPool: shared prefab-buffer bake for ZoneEnemyDirector/WaveDirector authoring (fields stay flat — no serialization change).
- Editor AnimRigUtil (HasParam + root-yaw overlay clip) shared by PlayerRigTools/EnemyRigTools; EnemyRigTools.MakeMat now GUID-preserving (CopySerialized over existing) — the composite AnimatorController copy left as-is with a caveat comment.

466/466 EditMode tests pass, 0 warnings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-08 21:26:23 -07:00
parent e27a495530
commit 21a4c71413
19 changed files with 189 additions and 121 deletions
@@ -143,17 +143,9 @@ namespace ProjectM.Simulation
});
// C3: the cone reads as weak vs the melee cleave without knockback — stamp it like melee
// (guarded: dummies lack KnockbackState → ECB throw; the boss is knockback-immune, A4).
if (m_KnockbackLookup.HasComponent(coneTargets[ci]) && !m_BossLookup.HasComponent(coneTargets[ci]))
{
float3 kd3 = coneTargetPos[ci] - xform.ValueRO.Position;
float2 kdir = math.lengthsq(kd3.xz) > 1e-6f ? math.normalize(kd3.xz) : cFace;
m_KnockbackLookup[coneTargets[ci]] = new KnockbackState
{
Dir = kdir,
Speed = Tuning.KnockbackSpeed,
UntilTick = TickUtil.NonZero(serverTick.TickIndexForValidTick + (uint)math.max(1, Tuning.KnockbackDurationTicks)),
};
}
KnockbackUtil.Stamp(ref m_KnockbackLookup, m_BossLookup, coneTargets[ci],
xform.ValueRO.Position, coneTargetPos[ci], cFace, Tuning.KnockbackSpeed,
TickUtil.NonZero(serverTick.TickIndexForValidTick + (uint)math.max(1, Tuning.KnockbackDurationTicks)));
}
}
uint coneCd = (uint)math.max(1, eff.ValueRO.CooldownTicks);
@@ -0,0 +1,28 @@
using Unity.Entities;
using Unity.Mathematics;
namespace ProjectM.Simulation
{
/// <summary>
/// Shared knockback stamp for melee/cone hits. Guarded exactly as the two call sites were: the target must own
/// <see cref="KnockbackState"/> (dummies/structures lacking it would throw at ECB playback if written) and must
/// NOT be a boss (<see cref="BossState"/> = knockback-immune, A4). The planar (XZ) heading is
/// normalize(targetPos - sourcePos), falling back to <paramref name="faceFallback"/> when that delta is
/// degenerate. Deduplicates the identical stamps in <see cref="AbilityFireSystem"/> (Warrior cone) and
/// <see cref="MeleeComboSystem"/> (melee cleave). Callers still gate their own speed/window (e.g. the melee
/// KnockSpeed &gt; 0 check) before calling.
/// </summary>
static class KnockbackUtil
{
public static void Stamp(ref ComponentLookup<KnockbackState> lookup, in ComponentLookup<BossState> bossLookup,
Entity target, float3 sourcePos, float3 targetPos, float2 faceFallback, float speed, uint untilTick)
{
if (!lookup.HasComponent(target) || bossLookup.HasComponent(target))
return;
float3 delta = targetPos - sourcePos;
float2 dir = math.lengthsq(delta.xz) > 1e-6f ? math.normalize(delta.xz) : faceFallback;
lookup[target] = new KnockbackState { Dir = dir, Speed = speed, UntilTick = untilTick };
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: fa42de08cd15e1841853823f81f94fa3
@@ -248,13 +248,9 @@ namespace ProjectM.Simulation
SourceNetworkId = c.OwnerId,
SourceTick = c.Stamp,
});
if (c.KnockSpeed > 0f && m_KnockbackLookup.HasComponent(target) && !m_BossLookup.HasComponent(target))
{
float3 d3 = enemyPositions[i] - c.From;
float2 kdir = new float2(d3.x, d3.z);
kdir = math.lengthsq(kdir) > 1e-6f ? math.normalize(kdir) : c.Face;
m_KnockbackLookup[target] = new KnockbackState { Dir = kdir, Speed = c.KnockSpeed, UntilTick = c.KnockUntil };
}
if (c.KnockSpeed > 0f)
KnockbackUtil.Stamp(ref m_KnockbackLookup, m_BossLookup, target,
c.From, enemyPositions[i], c.Face, c.KnockSpeed, c.KnockUntil);
}
}
// HARVEST: deplete every node/clutter in each swing's cone, crediting the shared ledger; write