# Handoff — Combat + UI Shell Phases A–K.1

**Date:** 2026-05-17 (revised, post-K.1)
**Status:** Phases A–J shipped + Phase K.1 live-validated. 16 commits on `master` (`31cf067…aa6cc8c`). Three production bugs surfaced + fixed during K.1; one motion-table follow-on left visually-unverified after 1070 access ended.

**Predecessor docs:**
- `docs/ui-shell-plugin-architecture-spec-2026-05-17.md` — the architectural spec
- `docs/combat-melee-cross-reference-2026-05-17.md` — Phase A three-source doc
- `docs/ui-conformance-audit-2026-05-17.md` — retail-vs-shipped audit

**Memory entries the next agent should read first:**
- `project_holtburger_phase_k1_pass_2026-05-17.md` — K.1 wire validation, end-to-end gameplay round-trip
- `project_holtburger_wasm_stack_overflow_fix_2026-05-17.md` — load-bearing fix that unblocked browser login
- `project_holtburger_motion_table_combat_path.md` — concrete plan for real MotionTable-driven anims
- `feedback_three_source_cross_reference.md` — the dev-cycle methodology
- `feedback_no_partial_demos.md` — "don't bypass the load-bearing path"

---

## What's true now

### Phase K.1 — live-ACE validation: **PASS (6/6)**

Every combat / magic wire packet shipped in Phases B–J round-trips against a live local ACE. ACE-side handler logs echo back the exact decoded values (`HandleActionTargetedMeleeAttack(DEADBEEF, 2, 0.5)`, etc.), confirming byte-perfect pack. End-to-end gameplay also confirmed: `@create 7` (Drudge Skulker) → enter combat → click drudge → `damageTaken` / `evadedAttacker` / `attackDone` / `playerStatsUpdated` events flow through to client plugins.

| Phase | Packet | Sub-op | Result |
|---|---|---|---|
| B | TargetedMeleeAttack | 0x0008 | PASS |
| E | TargetedMissileAttack | 0x000A | PASS |
| F | CastTargetedSpell | 0x004A | PASS |
| F | CastUntargetedSpell | 0x0048 | PASS |
| J | RemoveSpellFromBook | 0x01A8 | PASS |
| H/B prereq | ChangeCombatMode | 0x0053 | PASS |

Method + reproducer: `apps/holtburger-web/capture_phase_k_combat_validation.cjs` (automated wire byte-level checks), `apps/holtburger-web/k1_drive_combat.cjs` (end-to-end gameplay drive).

### Bugs fixed during K.1 (16 commits, three thematic groups)

**Load-bearing wasm fixes** (commit `c7c8a44`):
- `.cargo/config.toml` — raise wasm linker stack 1 MiB → 8 MiB. The recv-loop's two 128 KiB UDP buffers + async future state overflowed the default. Login OOB'd at first `CONNECT_REQUEST` handshake. Local chromium masked the trap as `"Target crashed"`; only real-GPU Chrome (the GTX 1070 path under `[[holtburger-clouds-e-done-2026-05-15]]`) surfaced the actual `RuntimeError: memory access out of bounds` at `holtburger_session::session::receive::recv_message::{{closure}}`.
- `ToggleCombatMode` treated `CombatMode::Undef` the same as `NonCombat`. Pre-fix the first toggle saw `Undef` (because ACE's `PrivateUpdatePropertyInt(CombatMode)` doesn't reliably hydrate `world.player.combat_mode` on the wasm side), fell through to `else → NonCombat`, no-op'd. Bonus: added `setCombatMode(mode)` wasm method so the Combat plugin can derive direction from the authoritative motion-stance label instead of the stale property cache.
- `publish_player_known_spells_snapshot` now merges three sources (`world.player.spells` from PlayerDescription, `entity.spell_book` from IdentifyObject, mid-session arm cache). Pre-fix only read the IdentifyObject path which never auto-fires for the local player on login → spellbook always empty after relog.
- New recv arms for `GameEvent::MagicUpdateSpell` (0x02C1) + `MagicRemoveSpell` (0x01A8). `@addspell` / `@removespell` now reflect in the spellbook live.

