From 0df0b451634804fda3487e3ce037ef8a953558a8 Mon Sep 17 00:00:00 2001 From: Luis Gonzalez Date: Sat, 6 Jun 2026 23:30:26 -0700 Subject: [PATCH] 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 --- CLAUDE.md | 25 ++++----- .../Vault/06_Roadmap/Synty_Asset_Inventory.md | 51 +++++++++++++++++ ...6-06-06_Enemy_Animation_Synty_Inventory.md | 45 +++++++++++++++ .../DR-023_Enemy_Animation_MonsterMash.md | 56 +++++++++++++++++++ 4 files changed, 163 insertions(+), 14 deletions(-) create mode 100644 Docs/Vault/06_Roadmap/Synty_Asset_Inventory.md create mode 100644 Docs/Vault/07_Sessions/2026/2026-06-06_Enemy_Animation_Synty_Inventory.md create mode 100644 Docs/Vault/07_Sessions/_Decisions/DR-023_Enemy_Animation_MonsterMash.md diff --git a/CLAUDE.md b/CLAUDE.md index b1c975bb3..251bfea4b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,7 +19,7 @@ Multiplayer game on **Unity DOTS (Entities) + Netcode for Entities** — server- | `com.unity.mathematics` | 1.3.3 | (transitive) | | `com.rukhanka.animation` | **2.9.0** | Local pkg (`Packages/com.rukhanka.animation`). ECS skeletal animation (Burst CPU/GPU skinning). Declares entities.graphics 1.4.16 → resolves on 6.4.0 via SemVer floor. Netcode replication **OFF** → client-derived. See [[DR-022_Animation_Pipeline_Rukhanka_Synty]]. | -Values match `packages-lock.json` (reconciled 2026-06-02; URP 17.4.0, test-framework 1.6.0, ugui 2.0.0, multiplayer.center 1.0.1). **History:** briefly tried **6.6.0a6** (renumbers Netcode→6.6.0/Physics→6.5.0/Entities→6.5.0) but its Netcode/Transport runtime is a **confirmed engine bug** ("invalid wrapped network interface") → reverted. If returning to 6.6, expect the renumber + re-test runtime. See [[DR-002_Unity66_Alpha_Netcode_Transport]] and `Docs/Vault/_Meta/CLAUDE_Build_Gotchas_Archive.md`. +Values match `packages-lock.json` (reconciled 2026-06-02; URP 17.4.0, test-framework 1.6.0, ugui 2.0.0, multiplayer.center 1.0.1). **History:** 6.6.0a6 was tried + reverted (Netcode/Transport runtime engine bug "invalid wrapped network interface"); returning to 6.6 means a package renumber + runtime re-test. See [[DR-002_Unity66_Alpha_Netcode_Transport]] + the gotchas archive. ## Namespaces & assembly split @@ -67,14 +67,14 @@ Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Bui - **Cooldown/spawn "next tick" sentinels:** route every stored tick through **`TickUtil.NonZero(...)`** (a computed `ServerTick+delay` can wrap to 0, the "ready" sentinel) and compare with `NetworkTick.IsNewerThan` / `.TicksSince`, **never** raw `uint <` / subtraction. - **`GhostRelevancy` for region splits:** use `GhostRelevancyMode.SetIsIrrelevant` (not `SetIsRelevant`) so untagged/global ghosts stay relevant for free — only enumerate cross-region ghosts to hide. `RegionTag{byte Region}` is **server-only, NOT a `[GhostField]`** (server decides relevancy; client just gains/loses ghosts). `RelevantGhostForConnection{int Connection (=NetworkId.Value); int Ghost (=GhostInstance.ghostId)}`. - **Shared GLOBAL state (cycle phase, resource ledger, goal meter) rides an UNTAGGED ghost**, never a region-tagged one (`SetIsIrrelevant` would hide it cross-region). Resolve a ledger buffer via a DISTINCT tag (`ResourceLedger`), **never `GetSingleton`** when a second `StorageEntry` buffer exists elsewhere → "multiple instances" throw. -- **Frontend world lifecycle (menu → on-demand worlds) ★:** `CreateLocalWorld` is `internal` in 1.13.2 — use the public `CreateClientWorld` / `CreateServerWorld` (they register the `ServerWorld` / `ClientWorld` statics the connection UI/overlay read); for the menu world use `DefaultWorldInitialization.Initialize(name, false)` (or `return false` from `GameBootstrap.Initialize`). **Never dispose/create worlds inside an ECS system** — run all create/dispose/scene-load on a frame-boundary coroutine (`SessionRunner`, `DontDestroyOnLoad`). The gameplay subscene streams into an on-demand world ONLY if a netcode world is the `DefaultGameObjectInjectionWorld` at `LoadScene` time (dispose the menu world first → set the default to the server world → `LoadScene(Game)`). See [[DR-019_Frontend_Menu_Settings_Saves_Build]]. +- **Frontend world lifecycle (menu → on-demand worlds) ★:** `CreateLocalWorld` is `internal` in 1.13.2 — use public `CreateClientWorld`/`CreateServerWorld` (they register the `ServerWorld`/`ClientWorld` statics the UI/overlay read); for the menu world use `DefaultWorldInitialization.Initialize(name, false)` (or `return false` from `GameBootstrap.Initialize`). **Never dispose/create worlds inside an ECS system** — do all create/dispose/scene-load on a frame-boundary coroutine (`SessionRunner`, `DontDestroyOnLoad`). The gameplay subscene streams into an on-demand world ONLY if a netcode world is the `DefaultGameObjectInjectionWorld` at `LoadScene` time (dispose the menu world → set the default to the server world → `LoadScene(Game)`). See [[DR-019_Frontend_Menu_Settings_Saves_Build]]. ### Physics & character controller - **Unity Physics 1.x bakes built-in `UnityEngine` colliders + `Rigidbody`** (the Physics-0.x `PhysicsShapeAuthoring`/`PhysicsBodyAuthoring` are gone). Static collider (no Rigidbody) → baked into the subscene PhysicsWorld, deterministic, no replication. `Rigidbody.FreezeRotation` is **NOT** honored by the baker — zero angular velocity + write rotation each tick, or set `PhysicsMass.InverseInertia = float3.zero`. - **The player is a Unity Character Controller kinematic character** (NOT a dynamic Rigidbody — `PlayerMoveSystem`/`PlayerPlanarConstraintSystem` were deleted; the DR-006 predicted-physics infra is kept). `PlayerControlSystem` maps input → `CharacterControl`; `CharacterProcessor` collide-and-slides in the relocated `KinematicCharacterPhysicsUpdateGroup`. CC 1.4.2 API = `IKinematicCharacterProcessor` + `KinematicCharacterDataAccess` + static `KinematicCharacterUtilities.Update_*` (verify shape with `unity_reflect`, don't assume the legacy aspect). - **`KinematicCharacterUtilities.BakeCharacter` aborts if the GameObject has a `Rigidbody`** and needs uniform (1,1,1) scale. **`CharacterInterpolation` must be PredictedClient-only** (register a `DefaultVariantSystemBase` stripping it from server + interpolated prefabs) — else double-interp on remotes. **Do NOT copy the CC sample's global `LocalTransform → DontSerializeVariant`** (project-wide; breaks the non-character ghosts that rely on stock `LocalTransform` replication). - **Top-down CC config:** `SnapToGround=false`, `InterpolateRotation=false` (rotation owned by `PlayerAimSystem`), `SimulateDynamicBody=false`; gravity handled by feeding `float3.zero` to `Update_GroundPushing`. -- **Hit/area tests must be SWEPT, not point checks** — a point distance check tunnels through a target when the per-tick step exceeds the target radius (high speed *or* tick-batching). Test the segment traversed this tick. **In a PLAIN `SimulationSystemGroup` system do NOT use `SystemAPI.Time.DeltaTime`** (it's the wall-frame delta, not the fixed step) — store the per-tick step on the projectile (`Projectile.LastStep`, written in the fixed-step group) and rebuild the segment as `cur - dir*LastStep`. A node hit by N projectiles in one tick: `ecb.DestroyEntity` **at-most-once** (destroyed-bitset; a double destroy throws at Playback). **When TWO target types share one projectile pass (resource nodes + Blight clutter), UNIFY the sweep into one best-target loop + one shared destroyed-bitset** — separate sweep systems each `DestroyEntity` a projectile that overlaps both → double-destroy at Playback (DR-018). **A float per-hit yield cast `(int)` that ALSO gates despawn is an immortal-sink footgun:** a sub-1.0 value → `(int)`=0 → no deposit AND no `Remaining` decrement, yet the shot is still consumed → an unkillable target that silently eats projectiles. Guard with `math.max(1,(int)yield)` in the consumer **and** `[Min(1f)]` on the authoring (the node path had it; the clutter sibling didn't). +- **Hit/area tests must be SWEPT, not point checks** — a point check tunnels when the per-tick step exceeds the target radius (high speed *or* tick-batching); test the segment traversed this tick. **In a PLAIN `SimulationSystemGroup` system do NOT use `SystemAPI.Time.DeltaTime`** (wall-frame delta, not the fixed step) — store the per-tick step on the projectile (`Projectile.LastStep`, written in the fixed-step group) and rebuild the segment as `cur - dir*LastStep`. A node hit by N projectiles in one tick: `ecb.DestroyEntity` **at-most-once** (destroyed-bitset; double destroy throws at Playback). **TWO target types in one projectile pass (nodes + Blight clutter): UNIFY into one best-target loop + one shared destroyed-bitset** (separate sweeps each destroy a projectile overlapping both → double-destroy, DR-018). **A per-hit yield `(int)` cast that also gates despawn is an immortal-sink** (sub-1.0 → 0 → no deposit, no `Remaining` decrement, shot still consumed): guard `math.max(1,(int)yield)` + `[Min(1f)]` authoring. ### Build / structures / grid - **Build-grid math must be deterministic + integer-stable:** corner-origin, center-returning, **half-open** cell bounds, `math.floor` (not truncation — negatives). Lock `CellSize`/`PlotSize` as a coordinate space once (`BaseGridMath`, EditMode-tested) — changing them invalidates placed structures. @@ -82,14 +82,14 @@ Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Bui - **Co-op placement atomicity:** commit the `StorageMath.Withdraw` + cell-reservation **in-place inside the RPC foreach** (only `Instantiate` goes through the ECB) so two same-tick requests for one cell can't both pass. - **Buildable turret = hitscan = reversed `EnemyAISystem`:** nearest living Husk in-region within Range, on `NextTick` cooldown append a direct `DamageEvent{Damage, SourceNetworkId=-1}` → reuses `HealthApplyDamageSystem`. No projectile → no tunnelling, no team model. - **Resource-gated ability tiers reuse `StatModifier`** — grow ONE `StatModifier{Target=Damage, Op=PercentAdd, SourceId=}` (replace-by-SourceId so the buffer stays bounded); `StatRecomputeSystem` folds it into `EffectiveAbilityStats` on both worlds. `GoalProgress{[GhostField] int Charge, Target}` lives on the global CycleDirector ghost. **Disk persistence shipped** — see the Automation + persistence bullets below. -- **M7 Automation (server-only, never predicted) ★:** `Harvester` / `Conveyor` / `Fabricator` are buildable machines on the same `PlacedStructure` ghost; each stores `PeriodTicks` + **server-only** `MachineInput` / `MachineOutput` buffers (NOT `[GhostField]`). Production runs in the plain server `SimulationSystemGroup` `[UpdateAfter(PredictedSimulationSystemGroup)]` (Harvester→Conveyor→Fabricator) and replicates only via the global ledger + `PlacedStructure`. Deterministic catch-up via `ProductionMath.CyclesDue` (**lower-bound 0, never 1** — a `1` premature-mints a restored `remaining==0` machine; period-0 guarded). Byte-only pure math (`ProductionMath` / `ConveyorMath.ResolveMoves` / `MachineSlotMath`) is EditMode-tested; `ConveyorMath` is order-independent (snapshot → stable-sort by `CellKey` → at-most-one destination claim → losers stall, no loss). `RuntimePlacedTag` marks player-built machines for the save-scan; `BuildPlaceSystem` stamps `LastProcessedTick=0` so runtime-placed machines hit `NeedsInit`. See [[DR-020_M7_Automation_Production_Chains]]. +- **M7 Automation (server-only, never predicted) ★:** `Harvester` / `Conveyor` / `Fabricator` are buildable machines on the `PlacedStructure` ghost; each stores `PeriodTicks` + **server-only** `MachineInput` / `MachineOutput` buffers (NOT `[GhostField]`). Production runs in the plain server `SimulationSystemGroup` `[UpdateAfter(PredictedSimulationSystemGroup)]` (Harvester→Conveyor→Fabricator), replicating only via the global ledger + `PlacedStructure`. Deterministic catch-up via `ProductionMath.CyclesDue` (**lower-bound 0, never 1** — a `1` premature-mints a restored `remaining==0` machine; period-0 guarded). Byte-only pure math (`ProductionMath` / `ConveyorMath.ResolveMoves` / `MachineSlotMath`) is EditMode-tested; `ConveyorMath` is order-independent (snapshot → stable-sort by `CellKey` → at-most-one destination claim → losers stall). `RuntimePlacedTag` marks player-built machines for the save-scan; `BuildPlaceSystem` stamps `LastProcessedTick=0` → runtime machines hit `NeedsInit`. See [[DR-020_M7_Automation_Production_Chains]]. - **Disk persistence (`SaveData`, single-slot atomic JSON at `persistentDataPath`) ★:** versioned, null on bad version, schema **additive** (bump the version, don't break it). **Born-correct load** — `CycleDirectorSpawnSystem` applies a staged `PendingSave` AT SPAWN so the director ghost never replicates a default first. Autosave on the Siege→Calm checkpoint + on quit-to-menu (`WorldLauncher.TrySaveFromServer`, host-only); `BaseRestoreSystem` replays saved structures **charge-free** with epoch-independent REMAINING-tick cooldowns + re-tags them. Shared `SaveStructureScan.Collect` (autosave + quit use ONE scan path). See [[DR-019_Frontend_Menu_Settings_Saves_Build]]. ### Presentation / juice / VFX - **All juice/HUD = client-only managed `SystemBase` in `PresentationSystemGroup`** (once/frame, no rollback double-fire) that OBSERVES replicated state, never mutates the sim. Read ECS via `SystemAPI.Query` in `OnUpdate` + `EntityManager.CompleteDependencyBeforeRO()` — NOT a MonoBehaviour `LateUpdate` (job-safety throw). `Entity` is a stable client dict key for a ghost's lifetime — **prune the cache each frame** (a pruned enemy = a kill → death VFX); **never `DestroyEntity` a ghost from the client** (`GhostDespawnSystem` owns despawn). Hit-stop = a camera punch, **never `Time.timeScale`** (corrupts the deterministic sim). - **Asset-free presentation:** procedural `AudioClip.Create` SFX; runtime `ParticleSystem` pool (Sprites/Default + HDR start color); code-built **UI Toolkit** HUD / menus (runtime `UIDocument` + shared `RuntimePanelSettings`; see the UITK bullet below). Edit a prefab asset's component in code via `PrefabUtility.LoadPrefabContents` → modify → **`SaveAsPrefabAsset(root, path)`** → `UnloadPrefabContents`. Watch **shared-material bleed** when re-tinting. ACES tonemapping needs URP color grading mode = HDR (`m_ColorGradingMode=1`). - **Prototype glue lives in `ProjectM.Client` as MonoBehaviours:** `PrototypeCameraRig` (player-following ARPG cam), `VFXConfig` (static `Instance` + prefab fields bridging authored VFX to the managed `CombatFeedbackSystem`; keep a procedural fallback). A **static presentation bridge must reset on play-enter** via `[RuntimeInitializeOnLoadMethod(SubsystemRegistration)]` (statics survive fast-enter-playmode reloads → stale flash). -- **UITK HUD + menus (in-game UI, on UI Toolkit) ★:** `MenuUi` owns the shared palette + element factories + `PanelSettings` / `EventSystem` plumbing and the **canonical `Round`/`Border` helpers**; `HudUi` is a thin extension (bars / labels). `HudSystem` is a `PresentationSystemGroup` observe-only `SystemBase` owning a runtime `UIDocument` (`sortingOrder 50`, behind the pause overlay's 100); it builds the tree on the first frame `rootVisualElement != null`, root `pickingMode = Ignore` so the HUD never eats world clicks (only palette buttons opt back in). **Runtime UITK needs a `PanelSettings` WITH a `themeStyleSheet`** (a `.tss` importing `unity-theme://default`) **AND** an `EventSystem` + `InputSystemUIInputModule` (Input System project) or buttons are silently dead. The **build palette** (lazy-built from the client `StructureCatalog`) drives click-to-place: ground-ghost preview (green/red via `BuildPreviewMath`, the client mirror of the server legality check), left-click places via the `BuildPlaceRequest` RPC, right-click/Esc cancels, `[`/`]`/R rotates a conveyor; `Fire` is suppressed while build mode is active. See [[DR-021_HUD_UITK_BuildPalette]]. +- **UITK HUD + menus ★:** `MenuUi` owns the shared palette + element factories + `PanelSettings`/`EventSystem` plumbing + the canonical `Round`/`Border` helpers; `HudUi` is a thin extension (bars/labels). `HudSystem` is a `PresentationSystemGroup` observe-only `SystemBase` owning a runtime `UIDocument` (`sortingOrder 50`, behind the pause overlay's 100); builds the tree on the first frame `rootVisualElement != null`, root `pickingMode = Ignore` so the HUD never eats world clicks (only palette buttons opt back in). **Runtime UITK needs a `PanelSettings` WITH a `themeStyleSheet`** (a `.tss` importing `unity-theme://default`) **AND** an `EventSystem` + `InputSystemUIInputModule` or buttons are silently dead. The **build palette** (lazy-built from the client `StructureCatalog`) drives click-to-place: ground-ghost preview (green/red via `BuildPreviewMath`, the client mirror of the server check), left-click → `BuildPlaceRequest` RPC, right-click/Esc cancels, `[`/`]`/R rotates; `Fire` suppressed in build mode. See [[DR-021_HUD_UITK_BuildPalette]]. ### Art import (HDRP store packs → URP) - BefourStudios art is **HDRP-authored** → magenta under URP 17.4 + Entities Graphics. **Convert, don't switch pipelines** (HDRP breaks Entities Graphics). Re-author to stock URP/Lit via `Assets/_Project/Scripts/Editor/EnvArtTools.cs` (menu `ProjectM/Art/1. Convert Curated Env Materials`). Synty art is **URP-native — no conversion**. @@ -106,7 +106,7 @@ Long-form originals + the milestone each came from: `Docs/Vault/_Meta/CLAUDE_Bui - **Cursor/reticle = client `PresentationSystemGroup` `SystemBase` (`AimReticleSystem`) that OBSERVES.** Re-raycast the KBM ground point INSIDE that system (PresentationSystemGroup runs after the follow-cam's LateUpdate) — latching from the gather drifts a frame behind. Hardware cursor hidden while aiming + focused, restored on focus-loss/`OnDestroy`. ### Animation (Rukhanka) ★ -Full rationale: [[DR-022_Animation_Pipeline_Rukhanka_Synty]]. Skeletal animation = **Rukhanka 2.9** (Entities-native; the only maintained option on the 6.4 stack — Latios/Kinemation isn't 6.4-compatible, Unity's official ECS-animation is vaporware). **Netcode replication OFF** (`RUKHANKA_WITH_NETCODE` undefined) → animation is **client-derived**: a client-only `SystemBase` (`PlayerAnimationDriveSystem`, `[WorldSystemFilter(LocalSimulation|ClientSimulation)]` + `[UpdateBefore(RukhankaAnimationSystemGroup)]`) reads replicated state and writes params via `AnimatorParametersAspect`/`FastAnimatorParameter`. No new `[GhostField]`s; no `DefaultVariant` strip (with the define off, no Rukhanka component is a ghost component → ghost hash/snapshot unchanged). +Full rationale: [[DR-022_Animation_Pipeline_Rukhanka_Synty]]. Skeletal animation = **Rukhanka 2.9** (Entities-native; the only maintained option on 6.4 — Latios/Kinemation not 6.4-compatible, Unity's official ECS-animation is vaporware). **Netcode replication OFF** (`RUKHANKA_WITH_NETCODE` undefined) → **client-derived**: a client-only `SystemBase` (`PlayerAnimationDriveSystem`, `[WorldSystemFilter(LocalSimulation|ClientSimulation)]` + `[UpdateBefore(RukhankaAnimationSystemGroup)]`) reads replicated state and writes params via `AnimatorParametersAspect`/`FastAnimatorParameter`. No new `[GhostField]`s; no `DefaultVariant` strip (define off → no Rukhanka component is a ghost component → ghost hash unchanged). - **The rig must bake on the SAME entity that holds the gameplay components the drive job reads.** Rukhanka puts the param buffer + index-table on the GO with `RigDefinitionAuthoring`, so put `Animator` + `RigDefinitionAuthoring` on the **player root** (not a child) and flatten the skeleton + SMRs under it — else the single-entity drive query matches nothing. - **CPU engine still skins via Entities-Graphics GPU deformation → needs a deformation-aware material** (`AnimatedLitShader`, a multi-target ShaderGraph that includes a `UniversalTarget`; Synty atlas → its `_BaseColorMap`). Stock URP/Lit renders **unskinned static** + a `"does not support skinning"` warning — NOT magenta (magenta = reusing an HDRP sample `.mat`). - **Importing the Rukhanka "Animation Samples"** (the only source of `AnimatedLitShader`) drags in 26 sample **subscenes** (one NRE's Rukhanka's **unguarded clip baker** — `AnimationClipBaker.ReadCurvesFromTransform` reads a null bone Transform on a missing-bone clip), sample **systems that run in your worlds**, and a conflicting **TextMesh Pro** folder. Fix: `MoveAsset` the 3 deformation ShaderGraphs to `_Project/Shaders/` (GUID-preserving → material ref intact), then delete the samples tree. @@ -115,6 +115,7 @@ Full rationale: [[DR-022_Animation_Pipeline_Rukhanka_Synty]]. Skeletal animation - **Build the controller via the `AnimatorController` API** (`manage_animation` silently drops enum/Vector blend-tree fields). **Skeleton-root detection = walk up from a bone to the soldier's direct child**, NOT `SkinnedMeshRenderer.rootBone` (that's the *bounds* root — the head SMR's is `Spine_03`; using it destroys the lower skeleton). - **Synty Polygon characters share one Generic skeleton**; the FBX needs **Optimize Game Objects OFF** (Rukhanka requirement; SciFiSpace was already so). Entity origin = capsule **center** (~1 m up) → offset the **un-keyed `Root` bone** local Y (clips key no Root/Hips position → Rukhanka bakes the offset as a constant → it persists through animation). Root motion **OFF** (the CC owns the transform; the blend tree is velocity-driven). - **`Unity.Physics` must be a DIRECT asmdef ref** for any system whose source-gen touches `KinematicCharacterBody` (it nests `Unity.Physics.ColliderKey`) → else CS8377/CS0012 in `*.g.cs` (same class as the `Unity.Transforms` direct-ref rule). +- **ENEMIES reuse the player pipeline** — a Husk is an ownerless interpolated ghost = a remote player, so `EnemyAnimationDriveSystem` mirrors `PlayerAnimationDriveSystem`'s REMOTE path (`LocalTransform` delta velocity + prevPos cache; facing via `AnimParamMath.PlanarForward`; maxSpeed from baked `EnemyStats`; `IsAttacking = AttackWindup != 0`). **Drop `[RequireMatchingQueriesForUpdate]`** so the prune runs every frame (Husks die often → else a cache entry leaks per kill). No server/asmdef/ghost-hash change. Build enemy prefabs via the **`EnemyRigTools`** editor tool (real `PrefabUtility`; `RigDefinitionAuthoring` by reflection), **GUID-preserving** (`DeleteAsset+CopyAsset` orphans subscene refs). `WaveSystem` uses `baked.WithPosition` (not `FromPosition` → resets Scale, a `[GhostField]`). See [[DR-023_Enemy_Animation_MonsterMash]] + [[Synty_Asset_Inventory]]. ### MCP / editor workflow ★ - **Edit Assets `.cs` ONLY via MCP `apply_text_edits` / `create_script`** (Unity's scripting pipeline) — the raw `Write` tool does NOT reliably trigger a recompile on an unfocused editor → tests/`execute_code` run a **stale assembly**. A raw-`Write`-created NEW `.cs` is worse — it gets **no `.meta` / no test-discovery** until `refresh_unity scope=all mode=force` (it compiles, but EditMode silently won't run it). (`Write`/`Edit` are fine for non-asset files: this vault, asmdef JSON, etc.) For comment/string-precise edits to existing scripts, `script_apply_edits` **`anchor_replace`** (regex anchor) + **`delete_method`** work cleanly even on a `struct : ISystem` (unlike `replace_method`). @@ -128,8 +129,8 @@ Full rationale: [[DR-022_Animation_Pipeline_Rukhanka_Synty]]. Skeletal animation ## Bootstrap & worlds - `ProjectM.Simulation.GameBootstrap : ClientServerBootstrap` overrides `Initialize` with `AutoConnectPort = 0` (M4 — listen/connect is explicit via the `ConnectionConfig` singleton + the per-world ConnectionControlSystems, no auto-connect). **Editor default = instant-into-game + MPPM** (creates `ServerWorld` (`WorldFlags.GameServer`) + `ClientWorld` (`WorldFlags.GameClient`)); the `ProjectM/Boot Into Menu (Editor)` EditorPref flips the MAIN editor to the frontend path. **Player builds boot the UITK frontend menu** (`return false` → one menu world, no netcode worlds until a menu choice). See [[DR-019_Frontend_Menu_Settings_Saves_Build]]. -- **Scenes:** `Assets/Scenes/MainMenu.unity` (build index 0) boots the UITK frontend (menu world only, no netcode); `Assets/Scenes/Game.unity` (index 1) holds gameplay with `Assets/_Project/Subscenes/Gameplay.unity` wired in as the baked subscene (GameObject `GameplaySubScene`). `SampleScene` / `DevSandbox` are kept as reference / dev scenes. The on-demand lifecycle (`WorldLauncher` / `SessionRunner` / `MainMenuController`) creates the right worlds per menu choice (Single / Host / Join), THEN `LoadScene(Game)` — the subscene streams into the on-demand world only if a netcode world is the `DefaultGameObjectInjectionWorld` when the scene loads. -- **Region split:** one server world; the expedition lives at `base + (1000,0,0)`, hidden per-connection via `GhostRelevancy` (see the Netcode gotchas). Expedition *place* = cosmetic ground/pillars in the Game scene at the +1000 offset; gameplay nodes/gates are baked subscene entities. See [[DR-013_M6_Aether_Cycle_Region_Split]]. +- **Scenes:** `Assets/Scenes/MainMenu.unity` (build index 0) boots the UITK frontend (menu world only, no netcode); `Assets/Scenes/Game.unity` (index 1) holds gameplay with `Assets/_Project/Subscenes/Gameplay.unity` wired in as the baked subscene (GameObject `GameplaySubScene`). `SampleScene` / `DevSandbox` are kept as reference / dev scenes. The on-demand lifecycle (`WorldLauncher` / `SessionRunner` / `MainMenuController`) creates the right worlds per menu choice (Single / Host / Join), THEN `LoadScene(Game)` (subscene-streaming rule above). +- **Region split:** one server world; the expedition lives at `base + (1000,0,0)`, hidden per-connection via `GhostRelevancy` (Netcode gotchas). Place = cosmetic ground/pillars at the +1000 offset; nodes/gates are baked subscene entities. See [[DR-013_M6_Aether_Cycle_Region_Split]]. ## DOTS / ECS conventions (authoritative summary) @@ -166,12 +167,8 @@ Full rules: `~/.claude/skills/dots-dev/references/dots-conventions.md` (Windows: | **Native Claude memory** (`memory/`, `MEMORY.md`) | Machine-local facts, working-style, preferences | **No** | - Where is X / who calls it → **serena** (fallback `Grep`/`Glob`). What did we decide / how does Z work → **basic-memory** → read the vault note. Literal string / asset GUID → **Grep/Glob**. Current DOTS API → **context7**. Conventions → this file. Long-form build lessons → `Docs/Vault/_Meta/CLAUDE_Build_Gotchas_Archive.md`. -- **Cross-machine rule:** durable truth goes in the **vault** or **this file** (both committed). Native `memory/` is local-only and does NOT sync — never the sole home of a decision. -- **serena C# caveat:** its language server is flaky on Unity. If `find_symbol` errors/stalls, **fall back to `Glob`/`Grep`**. +- **Cross-machine rule:** durable truth → the **vault** or **this file** (both committed); native `memory/` is local-only, never the sole home of a decision. **serena C# caveat:** flaky on Unity — if `find_symbol` stalls, fall back to `Glob`/`Grep`. ## Per-machine setup (NOT in git — redo on each machine) -`.mcp.json` is committed and portable (`${CLAUDE_PROJECT_DIR}` only). The **`dots-dev` skill travels with the repo** at `.claude/skills/dots-dev/` (auto-discovered on clone). Each machine still needs: -1. `uv`/`uvx`, the Obsidian app + `obsidian-cli`. (The `unity-mcp-skill` and native `memory/` notes are machine-local and do **not** sync.) -2. **basic-memory project registration:** `uvx basic-memory project add gamevault "/Docs/Vault" --default`, then `uvx basic-memory reindex --full --search --embeddings --project gamevault`. -3. Unity 6.4 opens the project and the CoplayDev Unity-MCP bridge connects (`mcpforunity://editor/state` → `ready_for_tools`). +`.mcp.json` is committed + portable (`${CLAUDE_PROJECT_DIR}`); the **`dots-dev` skill travels with the repo** (`.claude/skills/dots-dev/`). Each machine still needs: (1) `uv`/`uvx` + Obsidian app + `obsidian-cli` (the `unity-mcp-skill` + native `memory/` are machine-local, don't sync); (2) basic-memory registration — `uvx basic-memory project add gamevault "/Docs/Vault" --default` then `uvx basic-memory reindex --full --search --embeddings --project gamevault`; (3) Unity 6.4 open + the Unity-MCP bridge connected (`mcpforunity://editor/state` → `ready_for_tools`). diff --git a/Docs/Vault/06_Roadmap/Synty_Asset_Inventory.md b/Docs/Vault/06_Roadmap/Synty_Asset_Inventory.md new file mode 100644 index 000000000..61e85ac2b --- /dev/null +++ b/Docs/Vault/06_Roadmap/Synty_Asset_Inventory.md @@ -0,0 +1,51 @@ +--- +title: Synty Asset Inventory — enemy picks + future-development catalog +type: reference +date: 2026-06-06 +tags: [reference, synty, assets, enemies, environment, vfx, ui, roadmap] +permalink: gamevault/06-roadmap/synty-asset-inventory +--- + +# Synty Asset Inventory + +Catalog of the Synty Polygon packs under `Assets/Synty/`, produced by the [[DR-023_Enemy_Animation_MonsterMash]] research pass. Purpose: (1) pick enemy characters, (2) note what each pack offers future development. **Enemy-grade = shares the unified Synty Polygon *Generic* skeleton** (`animationType:3` + Optimize-Game-Objects OFF) so the existing `AC_Polygon` / Base-Locomotion clips + the DR-022/DR-023 Rukhanka recipe drive it with **zero per-pack avatar work**. + +## Character packs + +| Pack | Characters | Rig | Enemy use | +|---|---|---|---| +| **PolygonSciFiSpace** | the player soldier + aliens (`SM_Chr_Alien_01`), `BR_BigAlien`, robots | Generic ✓ (player reference) | Already the player. Aliens/BigAlien = a ready sci-fi enemy faction. | +| **PolygonSciFiCity** | 20 cyberpunk humanoids: Robot, CyborgNinja, Android, Cyber, Alien, Muscle, Junky/Garbage | Generic ✓ (verified) | **Best next enemy faction** — one skeleton + one atlas → one material; on-theme. | +| **PolygonKaiju** | 4 giant monsters (+40 recolor skins) | Generic core ✓ (extra Jaw/Eye bones idle; tail = separate attach) | **Brute/boss** (in use: Kaiju_01 = Brute). Reskin alts = cheap variants. | +| **PolygonWerewolf** | 1 werewolf + undead variant | Generic ✓ | **In use** (Grunt + Swarmer). Eye-glow mat; 21 biped attachments + 2 axes reusable. | +| **PolygonMech** | humanoid pilots (drop-in); the mech walker | pilots Generic ✓; walker = bespoke rig | Pilots = drop-in Grunts. Walker = its own rig (separate task). | +| **PolygonGeneric** | Robot / Skeleton / **Charred** (a literal "husk") | Generic ✓ | Charred/Robot/Skeleton are sci-fi-adjacent enemy options. | +| **PolygonDog** | Hellhound / Wolf / Zombie / Robot / SciFi dogs | **custom quadruped** (`animationType:2`, ships its own clips) | The ideal quadruped **Swarmer** — but a SEPARATE Rukhanka rig + dog controller (independent track, not drop-in). | +| PolygonFantasyHeroCharacters / Vikings / Western / DungeonRealms | fantasy/western humanoids (DungeonRealms has Undead Knight + Demon) | Generic ✓ / likely ✓ | Off-theme for sci-fi; fine as test beds or a themed mode. | + +## Environment / world (no characters — map + set-dressing) + +- **PolygonNatureBiomes** (highest env value): **Arid_Desert** reads as an off-world expedition site out of the box (satellite, solar panels, turbines, beacons, artefacts, even a **Warpgate**) — perfect for the **+1000 expedition region** ([[DR-013_M6_Aether_Cycle_Region_Split]]). Meadow_Forest (ships a post-process volume) for the home base. +- **PolygonNature**: trees/rocks/foliage/water + skybox — terrain dressing, harvestable-node visuals. +- **PNB_Core**: shared sky/weather/water/aurora atmosphere (rain/snow/fog FX, skydome, 9 ShaderGraphs — verify under URP 17.4; use the skydome in the classic-URP cosmetic root, not baked EG meshes). +- **PolygonKaiju** (env half): 23 buildings incl. **pre-destroyed** highrises/blocks + 40 props (cars, boats, bridge+rubble, neon signs, powerlines, crane, jet, missile) — a ruined-city map + FX meshes (bullets/fire/stomp). +- **PolygonFantasyKingdom**: castle/base kit + 15 **siege engines** → turret/structure visuals. **PolygonGeneric** `Models/Base`: sci-fi wall/floor kit with destroyed variants → buildable structures. **PolygonWestern/DungeonRealms/Carnival**: modular building kits, props. + +## FX / props + +- **PolygonParticleFX** (high presentation value): ~180 ready FX prefabs — muzzle/tracer/shell-eject (player gun + turret), impacts, **BloodSplat/Explosion_Body/GoreChunk** (Husk-death VFX), pickup/heal/levelup/coins (resource & goal feedback), **Portal** (expedition gateway), fire/smoke/dust (base-damage & machine ambience). Wire into `VFXConfig`/`CombatFeedbackSystem` as client-only cosmetics; strip any self-propelling Rigidbody/colliders; verify mats under URP. + +## UI / icons + +- **PolygonIcons**: 520 3D icon meshes (Network/Error/Node, Gear/Volume, Play/Pause, Heart/Shield, Crafting/Coin/Ingot, Skull/Star) — need a render-texture→sprite bake to use as UITK background-images. +- **InterfaceSciFiSoldierHUD**: a UGUI kit (does NOT drop into the code-built UITK HUD) — **harvest its sprites/fonts**: Icons_Status (Health/Armor/Oxygen/Radiation), Icons_Resources (125), Icons_Weapons/Ammo, HUD 9-slice panel skins, 3 cursors (for `AimReticleSystem`), Exo 2.0 + Orbitron fonts. +- **InterfaceCore**: UGUI dependency base — harvest 631 input-prompt glyph PNGs (Icons_Input) for scheme-correct (KBM/Gamepad) control hints. +- **SyntyPackageHelper / AnimationBaseLocomotion**: import helper (no game content) / the shared `AC_Polygon_*` controllers + locomotion clips already driving every Generic-Polygon character. + +## Roadmap hooks (from this catalog) + +1. **Cyberpunk enemy faction** — add PolygonSciFiCity meshes as new wave variants on the exact DR-023 pipeline (one new material + variant-table rows in `EnemyRigTools`). +2. **Quadruped Swarmer** — PolygonDog as a separate Rukhanka rig + dog controller (its bundled Attack/Death clips enable a real death anim). +3. **Ruined-city / off-world maps** — Kaiju destroyed-city + NatureBiomes Arid_Desert for the base + expedition regions. +4. **Combat/heal/portal VFX** — wire PolygonParticleFX into `VFXConfig`. +5. **HUD polish** — bake PolygonIcons + harvest Interface* sprites/fonts into the UITK HUD ([[DR-021_HUD_UITK_BuildPalette]]). diff --git a/Docs/Vault/07_Sessions/2026/2026-06-06_Enemy_Animation_Synty_Inventory.md b/Docs/Vault/07_Sessions/2026/2026-06-06_Enemy_Animation_Synty_Inventory.md new file mode 100644 index 000000000..337168e75 --- /dev/null +++ b/Docs/Vault/07_Sessions/2026/2026-06-06_Enemy_Animation_Synty_Inventory.md @@ -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]]. diff --git a/Docs/Vault/07_Sessions/_Decisions/DR-023_Enemy_Animation_MonsterMash.md b/Docs/Vault/07_Sessions/_Decisions/DR-023_Enemy_Animation_MonsterMash.md new file mode 100644 index 000000000..ba333a6ae --- /dev/null +++ b/Docs/Vault/07_Sessions/_Decisions/DR-023_Enemy_Animation_MonsterMash.md @@ -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.