# Phase B.4c — Door Swing Animation Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Make Holtburg's inn doors visibly swing open / closed when the player Uses them. Closes #58 and completes the M1 demo target *"open the inn door"* with full visual feedback.

**Architecture:** One block edit in `GameWindow.cs` adds a Door-specific spawn-time branch alongside the existing creature gate at line 2692. Detect Door entities by `spawn.Name == "Door"`. For each, build the same `AnimationSequencer` as creatures (load `MotionTable` from dats, construct sequencer) and immediately seed it with the `Off` cycle (closed) or `On` cycle (already open) based on the spawn's `PhysicsState` ETHEREAL bit. The existing `OnLiveMotionUpdated` handler then routes naturally — no downstream changes. Adds one diagnostic line in the UM handler for greppable verification. Spec: [`docs/superpowers/specs/2026-05-13-phase-b4c-design.md`](../specs/2026-05-13-phase-b4c-design.md).

**Tech Stack:** C# .NET 10 · existing `AnimationSequencer` + per-frame tick + WB renderer · no new dependencies.

---

## File map

| File | Op | Why |
|---|---|---|
| `src/AcDream.App/Rendering/GameWindow.cs` | Modify | Add `IsDoorSpawn` static helper + Door registration branch (after the existing `idleCycle` gate at line 2692) + `[door-cycle]` diagnostic in `OnLiveMotionUpdated`. |

No new files. No unit tests added — GameWindow integration code is runtime-verified per the project's existing precedent (B.4b's switch cases, L.2g's MotionUpdated routing).

---

## Task 1 — Add `IsDoorSpawn` helper + Door registration branch

**Files:**
- Modify: `src/AcDream.App/Rendering/GameWindow.cs`

This task adds two things in one commit: the static helper that detects Door entities, and the new `else if` branch in the live-spawn handler that registers them with a seeded `AnimationSequencer`.

- [ ] **Step 1: Add the `IsDoorSpawn` helper**

Insert this static helper immediately above the live-spawn handler. Use the `Edit` tool with this exact anchor:

`old_string`:
```
    private void OnLiveEntitySpawnedLocked(AcDream.Core.Net.WorldSession.EntitySpawn spawn)
```

`new_string`:
```
    /// <summary>
    /// Phase B.4c — door detection by server-sent name. Doors fail the
    /// generic multi-frame-idle gate at line 2692 (no idle cycle), so we
    /// register them via a sibling branch with a state-seeded sequencer.
    /// </summary>
    private static bool IsDoorSpawn(AcDream.Core.Net.WorldSession.EntitySpawn spawn)
        => spawn.Name == "Door";

    private void OnLiveEntitySpawnedLocked(AcDream.Core.Net.WorldSession.EntitySpawn spawn)
```

- [ ] **Step 2: Add the Door registration branch**

Insert the new `else if` branch immediately after the existing `idleCycle` gate's closing brace (around line 2800). The anchor is the comment line that follows the gate. Use the `Edit` tool:

`old_string`:
```
        }

        // Dump a summary periodically so we can see drop breakdowns without
        // waiting for a graceful shutdown.
        if (_liveSpawnReceived % 20 == 0)
```