**UX polish** (same commit `c7c8a44`):
- `scene3d/camera.js`: retail-AC mouse model. Right-mouse-button + drag rotates the follow camera. Cursor always visible (no PointerLockControls auto-lock). Browser context menu suppressed on the canvas. `C` key now goes directly to `topDown` (de-facto minimap) on first tap; cycle order is `follow → topDown → orbit → follow`.
- `scene3d/picking.js`: charge-attack manual-input override — WASD/Q/E/Shift abort the rAF auto-pursue. **Any left-click also cancels an in-flight charge**, even if the click misses an entity (closes the "stuck walking north after a missed click" path + restores NPC-talk / interact-with-object behaviour after a charge).
- `scene3d/entities.js`: `classifyMotionCommand` widened to recognize attack-family (0x0058–0x006A, 0x0061, 0x00D0–0x00D2, 0x018F–0x0194), cast-family (0x002B–0x0032, 0x006F–0x0078, 0x00D3), and Jump/JumpCharging (0x003B / 0x001D). `setMotion` plays them with `LoopOnce` through the existing `AnimationCache.get(...)` → `fetchEntityAnimationKeyframes` path. **Not visually re-verified** before 1070 access ended; see `Open` below.
- `plugins/combat-bar.js`: standalone `stance-toggle` plugin folded in. One slot instead of two. Minimalist labels (Hi/Mid/Lo, Power, Repeat, Charge, one-word stance). Stance button uses `setCombatMode(1|2)` derived from the live motion-stance label — toggle now works in both directions.
- `plugins/spellbook.js`: misleading "log in to populate" → "No spells known." (you ARE logged in). Uncatalogued spells (rare with the full LSD catalog now wired) render as `Spell #<id>` so they're visible. Catalog filter logic preserved.
- `index.html`: `applyConfirmedStance` emits `playerStatsUpdated` on every kind=5 motion update so plugins refresh. Standalone stance-toggle slot removed from `barSlots`.

**Catalog + framework** (commits `3824fac` + `aa6cc8c`):
- Spell catalog: 26 starters → **6266 LSD entries** (1.68 MB). Generated by `scripts/build_spells_catalog.py` from `external/LSD-Partial-2025-02-23_16-15/spells.json`. Re-run the script when LSD updates. Browser caches the catalog after first fetch; module-level Promise dedup means at most one fetch per browser session.
- **Vitals HUD is now a plugin.** Phase H's `#vitals-hud-overlay` div + inline `renderVitalsHud` was extracted to `plugins/vitals-hud.js`. Required two new bar framework hooks:
  - `mount({ client, slot, root, bar })` — per-slot lifecycle, called once at `mountBar` time for every slot. Plugins that need an always-on overlay (vitals, future radar/compass) wire DOM here instead of (or in addition to) `activate(bodyEl, ctx)`.
  - `iconHidden: true` slot flag — registers the plugin but skips the bar-button render, so always-on overlays don't claim bar real estate.

### Combat surface area now live (post-K.1)

- **Bar slots** — `⚒ RynthSuite · ⚔ Combat · 📖 Spellbook · ⚙ Settings`. Vitals HUD is a registered (iconHidden) slot.
- **Top overlay** — `#hb-vitals-hud` (HP/Stamina/Mana), owned by `plugins/vitals-hud.js`. Subscribes to `playerStatsUpdated` for refresh.
- **Combat plugin (`⚔ Combat`)** shows Stance (one-word) + Combat/Peace toggle button, Hi/Mid/Lo height, Power slider, Repeat tick, Charge tick. In magic stance it transforms into the spell picker (7 numbered tabs × 8 slots per tab).
- **In-viewport** — left-click selects entity / attacks / cast / use-object based on stance; charge auto-pursues to range; right-drag rotates camera; WASD/click-elsewhere aborts charge.
- **Wire primitives** — Jump (Phase 4), Melee/Missile/MagicCast (B/E/F), ForgetSpell (J), Combat-mode toggle/set.
- **Events to `client.events`** — `damageDealt`, `damageTaken`, `evadedTarget`, `evadedAttacker`, `attackDone`, `combatCommenceAttack`, `playerStatsUpdated`.
- **localStorage** — `holtburger_ui_bar_v1`, `holtburger_combat_bar_v1`.
- **Data** — `data/spells-catalog.json` (1.68 MB, 6266 LSD spells), `data/spell-components.json` (8 scarabs — still partial).

