Task-by-task plan with full code in every step, no placeholders. Task 1: IsDoorSpawn helper + Door registration branch (state-seeded SetCycle from spawn PhysicsState ETHEREAL bit). Task 2: [door-cycle] diagnostic in OnLiveMotionUpdated for greppable verification. Task 3: Holtburg inn doorway visual test (user-performed). Task 4: ship handoff + close #58 + roadmap/CLAUDE/memory updates. Self-review table at bottom maps every spec section to its task(s); all covered. Companion to spec docs/superpowers/specs/2026-05-13-phase-b4c-design.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
22 KiB
Phase B.4c — Door Swing Animation Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use
superpowers:subagent-driven-development(recommended) orsuperpowers:executing-plansto 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.
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
IsDoorSpawnhelper
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
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
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:
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:
$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:
- Wait ~8s for the player to spawn at Holtburg.
- Walk to the inn doorway (south building, north-facing door).
- Observe: door visually closed.
- Double-left-click the door.
- Observe: door visibly swings open over a fraction of a second.
- Walk forward through the open doorway.
- Wait ~30s in the inn.
- Observe: door visibly swings closed.
- Bump the closed door — confirm it blocks again.
- Close the client window.
- Step 4: Grep the log
Run via 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
DoorAnimationStateoutside 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] registerednever logs: spawn-time branch isn't firing. Checkspawn.Nameactual value (might be localized or padded). Add a one-lineConsole.WriteLineofspawn.Namein the live-spawn handler to surface it, then reviseIsDoorSpawnaccordingly.
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 of3e08e10(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 animationblock from "Active issues". -
Paste under "Recently closed" with header changed to
## #58 — [DONE 2026-05-13] Door swing animation: .... -
Add
**Status:** DONEand**Closed:** 2026-05-13lines. -
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 animationfrom 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 onACDREAM_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
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:
- Run the final whole-branch code review (Opus per CLAUDE.md "load-bearing quality review of a phase boundary").
- Merge
claude/phase-b4c-door-anim→mainwith--no-ff. - Verify tests on merged main.
- 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.