This commit is contained in:
2026-07-07 20:51:18 -07:00
parent d2203c8aa1
commit cc2e7ad95e
20 changed files with 794 additions and 35 deletions
@@ -0,0 +1,47 @@
---
title: 2026-07-07_Expedition_Enemy_Stuck_Harvest_Feedback
type: note
permalink: gamevault/07-sessions/2026/2026-07-07-expedition-enemy-stuck-harvest-feedback
---
# 2026-07-07 — Expedition: enemy-stuck fix + visibility markers + harvest feedback
**Driver:** `/dots-dev` (Small track — no ghost/RPC/relevancy/ordering change; server-only movement + client-only presentation). Two operator-reported expedition problems.
## Problems → root causes (ground truth)
1. **Enemies get stuck on cover collisions + are hard to see → a leftover enemy soft-locks the room.** Three linked causes:
- **Movement:** `EnemyMoveUtil.SweptMove` (server-only collide-and-slide, the sole enemy mover) had (a) **no depenetration** — an enemy spawned inside / shoved into a collider gets `SphereCast` fraction ≈0 → `stop = from` → frozen forever; (b) `EnemyAIMath.SlideVelocity` flattened the wall normal to XZ, so a rounded/short Synty **cover rock** (near-vertical normal) collapsed to ~0 → returned motion *straight into the wall* → net-zero; (c) only **one** slide iteration → concave rock∧boundary-rim pockets trap. The Phase-1 in-room cover rocks (4/room, Environment-layer) were the fresh geometry exposing all three. No pathfinding exists (straight-line seek + reactive slide).
- **Soft-lock:** `RoomEnemyDirectorSystem` gates clear on `aliveZone == 0` with **no timeout/fallback** — one stuck *living* Husk blocks the room forever (the `Dying` corpse-exclusion doesn't help a living one).
- **Visibility:** enemies are small, Synty-atlas lit (no rim), Swarmer desaturated green; **no persistent marker / off-screen indicator** existed.
2. **Harvest nodes lacked obvious hit feedback.** `WorldFeedbackSystem` already chipped particles+SFX on a replicated `Remaining` decrease, but subtly; no node body reaction.
## Fixes shipped
**Enemy movement (server-only, no wire change):**
- `EnemyAIMath.SlideVelocity` — degenerate (near-vertical/zero) XZ normal now deflects to a horizontal **tangent** (`(x,0,z)→(-z,0,x)`) instead of driving into the wall. Unit test updated (`SlideVelocity_DegenerateNormal_DeflectsToTangent`).
- `EnemyMoveUtil` — added a stateless **`Depenetrate`** (`CollisionWorld.CalculateDistance(PointDistanceInput,…)` → push out along `SurfaceNormal` by `radiusDistance`, capped at 1 radius/tick, floor/ceiling normals ignored) + **2 slide iterations** in `SweptMove`.
- `EnemyAISystem` — a **depenetration pre-pass** (every living enemy, top of `OnUpdate`; boss excluded) + a **stuck-nudge backstop pass** (after separation): an enemy that wants to close but gains no ground toward its target for `StuckUnstickTicks (90 ≈ 1.5s)` phase-nudges toward the nearest target (sweep bypassed) for a `NudgeBurstTicks (45)` burst — guarantees a room always resolves. Progress measured as **distance CLOSED toward target** (not raw displacement) so the separation jiggle can't mask a stuck Husk. Backstop consts inline (not on the RPC'd `TuningConfig` — server-only, avoids the IRpcCommand-hash blast-radius). New server-only component **`EnemyNavState`** (`LastPos`/`StuckTicks`/`NudgeUntilTick`, **not** a `[GhostField]`), baked by `EnemyAuthoring`. Spitters (hold at range) + boss excluded.
**Enemy visibility (client-only, new):** `EnemyMarkerSystem` — own runtime UIDocument (sortingOrder 48), pooled markers: off-screen enemies get a clamped edge **arrow** (reuses `OnboardingSystem`'s WorldToScreen edge-intersection math), on-screen get a subtle overhead **pip** that fades toward `EnemyMarkerMinAlpha` as the on-screen count rises. Range-gated to the local player (`EnemyMarkerRange 140`), boss + corpses excluded. Knobs in `FeelConfig`.
**Harvest feedback (client-only):**
- `NodeFeedbackSystem` (new) — on a `ResourceNode`/`BlightClutter` `Remaining` decrease, drives a **client-owned `PostTransformMatrix`** on the node root: progressive **shrink** toward `NodeMinScale` as it depletes + a brief **scale-pop** per hit. Chose a scale reaction over a `_BaseColor` flash because node materials (unlike white-based enemy/player bodies) aren't guaranteed white, so a base-color override could clobber their rest tint.
- `WorldFeedbackSystem` — beefed chip burst (6→12) + SFX (0.30→0.42) + a proximity-gated per-hit **camera micro-punch**.
- Knobs in `WorldFeelConfig`.
## Validation
- **L1** console clean (0 errors) throughout — including while the freshly-edited **Bursted** `EnemyAISystem` processed 5 real enemies with the new passes (no stale-Burst-binary `InvalidOperationException`, no exceptions).
- **L2** EditMode **455/455** (incl. the updated `SlideVelocity` tangent test).
- **L3 / Play smoke:** drove a run into an expedition COMBAT room (server-side ready). Enemies spawned on the ring (~14u) and **advanced across the rock-filled room to the player** (dist → 1.4, attacking) — repeatedly; not stuck. **Enemy markers visually confirmed** (`scratchpad/markers_03.png`: amber ▾ pips above every Husk, incl. a distant one by the top-right rocks). server==client enemy positions matched. Both new client systems + `WorldFeedbackSystem` ran against real enemies/nodes with zero errors.
### Validation gotcha (recorded)
Driving an AFK run headless is flaky: the buffed player's `Health` is **reset to the class value (~130) on (re)spawn**, so a `SetComponentData(Health=100000)` buff does NOT persist → the AFK player dies to the swarm in ~15s and the run aborts (`expeditionPlayers==0 → Returning`), despawning the room. Also **post-abort re-launch is gated** (a clean Play restart from Staging launches reliably on the first ready; re-readying after an abort is intermittent). To screenshot mid-fight, **capture in the SAME `execute_code` call that first detects `enemies>0`** (round-trip > survival window).
## Not freeze-framed (operator eyeball on next playtest — trivial with real input)
- **Node shrink/pop VISUAL** — `NodeFeedbackSystem` ran clean against the 2 nodes but the run-timing never let me catch the shrink on-screen. **One risk to confirm:** the shrink uses `PostTransformMatrix` on the node **root** — this scales the mesh only if the mesh is on the root or on *parented* children. If a node's mesh is an unparented render child, scale the children instead (cheap fix).
## Next-session intent
Operator playtests the expedition: confirm (1) no stuck stragglers across a few rooms, (2) markers read well / not cluttered, (3) node shrink+pop visible on harvest, (4) tune knobs (`FeelConfig.EnemyMarker*`, `WorldFeelConfig.Node*`/`Chip*`, backstop consts in `EnemyAISystem`). Then bank as a DR if the design sticks.
Related: [[DR-023_Enemy_Animation_MonsterMash]] · [[DR-044_Expedition_Redesign_Shipped_Demo_Polish]] · [[DR-045_Combat_Demo_Feel_Boss_Fight]]