---

## Open

> The user clarified during K.1: combat *behavior* did not advance beyond what Phases B–J already shipped. K.1 was bug-fix + validation around the existing combat surface, not new combat mechanics. Treat the items below as "K.2 candidates" — pick one per phase, three-source doc first per `feedback-three-source-cross-reference`.

### Validation gaps still open

- **Real MotionTable swing/cast/jump anims**: the `classifyMotionCommand` widening + `LoopOnce` integration in `setMotion` is in place, but the user lost 1070-Chrome access before visually confirming. Risks: motion-tables for the unarmed-HandCombat stance may not have the attack-family clips ACE broadcasts (silent fallback to no animation); the inline `setSwingPose` vibe tween is cleared at clip-play time but `_jumpPoseTween` still runs for jump (overlap may flicker). **Acceptance test**: open `?renderer=3d`, enter Melee, swing on a drudge — body should animate via real keyframes, not the triangle-wave arm sweep. Same for Jump.
- **Power-meter refill timer**: still using `600 + power × 1200 ms` approximation in `plugins/combat-bar.js`. Real ACE refill timer is in the `AttackDone` / `CombatCommenceAttack` event payloads (`GameEventAttackDone.cs` carries the `WeenieError errorType` u32; `CombatCommenceAttack` is header-only). Need to confirm whether refill duration is actually broadcast or if we just need to read the player's quickness/burden derivation from `world.player`.

### Tier 1 — gameplay-affecting

1. **Real MotionTable anims** (above). Highest leverage; partly done.
2. **Radar / minimap**. The `C`-cycled `topDown` orthographic camera is a de-facto minimap (the user noted "we kind of already have a unique to our client minimap"), but it's a full-screen overlay, not a corner widget. Build a circular top-right radar plugin that reads `entityMap` + filters by classification (player/monster/NPC/item/corpse). Mount via the new `iconHidden` framework if it should be always-on, or as a normal panel if it should be toggleable. Plugin file: `plugins/radar.js`.
3. **Compass.** Smaller scope than radar. Reads `pose.heading`. Probably another `iconHidden` plugin with a small top-edge widget.

### Tier 2 — visible but second-order

4. **Power-meter real refill timer** (above).
5. **Full `SpellComponentTable` extraction** (163 entries). Current `data/spell-components.json` has only 8 scarabs. Path: small Rust tool wrapping the wasm-side DAT reader to dump DID `0x0E00000F`. (DatReaderWriter standalone has codegen-dep issues.)
6. **Components panel + `@fillcomps`** — retail's `Magic Panel` "Components" tab + the four `@fillcomps` chat-command variants.
7. **Options Panel** plugin. Retail had 4 tabs (Gameplay / Character / Chat / Config). Today only combat-related options persist to localStorage; server-side `CharacterOption` bitfield not round-tripped.
8. **Map panel** (audit Phase J+).

### Tier 3 — polish / cosmetic

9. **Spell icons via DAT.** LSD's `iconID` (surface DID at 0x06xxxxxx) is in the catalog now but not rendered. Wire `fetchSurfacesPixels` for spellbook + combat-bar slot icons.
10. **Re-examine wire opcode** — manual `IdentifyObject` request from J.3's Examine button + a small appraisal panel.
11. **Inventory plugin** — absorb `#inventory-panel` + Selected Item Box into a bar-plugin slot. Same "everything is a plugin" refactor the vitals HUD just got.
12. **Tab names / icons** for spell-bar tabs. Today they're "1"–"7"; retail allowed renaming.
13. **Multiple secondary chat windows.** Retail had 4 named chat-window tabs with per-window category subscriptions.
14. **Allegiance / Social Systems panels** (Phase K+).
15. **Stretch UI / Display 3D Tooltips / Show Coordinates by Radar** Options Panel toggles (audit catch-all).

### Tier 4 — Phase A (cross-reference) blockers