`new_string`:
```
        }
        else if (IsDoorSpawn(spawn) && _animLoader is not null)
        {
            // Phase B.4c — Door swing animation. Doors fail the
            // multi-frame-idle gate above (no idle cycle) but DO have a
            // MotionTable with On/Off cycles that ACE drives via
            // UpdateMotion. Register with a seeded sequencer so the
            // per-frame tick has frames to advance from frame 1 (without
            // the seed, Sequencer.Advance(dt) returns no frames and the
            // MeshRefs rebuild at line 7691 collapses the door to origin).
            //
            // Initial cycle mirrors ACE's Door.cs:43
            // (CurrentMotionState = motionClosed): Off when the door is
            // closed at spawn, On when the spawn PhysicsState carries the
            // ETHEREAL bit (door was already open in ACE's DB).
            uint mtableId = spawn.MotionTableId ?? (uint)setup.DefaultMotionTable;
            if (mtableId != 0)
            {
                var mtable = _dats.Get<DatReaderWriter.DBObjs.MotionTable>(mtableId);
                if (mtable is not null)
                {
                    var sequencer = new AcDream.Core.Physics.AnimationSequencer(setup, mtable, _animLoader);

                    const uint NonCombatStance = 0x80000001u;
                    const uint MotionOn = 0x4000000Bu;   // ACE MotionCommand.On (door open)
                    const uint MotionOff = 0x4000000Cu;  // ACE MotionCommand.Off (door closed)
                    const uint EtherealPs = 0x4u;
                    uint spawnState = spawn.PhysicsState ?? 0u;
                    uint initialCycle = (spawnState & EtherealPs) != 0 ? MotionOn : MotionOff;
                    if (sequencer.HasCycle(NonCombatStance, initialCycle))
                        sequencer.SetCycle(NonCombatStance, initialCycle);

                    var template = new (uint, IReadOnlyDictionary<uint, uint>?)[meshRefs.Count];
                    for (int i = 0; i < meshRefs.Count; i++)
                        template[i] = (meshRefs[i].GfxObjId, meshRefs[i].SurfaceOverrides);

                    _animatedEntities[entity.Id] = new AnimatedEntity
                    {
                        Entity = entity,
                        Setup = setup,
                        Animation = null,            // sequencer-driven; tick reads sequencer state
                        LowFrame = 0,
                        HighFrame = 0,
                        Framerate = 0f,
                        Scale = scale,
                        PartTemplate = template,
                        CurrFrame = 0,
                        Sequencer = sequencer,
                    };

                    if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeBuildingEnabled)
                        Console.WriteLine(System.FormattableString.Invariant(
                            $"[door-anim] registered guid=0x{spawn.Guid:X8} entityId=0x{entity.Id:X8} mtable=0x{mtableId:X8} initialCycle=0x{initialCycle:X8}"));
                }
            }
        }

        // Dump a summary periodically so we can see drop breakdowns without
        // waiting for a graceful shutdown.
        if (_liveSpawnReceived % 20 == 0)
```

- [ ] **Step 3: Build green**

Run: `dotnet build src/AcDream.App/AcDream.App.csproj -c Debug`

Expected: build succeeds, 0 errors. Any new warnings should be tied to the additions only.

If a name like `meshRefs`, `entity`, `setup`, or `scale` doesn't resolve in scope at the insertion point, STOP and report — these are variables that exist in the surrounding scope at line 2800 of `OnLiveEntitySpawnedLocked` (verified during spec authoring at line 2700-2784 reads). They should be in scope; if Edit landed in the wrong place, fix the anchor first.

- [ ] **Step 4: Tests green**

Run: `dotnet test`

Expected: **1046 pass / 8 pre-existing-baseline fail** (same as main HEAD `3e08e10`). Any regression here means the new branch is touching unrelated code paths — investigate.

- [ ] **Step 5: Commit**

