Docs: DR-023 enemy animation + Synty asset inventory; trim CLAUDE.md

DR-023 decision record (client-derived enemy animation, monster-mash roster,
EnemyRigTools, WaveSystem scale-fix, GUID-preserving rebuild) + session log +
Synty_Asset_Inventory (enemy-grade table + future-dev catalog for the 14 new
packs). CLAUDE.md: add the enemy-animation gotchas bullet and condense several
build-gotcha bullets back below pre-session size.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-06 23:30:26 -07:00
parent f4c861ee91
commit 0df0b45163
4 changed files with 163 additions and 14 deletions
@@ -0,0 +1,45 @@
---
date: 2026-06-06
type: session
tags: [session, animation, rukhanka, synty, enemies, netcode, presentation, dots, slice, inventory]
---
# Session 2026-06-06 — Enemy animation (Rukhanka, client-derived), Slice 2 + Synty inventory
## Goal
Operator (via `/dots-dev`, ultracode): *"Extend the animation system to encompass enemies. I've added a ton more Synty assets — inventory them and pick the enemies to use. Note other assets for future goals and development."*
## Process
- **Research/inventory Workflow (≈22 read-only agents):** one agent per Synty character pack + batched env/FX/UI packs (→ structured inventory), 3 agents mapping the enemy + animation code surface, 2 context7/source agents confirming the Rukhanka enemy extension, 1 synthesis. Key finding: a Husk = an ownerless interpolated ghost = structurally a **remote player**, which DR-022's `RemoteDriveJob` already animates from `LocalTransform` deltas → the enemy drive is that path generalized; **no new netcode surface**.
- **Clarifying gate (AskUserQuestion):** roster = **monster-mash** (Werewolf=Grunt, Werewolf-Undead=Swarmer, Kaiju=Brute); scope = **locomotion + attack telegraph** (death-anim deferred).
- **Plan → approval → execution**, serialized through the one live editor (research fanned out in parallel; all Unity mutations done inline, validating each).
## Done
- **Inventory + picks** → [[Synty_Asset_Inventory]] (enemy-grade table + future-dev catalog: env/FX/UI/build). All enemy picks confirmed Generic + Optimize-OFF (drop-in for the DR-022 recipe).
- **Reusable `EnemyRigTools` editor tool** (`ProjectM/Animation` menu) — 3 idempotent steps building the materials, `AC_EnemyTopDown` + `EnemyAttackWindup.anim`, and the 3 rigged prefabs via real `PrefabUtility` C# (mirrors the DR-022 player recipe; `RigDefinitionAuthoring` via reflection). **GUID-preserving** in-place rebuild so re-runs never orphan the subscene refs.
- **`EnemyAnimationDriveSystem`** (client-only, mirrors the remote-player path): `[WithAll(EnemyTag)]`, velocity from `LocalTransform` delta (prevPos cache + per-frame prune, NOT `[RequireMatchingQueriesForUpdate]` so the prune always runs — Husks die often), facing from `LocalTransform.Rotation`, maxSpeed from baked `EnemyStats`, `IsAttacking = AttackWindup != 0`. Reuses `AnimParamMath.LocomotionParams`; added `AnimParamMath.PlanarForward` (+4 EditMode tests). No new `[GhostField]`, no server change, no asmdef change.
- **3 animated prefabs**: `EnemyWerewolf` (Grunt 0.8), `EnemyWerewolfUndead` (Swarmer 0.6, green-tinted undead skin), `EnemyKaiju` (Brute 1.3). Wired into the gameplay subscene `WaveDirector.EnemyPrefabs[]`. Capsule prefabs kept as pristine ghost templates.
- **`WaveSystem` Scale-clobber fix**: spawner used `LocalTransform.FromPosition` (resets Scale→1, a replicated `[GhostField]`) → now `baked.WithPosition(pos)` preserves the variant scale.
## Validation (runtime, focused editor, real Server+Client)
- EditMode **204 → 208** (+4 PlanarForward). Clean compile; console clear; **no "does not support skinning"**.
- Spawned one of each variant (debug-RPC `SpawnWave`, then direct server instantiate from the prefab pool). All 3 replicate to the client with the **Rukhanka rig + 5-param buffer** and **correct baked scales** (0.80/0.60/1.30 — scale-fix works through replication).
- **Drive proven via live param sampling:** moving Husk `MoveZ≈Speed≈0.95` (forward run); stopped-at-player `Speed=0`; winding-up `IsAttacking=true` — locomotion + telegraph both correct.
- **Feet-on-ground (`WorldRenderBounds`):** werewolves feetY ≈ 0.05/0.04 ✓; Kaiju measured 0.32 sunk → Root-Y 0.77 → **0.52** (GUID-preserving re-run kept the subscene refs). Kaiju eyeball on next Play.
## Gotchas captured
- `WaveSystem.FromPosition` Scale-clobber (above). `DeleteAsset+CopyAsset` mints new GUIDs → orphans subscene refs (→ GUID-preserving in-place rebuild). `execute_code`: no `using`/aliases; `GetComponentData`/`GetBuffer` ambiguous via reflection (use concrete generics); `Rukhanka.ParameterValue` is a union (read `floatValue`/`boolValue`). → folded into [[DR-023_Enemy_Animation_MonsterMash]] + CLAUDE.md.
## Next-session intent
- Eyeball the Kaiju feet/scale in a natural Play; per-SMR eye-glow material.
- **Death animation** (deliberate netcode change: `Dead` enableable from replicated Health + delayed despawn) so a death clip plays before despawn.
- **Cyberpunk enemy faction** (PolygonSciFiCity) as new wave variants on this exact pipeline — one material + `EnemyRigTools` variant rows.
- Wire **PolygonParticleFX** (blood/gore/explosion) into `VFXConfig` for richer Husk-death VFX.
See [[DR-023_Enemy_Animation_MonsterMash]].
@@ -0,0 +1,56 @@
---
id: DR-023
title: Enemy skeletal animation (Rukhanka, client-derived) — Slice 2, monster-mash roster + EnemyRigTools
status: accepted
date: 2026-06-06
tags:
- decision
- animation
- rukhanka
- synty
- netcode
- enemies
- presentation
- dots
permalink: gamevault/07-sessions/decisions/dr-023-enemy-animation-monster-mash
---
# DR-023 — Enemy skeletal animation (Rukhanka + Synty), client-derived — Slice 2
## Context
[[DR-022_Animation_Pipeline_Rukhanka_Synty]] brought the **player** alive (Synty SciFiSpace soldier, Rukhanka 2.9, client-derived, netcode-OFF). Its roadmap named enemies next. Operator (via `/dots-dev`, ultracode): *"extend the animation system to encompass enemies. I've added a ton more Synty assets — inventory them and pick the enemies to use. Note other assets for future goals."*
Husks were primitive **capsules** (`Enemy`/`EnemySwarmer`/`EnemyBrute` prefabs = built-in capsule mesh, no skeleton). A Husk is an **ownerless interpolated ghost** (server-moved by `EnemyAISystem`, position+rotation via stock `LocalTransform` replication, no `KinematicCharacterBody` on the client) — i.e. structurally identical to a **remote player**, which DR-022's `PlayerAnimationDriveSystem.RemoteDriveJob` already animates from `LocalTransform.Position` deltas. So enemy animation is the remote-player path generalized — no new netcode surface.
A read-only research **Workflow** (≈22 agents) inventoried the new Synty packs, mapped the enemy + animation code surface, and confirmed the Rukhanka extension. Clarifying answers (AskUserQuestion): roster = **monster-mash**, scope = **locomotion + attack telegraph** (no death-anim this slice). Full inventory: [[Synty_Asset_Inventory]].
## Decision
1. **Roster (monster-mash), all on the unified Synty Polygon Generic skeleton (drop-in for the DR-022 recipe):**
- **Grunt** = PolygonWerewolf `SM_Werewolf_01` (scale 0.8).
- **Swarmer** = the same werewolf, **undead skin** (a green-tinted `_BaseColor` over the werewolf atlas), scale 0.6.
- **Brute** = PolygonKaiju `SM_Chr_Kaiju_01` (scale 1.3). Kaiju share the Generic CORE skeleton; their extra bones (Jaw/Eyes/Belly) just idle at bind pose; the tail is a separate attachment, omitted.
2. **Client-derived, netcode replication OFF — same doctrine as DR-022.** A new client-only `EnemyAnimationDriveSystem` (`[WorldSystemFilter(LocalSimulation|ClientSimulation)]`, `[UpdateBefore(RukhankaAnimationSystemGroup)]`, `SystemBase`) runs one Bursted `IJobEntity` `[WithAll(EnemyTag)]`: velocity from `LocalTransform.Position` frame-delta (per-`Entity` `prevPos` cache + per-frame prune), facing from `LocalTransform.Rotation` (`AnimParamMath.PlanarForward`, the server faces the target), maxSpeed from the baked-on-both-worlds `EnemyStats.MoveSpeed`, `IsAttacking = AttackWindup.WindUpUntilTick != 0`. Reuses `AnimParamMath.LocomotionParams` unchanged. **No new `[GhostField]`s, no `DefaultVariant` strip, no ghost-hash change, no asmdef change** (`ProjectM.Client` already refs Rukhanka + Unity.Physics); the server-side Rukhanka strip ([[DR-022_Animation_Pipeline_Rukhanka_Synty]]'s `ServerStripAnimationSystem`, matched by assembly name) already covers enemy rigs — **zero server change**.
- **Deliberately NOT `[RequireMatchingQueriesForUpdate]`**: Husks despawn far more often than players, so the prune must run every frame (even with zero live Husks) or the cache leaks one entry per kill.
3. **Attack telegraph rides the existing replicated `AttackWindup`** ([GhostField], non-zero for the ~0.3s wind-up, reset to 0 after the strike) → a clean idempotent `IsAttacking` bool, no tick comparison (the same value `CombatFeedbackSystem` already reads). Controller `AC_EnemyTopDown` forks `AC_PlayerTopDown` (identical `MoveX/MoveZ/Speed` 2D-Freeform locomotion tree so the math + job are shared) + an **Attack** state (`AnyState→Attack` on `IsAttacking`, `Attack→Exit` on `!IsAttacking`). The attack clip `EnemyAttackWindup.anim` is authored: a non-looping forward **pitch of the `Root` bone** (whole-body lunge) — pack-agnostic (Root exists on every Synty rig; locomotion clips don't key Root, so no conflict and it returns to its Y-offset on exit).
4. **Death stays as-is (instant despawn + `CombatFeedbackSystem` VFX) — deferred.** A death animation needs the entity to outlive `Health<=0`, i.e. a deliberate server change (a `Dead` enableable derived from replicated Health + a delayed despawn); scheduled separately so this slice keeps the netcode surface unchanged.
5. **A reusable `EnemyRigTools` editor tool** (menu `ProjectM/Animation`, namespace `ProjectM.EditorTools`, like `EnvArtTools`) builds the whole pipeline in three idempotent steps — materials, controller+clip, the 3 rigged prefabs — by mirroring the DR-022 player-rig recipe in real `PrefabUtility` C# (robust vs the enum/Vector-drop MCP prefab traps). `RigDefinitionAuthoring` is added by **reflection** (no Rukhanka asmdef ref — same "by name" tactic as the server strip). This is the "scalable/sustainable for a solo dev" artifact: a future enemy is one variant-table row + a re-run.
## Consequences (validated at runtime, Unity 6.4.7, real Server+Client)
- **EditMode 204 → 208** (+4 `AnimParamMath.PlanarForward` tests). Clean compile, console clear (no NRE, **no "does not support skinning"** → the deformation material skins correctly).
- Spawned one of each variant: all 3 replicate to the client carrying the **Rukhanka rig + param buffer** (5 params = MoveX/MoveZ/Speed + IsDead/IsAttacking), and **the baked variant scales survive** (0.80 / 0.60 / 1.30).
- **Drive proven by sampling the live param buffer:** a moving Husk reads `MoveZ≈Speed≈0.95` (forward run), a Husk stopped at the player reads `Speed=0`, and Husks in wind-up read `IsAttacking=true` — exactly the intended locomotion + telegraph.
- **Feet-on-ground via `WorldRenderBounds`:** werewolf Grunt/Swarmer feetY ≈ 0.05/0.04 (Root-Y 1.25 / 1.67); Kaiju measured 0.32 sunk → Root-Y corrected 0.77 → **0.52** (re-run preserved the prefab GUID, so the subscene refs held).
- **Files.** New: `EnemyAnimationDriveSystem.cs`, `EnemyRigTools.cs` (editor), `AC_EnemyTopDown.controller`, `EnemyAttackWindup.anim`, `M_Enemy_Werewolf_Animated` / `M_Enemy_WerewolfUndead_Animated` / `M_Enemy_Kaiju_Animated.mat`, `EnemyWerewolf` / `EnemyWerewolfUndead` / `EnemyKaiju.prefab`; `AnimParamMath.PlanarForward` (+4 tests). Modified: `WaveSystem.cs` (scale-fix, below), the gameplay subscene `WaveDirector.EnemyPrefabs[]` (→ the 3 new prefabs). Capsule prefabs (`Enemy`/`EnemySwarmer`/`EnemyBrute`) kept as pristine ghost-config templates.
## Findings / gotchas
- **`WaveSystem` Scale-clobber (fixed).** The spawner used `LocalTransform.FromPosition(pos)`, which resets Scale→1 — silently flattening every variant to scale 1 (Scale is a replicated `[GhostField]`). Now reads the prefab's baked `LocalTransform` and `WithPosition(pos)` (preserves Scale + rotation). A bug fix, not a netcode change.
- **GUID-preserving rebuild.** A naive `DeleteAsset + CopyAsset` mints a NEW GUID each run → orphans the subscene's `WaveDirector` refs. `EnemyRigTools.BuildOne` copies the template only when the output is absent, else modifies it **in place** (clear children + rig components, re-add) → stable GUID across re-runs. (The Materials/Controller steps still recreate assets — re-running those means re-pointing their consumers.)
- **`execute_code` reflection traps:** runs as a method body (no `using`/aliases; fully-qualify), and `EntityManager.GetComponentData`/`GetBuffer` are ambiguous via plain `GetMethod` (overloads) — use the concrete generic directly (it can reference project + Rukhanka types). The `Rukhanka.ParameterValue` value is a union — read `floatValue`/`boolValue`.
- **Kaiju Root-Y (0.52)** is a calculated correction from the measured 0.32 sink (werewolves were visually validated on-ground); eyeball on the next natural Play. Kaiju at scale 1.3 reads ~2.8 m tall — a proper Brute.
- Deferred: death anim (R3, needs the server `Dead`/delayed-despawn change); per-SMR eye-glow material (slice 1 sets every SMR slot to the one body deformation material); a second-faction roster (PolygonSciFiCity cyber enemies) is the natural next add on this exact pipeline — see [[Synty_Asset_Inventory]].
Builds on [[DR-022_Animation_Pipeline_Rukhanka_Synty]] (the recipe + server strip), [[DR-016_Stage_G_Combat_Gameplay]] (Husk/AttackWindup), [[DR-017_Persistent_Base_Player_Driven_Pacing]] (the player-driven Siege loop these enemies populate). Serves the "feel alive" goal.