Need a three-source doc before code lands:
- **Trade** — `Trade.cs` in ACE. Protocol crate has `OpenTradeNegotiations`, `AddToTrade`, etc. Wire plumbed; UX missing.
- **Allegiance** — `SwearAllegiance` / `BreakAllegiance` in protocol already. Social systems UX missing.
- **Item Tinkering** — full system, never touched.
- **House ownership / Mansion teleport** — `TeleToMansion` already in protocol. UX missing.

### Items closed since the original handoff

| Old item | Closed by |
|---|---|
| 1 — Live-ACE validation pass | K.1 (6/6 PASS, commit `c7c8a44`) |
| 4 — Charge Attack manual-input override | Same commit (WASD/QE/Shift abort + click-anywhere cancel) |
| 7 — Bigger spell catalog | LSD catalog wired, 26 → 6266 (commit `3824fac`) |
| Validation gap 4 — Stance change detection | K.1 driven, now works (after the Undef + setCombatMode + applyConfirmedStance fixes) |
| Validation gap 5 — `Entity.spell_book` population | Fixed via the publish-snapshot merge + the MagicUpdateSpell arm |

---

## File-by-file inventory (current)

| Concern | File |
|---|---|
| Bar shell + `mount` / `iconHidden` framework | `apps/holtburger-web/ui/bar.js` |
| Plugin facade (`createClient`) | `apps/holtburger-web/plugins/api.js` |
| Combat plugin (stance + attack-controls + spell picker) | `apps/holtburger-web/plugins/combat-bar.js` |
| Spellbook plugin | `apps/holtburger-web/plugins/spellbook.js` |
| Vitals HUD (always-on overlay) | `apps/holtburger-web/plugins/vitals-hud.js` |
| 3D click-to-entity + charge + WASD abort | `apps/holtburger-web/scene3d/picking.js` |
| Entity rigs + motion classification | `apps/holtburger-web/scene3d/entities.js` |
| Camera modes + right-drag mouselook | `apps/holtburger-web/scene3d/camera.js` |
| Animation cache | `apps/holtburger-web/scene3d/animation.js` |
| Spell catalog (6266 LSD) | `apps/holtburger-web/data/spells-catalog.json` |
| Spell-catalog regenerator | `scripts/build_spells_catalog.py` |
| Spell-component names (8 scarabs — partial) | `apps/holtburger-web/data/spell-components.json` |
| Selected-item box (in-line, not yet a plugin) | `apps/holtburger-web/index.html` (`#inventory-panel`, ~line 5540) |
| Wasm-side SessionCommand + wire arms | `apps/holtburger-web/src/lib.rs` (large) |
| Wasm linker stack-size override | `external/holtburger/.cargo/config.toml` |
| Protocol opcodes | `crates/holtburger-protocol/src/opcodes.rs` |
| Protocol GameAction packets | `crates/holtburger-protocol/src/messages/` |
| Capture script — wire validation | `apps/holtburger-web/capture_phase_k_combat_validation.cjs` |
| Capture script — end-to-end gameplay drive | `apps/holtburger-web/k1_drive_combat.cjs` |
| K.1 probe scripts (CDP diagnostics) | `apps/holtburger-web/probe_*.cjs` |

**Removed:** `plugins/stance-toggle.js` (folded into combat-bar; file still on disk but no longer imported — safe to delete in a follow-on cleanup).

## Cross-reference docs