```bash
git add src/AcDream.App/Rendering/GameWindow.cs
git commit -m "$(cat <<'EOF'
feat(B.4c): door spawn-time AnimationSequencer with state-seeded initial cycle

Adds IsDoorSpawn helper and a sibling branch to the live-spawn
handler's animation registration gate. Detects entities where
spawn.Name == "Door" and registers them in _animatedEntities with an
AnimationSequencer seeded from the spawn PhysicsState's ETHEREAL bit
(Off cycle if closed, On if already open).

Mirrors ACE Door.cs:43 (CurrentMotionState = motionClosed) so the
sequencer always has frames for the per-frame tick to advance from
the first render. Without the seed, Advance(dt) returns no frames and
the MeshRefs rebuild at line 7691 collapses the door to origin.

No changes to OnLiveMotionUpdated, AnimationSequencer, EntitySpawnAdapter,
or the per-frame tick. The tick's sequencer branch at line 7497 reads
ae.Sequencer.Advance(dt) and never touches ae.Animation in this path
(only the legacy slerp else branch at line 7644+ does).

[door-anim] registered diagnostic gated on ACDREAM_PROBE_BUILDING.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Task 2 — Add `[door-cycle]` UM dispatch diagnostic

**Files:**
- Modify: `src/AcDream.App/Rendering/GameWindow.cs`

Adds one diagnostic line in `OnLiveMotionUpdated` that fires whenever an `UpdateMotion` arrives for an entity named "Door". Greppable trail for verification of the open/close cycle dispatch.

- [ ] **Step 1: Locate the diagnostic insertion point**

Run (Grep tool):
```
pattern: ACDREAM_DUMP_MOTION.*== "1"
path:    src/AcDream.App/Rendering/GameWindow.cs
output:  content with -n
```

Expected: one match around line 3075 in the `OnLiveMotionUpdated` body. The `[door-cycle]` diagnostic goes immediately after this `ACDREAM_DUMP_MOTION` block so both diagnostics are grouped.

- [ ] **Step 2: Add the `[door-cycle]` diagnostic**

Use the `Edit` tool. The anchor is the closing brace + blank line + the next code section ("Wire server-echoed RunRate") which follows the `ACDREAM_DUMP_MOTION` block at line 3075-3087:

`old_string`:
```
                $"UM guid=0x{update.Guid:X8} mt=0x{update.MotionState.MovementType:X2} stance=0x{stance:X4} cmd={cmdStr} spd={spd:F2} " +
                $"| seq now style=0x{seqStyle:X8} motion=0x{seqMotion:X8}");
        }

        // Wire server-echoed RunRate first — used for the player's own
```

`new_string`:
```
                $"UM guid=0x{update.Guid:X8} mt=0x{update.MotionState.MovementType:X2} stance=0x{stance:X4} cmd={cmdStr} spd={spd:F2} " +
                $"| seq now style=0x{seqStyle:X8} motion=0x{seqMotion:X8}");
        }

        // Phase B.4c — durable per-Door UM dispatch trail for visual-test grep.
        if (AcDream.Core.Physics.PhysicsDiagnostics.ProbeBuildingEnabled
            && _liveEntityInfoByGuid.TryGetValue(update.Guid, out var doorInfo)
            && doorInfo.Name == "Door")
        {
            Console.WriteLine(System.FormattableString.Invariant(
                $"[door-cycle] guid=0x{update.Guid:X8} stance=0x{update.MotionState.Stance:X4} cmd=0x{(update.MotionState.ForwardCommand ?? 0u):X4}"));
        }

        // Wire server-echoed RunRate first — used for the player's own
```

- [ ] **Step 3: Build green**

Run: `dotnet build src/AcDream.App/AcDream.App.csproj -c Debug`

Expected: build succeeds, 0 errors.

If the name `_liveEntityInfoByGuid` doesn't resolve, STOP and report. It exists in `GameWindow.cs` (verified during spec authoring; used elsewhere in `DescribeLiveEntity` around line 8758 of the B.4b-shipped tree).

If `doorInfo.Name` doesn't resolve, the field on the live-entity info struct may be named differently (e.g. `EntityName`). Use Grep to find the existing usage pattern and adjust.

- [ ] **Step 4: Tests green**

Run: `dotnet test`

Expected: same **1046 pass / 8 pre-existing-baseline fail** from Task 1.

- [ ] **Step 5: Commit**

```bash
git add src/AcDream.App/Rendering/GameWindow.cs
git commit -m "$(cat <<'EOF'
feat(B.4c): [door-cycle] diagnostic in OnLiveMotionUpdated

Logs one line per UpdateMotion arriving for an entity named "Door"
when ACDREAM_PROBE_BUILDING=1. Greppable trail for the B.4c visual
test: confirms the dispatcher hit the sequencer for door open / close.