- `docs/ui-shell-plugin-architecture-spec-2026-05-17.md` — overall architecture (still current; the `mount` / `iconHidden` additions extend but don't break the spec)
- `docs/combat-melee-cross-reference-2026-05-17.md` — Phase A example for melee
- `docs/ui-conformance-audit-2026-05-17.md` — retail-vs-shipped gap analysis

## Reference repos (already cloned)

- `/mnt/wbterminal1/ac-refs/rynthsuite/` — github.com/tombohar/RynthSuite
- `/mnt/wbterminal1/ac-refs/rynthcore/` — github.com/tombohar/RynthCore
- `/mnt/wbterminal1/ac-refs/ac-data-repo/asheron.fandom.com/` — fandom wiki XML dumps
- `~/ace-server/Source/` — full ACE server (wire-format authority)
- `~/ac-headers/acclient.h` — retail client debug-info (348 enums + 6936 structs)
- `~/ac_base_dats/` — canonical base DATs
- `external/LSD-Partial-2025-02-23_16-15/spells.json` — full LSD spell metadata (5.2 MB; now derived into `data/spells-catalog.json`)
- `external/DatReaderWriter/` — C# DAT toolkit (codegen-dep issues if used standalone)

## Build / run commands

- `cargo check -p holtburger-web --target wasm32-unknown-unknown` — fast type-check
- `wasm-pack build --target web --out-dir pkg --dev` — generate JS bundle (~3–20 s)
- `python3 -m http.server 8765 --bind 127.0.0.1` from `external/holtburger/` — dev server
- `cargo run -p holtburger-wsbridge -- --listen 127.0.0.1:8080` — WS↔UDP bridge
- Local ACE: `tail -F /dev/null | ./ACE.Server > ace.log 2>&1 &` from `~/ace-server/Source/ACE.Server/bin/Release/net10.0/` (the `tail -F /dev/null` stdin trick prevents ACE's REPL from spamming the prompt loop into the log — without it the log file grows to GB in minutes; see `[[phase-k1-combat-validation-2026-05-17]]`)
- `python3 scripts/build_spells_catalog.py` — regenerate the spellbook catalog from LSD
- `node --check <file.js>` — quick JS syntax check
- `cargo test -p holtburger-protocol --lib` — protocol pack/unpack parity tests (268 passing)

## Common gotchas (from memory)

- **ACE log spam trap**: redirecting ACE's stdin to `/dev/null` makes the REPL spam `ACE >> ` infinitely. Use `tail -F /dev/null |` as stdin. See `[[phase-k1-combat-validation-2026-05-17]]`.
- **Wasm stack**: `.cargo/config.toml` sets 8 MiB. Default 1 MiB OOBs on login. See `[[holtburger-wasm-stack-overflow-fix-2026-05-17]]`.
- **Local chromium masks wasm traps** as "Target crashed". For wasm OOB debugging, connect to a real-GPU Chrome via CDP — the trap surfaces cleanly with full Rust stack trace.
- `pkg/holtburger_web.d.ts` is sometimes stale for methods added in `lib.rs` between builds; regenerate via `wasm-pack build`.
- The `external/holtburger/dist/` symlink points to `/mnt/wbterminal2/holtburger-dist` (per memory `holtburger_bake_disk_trap`). Don't bake into `/` or `/tmp`.
- Permission system: `sudo` needs the password explicitly each time. Do NOT save anywhere.

---

## Recommended Phase K.2

Pick **one** of the Tier 1 items:

1. **Verify real MotionTable anims visually** (smallest scope — the wiring is in place; just need to confirm the clips play). Then add the missing Phase A doc and clean up the now-redundant `setSwingPose` / `_jumpPoseTween` vibe tweens.
2. **Radar plugin** — `plugins/radar.js` mounted via `iconHidden`, reads `entityMap`, draws a top-right circle. Phase A doc first: cross-reference retail radar behaviour (filter toggles, range, rotation) against ACE's `WorldObject.RadarColor` properties and RynthSuite's `RadarManager` if it has one.
3. **Compass plugin** — even smaller. Reads `pose.heading`, renders a heading needle. Same `iconHidden` mount pattern.

Subsequent phases (L, M, …) pick from Tier 2/3 individually. The `mount` + `iconHidden` framework hooks are the natural cadence for always-on widgets going forward; click-to-open plugins keep the existing `activate(bodyEl, ctx)` pattern.

---

## Closing note

K.1 closed the load-bearing "is the wire actually right?" question — yes, 6/6 packets verified against a live ACE with byte-perfect handler decodes. The three production bugs uncovered along the way (wasm stack, Undef-stance toggle, spell book population) were all real prod bugs that no offline smoke test would have caught — vindicates the `feedback-no-partial-demos` rule.

Combat behaviour is unchanged from the Phases B–J shipped surface; K.1 was bug-fix + validation, not new mechanics. The next user-visible win (Tier 1) is either visual (real MotionTable anims, radar, compass) or precision (real power-meter refill timer). All four are scoped to a single plugin or single file each.

— end of handoff