Durable subsystem-named tag per the Opus reviewer's B.4b feedback
([B.4c] would rot after phase archival; [door-cycle] survives).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

---

## Task 3 — Visual verification at Holtburg inn doorway

**This task is performed by the user.** The implementing agent kicks off the launch in background; the user observes the running client and reports the result.

- [ ] **Step 1: Kill any stale client + wait for ACE session cleanup**

Run via PowerShell:
```powershell
Get-Process -Name AcDream.App -ErrorAction SilentlyContinue | Stop-Process -Force
Start-Sleep -Seconds 20
```

Per CLAUDE.md "Logout-before-reconnect": ACE keeps the last session alive briefly after disconnect. 20s is the empirical minimum from B.4b's debug session.

- [ ] **Step 2: Launch the client with probes enabled**

Run via Bash tool with `run_in_background: true`:
```powershell
$env:ACDREAM_DAT_DIR        = "$env:USERPROFILE\Documents\Asheron's Call"
$env:ACDREAM_LIVE           = "1"
$env:ACDREAM_TEST_HOST      = "127.0.0.1"
$env:ACDREAM_TEST_PORT      = "9000"
$env:ACDREAM_TEST_USER      = "testaccount"
$env:ACDREAM_TEST_PASS      = "testpassword"
$env:ACDREAM_DEVTOOLS       = "1"
$env:ACDREAM_PROBE_BUILDING = "1"
$env:ACDREAM_DUMP_MOTION    = "1"
dotnet run --project src\AcDream.App\AcDream.App.csproj --no-build -c Debug 2>&1 |
  Tee-Object -FilePath "launch-b4c.log"
```

- [ ] **Step 3: User performs the scenario**

In the running client:
1. Wait ~8s for the player to spawn at Holtburg.
2. Walk to the inn doorway (south building, north-facing door).
3. Observe: door visually closed.
4. Double-left-click the door.
5. **Observe: door visibly swings open over a fraction of a second.**
6. Walk forward through the open doorway.
7. Wait ~30s in the inn.
8. **Observe: door visibly swings closed.**
9. Bump the closed door — confirm it blocks again.
10. Close the client window.

- [ ] **Step 4: Grep the log**

Run via PowerShell:
```powershell
Select-String -Path launch-b4c.log -Pattern "door-anim|door-cycle|UM guid=.*Door|setstate.*0x7A9B4015"
```

Expected matches (in approximate order):
- `[door-anim] registered guid=0x... mtable=0x... initialCycle=0x4000000C` (one per closed door at world load)
- (user double-clicks)
- `UM guid=0x7A9B4015 mt=... stance=0x0001 cmd=0x000B ...` (existing UM dump for the open motion)
- `[door-cycle] guid=0x7A9B4015 stance=0x0001 cmd=0x000B` (NEW; cmd=On)
- `[setstate] guid=0x7A9B4015 entityId=0x000F4245 state=0x0001000C` (L.2g chain)
- ~30s gap
- `UM guid=0x7A9B4015 mt=... stance=0x0001 cmd=0x000C ...`
- `[door-cycle] guid=0x7A9B4015 stance=0x0001 cmd=0x000C` (NEW; cmd=Off)
- `[setstate] guid=0x7A9B4015 entityId=0x000F4245 state=0x00010008`

- [ ] **Step 5: Decide on follow-up based on observed behavior**

- **Animation plays + door rests at open pose for ~30s + animation plays again on close + rests at closed pose**: success. Proceed to Task 4.
- **Animation plays as a loop instead of one-shot** (door spins continuously): pivot to Approach C from the spec (bespoke `DoorAnimationState` outside the sequencer). Out of B.4c scope; revise the spec and file a slice 2.
- **No animation, but log shows the dispatch fired**: motion-table cycle resolution issue. Inspect `mtable.Cycles[(0x80000001 << 16) | 0x4000000B]` to see if the cycle is present. May need a different cycle key form.
- **`[door-anim] registered` never logs**: spawn-time branch isn't firing. Check `spawn.Name` actual value (might be localized or padded). Add a one-line `Console.WriteLine` of `spawn.Name` in the live-spawn handler to surface it, then revise `IsDoorSpawn` accordingly.

---

## Task 4 — Ship handoff + close #58 + roadmap/CLAUDE/memory updates

**Files (in-repo):**
- Create: `docs/research/2026-05-13-b4c-shipped-handoff.md`
- Modify: `docs/ISSUES.md` (close #58)
- Modify: `docs/plans/2026-04-11-roadmap.md` (add B.4c row to shipped table)
- Modify: `CLAUDE.md` ("Currently in Phase L.2" paragraph + Next phase candidates)

**File (outside repo):**
- Modify: `C:\Users\erikn\.claude\projects\C--Users-erikn-source-repos-acdream\memory\project_interaction_pipeline.md`

- [ ] **Step 1: Write the ship-handoff doc**

Create `docs/research/2026-05-13-b4c-shipped-handoff.md`. Model after `docs/research/2026-05-13-b4b-shipped-handoff.md` for structure: TL;DR / What shipped (commit table) / End-to-end flow with actual observed evidence / Open notes / Reproducibility / Worktree state.

Required content:
- TL;DR: B.4c shipped, M1 demo target "open the inn door" now has full visual feedback. ~50 LOC, 2 implementation commits + 1 docs commit.
- What shipped table (2 implementation commits from Tasks 1+2)
- Actual observed `[door-anim]` and `[door-cycle]` log lines from Task 3 step 4
- Worktree branch: `claude/phase-b4c-door-anim`, 4 commits ahead of `3e08e10` (the B.4b merge)

- [ ] **Step 2: Move #58 from Active to Recently Closed in `docs/ISSUES.md`**

Edit `docs/ISSUES.md`:
- Cut the `## #58 — Door swing animation` block from "Active issues".
- Paste under "Recently closed" with header changed to `## #58 — [DONE 2026-05-13] Door swing animation: ...`.
- Add `**Status:** DONE` and `**Closed:** 2026-05-13` lines.
- Add a one-paragraph closure summary describing the fix: Door-specific spawn-time branch + state-seeded SetCycle + UM diagnostic. Cite this PR's merge commit + the handoff doc.

- [ ] **Step 3: Update the roadmap's shipped table**

Edit `docs/plans/2026-04-11-roadmap.md`. Add a new row to the "shipped" table:

```
| 2026-05-13 | Phase B.4c — Door swing animation | <commit> | Closes #58. Door-specific spawn-time AnimationSequencer registration with state-seeded initial cycle. M1 demo target "open the inn door" now has full visual feedback. |
```

(Read the table first to match its column structure exactly — the B.4b row uses `Phase | What landed | Verification`; match that.)

- [ ] **Step 4: Update `CLAUDE.md` "Currently in Phase L.2" paragraph + Next phase candidates**

Edit `CLAUDE.md`:
- Update "Currently in Phase L.2" paragraph to reflect B.4c shipped + visual-verified 2026-05-13.
- Remove `#58 — Door swing animation` from the "Next phase candidates" list.
- Elevate the next candidate (currently #2 in the list: "Triage the chronic open-issue list") to #1, OR pick a different next-phase based on M1 critical-path-ness. The natural next step per CLAUDE.md's "work-order autonomy" rule is whichever progresses M1's remaining demo targets ("click an NPC", "pick up an item") — file a one-line note that these are the M1-critical-path follow-ups even though they aren't pre-specced phases.

- [ ] **Step 5: Update the memory file** (outside the repo, NOT git-tracked)

Edit `C:\Users\erikn\.claude\projects\C--Users-erikn-source-repos-acdream\memory\project_interaction_pipeline.md`:
- Append a "B.4c shipped 2026-05-13" entry to the components table:
  - `Door swing animation` — exists now (`GameWindow.cs IsDoorSpawn + sibling spawn-time branch`)
  - `[door-anim]` / `[door-cycle]` diagnostics — gated on `ACDREAM_PROBE_BUILDING`
- Note: animation routing is door-specific, not general non-creature support yet (chests/levers/traps still drop through the gate).

- [ ] **Step 6: Commit the in-repo docs**

```bash
git add docs/research/2026-05-13-b4c-shipped-handoff.md docs/ISSUES.md docs/plans/2026-04-11-roadmap.md CLAUDE.md
git commit -m "$(cat <<'EOF'
docs(B.4c): ship handoff + close #58 + roadmap/CLAUDE update

Phase B.4c shipped end-to-end 2026-05-13. Holtburg inn doorway
double-click verified: door visually swings open, player walks
through, door visually swings closed ~30s later.

2 implementation commits:
- B.4c Task 1: door spawn-time AnimationSequencer with state-seeded cycle
- B.4c Task 2: [door-cycle] diagnostic in OnLiveMotionUpdated

Closes #58. Memory file project_interaction_pipeline.md updated
outside the repo.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

(The memory file lives outside the repo — update it but don't include it in this commit.)

- [ ] **Step 7: Hand off to merge (controller does final review + merge)**

After this commit, hand off to the controller. The controller will:
1. Run the final whole-branch code review (Opus per CLAUDE.md "load-bearing quality review of a phase boundary").
2. Merge `claude/phase-b4c-door-anim` → `main` with `--no-ff`.
3. Verify tests on merged main.
4. Remove the worktree (best-effort; submodules may block per the B.4b finishing experience).

---

## Self-review against the spec

| Spec section | Plan task(s) | Coverage |
|---|---|---|
| §Architecture: sibling branch after creature gate at line 2692 | Task 1 step 2 | covered |
| §Architecture: state-seeded initial cycle from spawn.PhysicsState | Task 1 step 2 | covered |
| §Components: `IsDoorSpawn(spawn) => spawn.Name == "Door"` | Task 1 step 1 | covered |
| §Components: Door registration body (sequencer build + SetCycle + AnimatedEntity register) | Task 1 step 2 | covered |
| §Components: `[door-anim] registered` diagnostic on spawn | Task 1 step 2 | covered (inline in registration body) |
| §Components: `[door-cycle]` diagnostic in OnLiveMotionUpdated | Task 2 step 2 | covered |
| §Data flow: spawn → seeded cycle → UM dispatch → state flip → animation | Tasks 1+2 + L.2g (existing) | covered (L.2g pipeline is the upstream dependency) |
| §Error handling: door has no MotionTable | Task 1 step 2 (`if (mtableId != 0)` + inner `if (mtable is not null)`) | covered |
| §Error handling: MotionTable lacks On/Off cycle | Task 1 step 2 (`if (sequencer.HasCycle(...))` gate around SetCycle) | covered |
| §Error handling: `_animLoader` null | Task 1 step 2 (outer `&& _animLoader is not null`) | covered |
| §Error handling: spawn.Name != "Door" | (no code change — silent fallback, acceptable per spec) | covered by omission |
| §Testing: runtime visual verification at Holtburg | Task 3 | covered |
| §Testing: log grep | Task 3 step 4 | covered |
| §Acceptance: build + tests green | Tasks 1+2 steps 3-4 | covered |
| §Acceptance: ISSUES.md #58 → Recently closed | Task 4 step 2 | covered |
| §Acceptance: roadmap + CLAUDE.md update | Task 4 steps 3-4 | covered |
| §Non-goals: sound effects, dust, lighting, collision rotation, generalized non-creature support | (none — explicitly deferred) | covered by omission |

No placeholders. No "TBD." Every code step shows the actual code; every command step shows the exact command and expected output. Type names (`AnimationSequencer`, `AnimatedEntity`, `MotionTable`, `EntitySpawn`) match across tasks. Diagnostic tags (`[door-anim]`, `[door-cycle]`) consistent throughout.
