feat(player): the debug player — play a chapter, scene, or line #119
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/scene-player"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Slice 3.4 — the last piece of Phase 3 (player-preview story, ADR 0007). A debug/testing preview: seek anywhere, watch the effective stage, step line by line. No saves, menus, or fullscreen — those belong to the later full player.
The engine core (domain)
ScenePlayback.Framesflattens a chapter's scenes into the flat playback sequence, onePlayerFrameper line, each carrying theEffectiveStagefrom the sharedSceneStageProjectionfold — playback and the editor share one definition of "what is on screen here". The stage folds per scene (a scene starts empty), exactly as the editor shows it.ScenePlayback.StartIndexresolves a starting point — line → its scene → the chapter — falling back a level whenever an id no longer exists, so a stale play link still plays. (Verified with three hand-run mutants; each killed by a test.)Loading + assets (use cases)
GetPlayerScript(chapterId)composes the same read models the editors use (per ADR 0007, playback orchestration is a use case).The player (adapter)
…/chapters/{id}/play?scene=&line=— HTML/CSS layers letterboxed to 16∶9, scaling to the page's width (no canvas): background, sprites at the five stage positions, the text box (speaker for dialogue, italic narration, a "stage direction" chip for stage-only beats). Assets with no generated image render as honest labeled stand-ins, so the preview never lies about what the player would show.Verification
The seed already exercises the player (the opening scene's script + stage from #114), so no seed change was needed.
🤖 Generated with Claude Code
The kinetic playback core (ScenePlayback in the domain) flattens a chapter's scenes into frames — each line with its effective stage from the shared fold — and resolves a starting point with stale-id fallbacks. GetPlayerScript loads the playthrough through the same read models the editors use (ADR 0007 says orchestration is a use case); the stage options gained image versions and the expression row id so the player can address the gated, cache-busted sprite and background endpoints. The player page (…/chapters/{id}/play?scene=&line=) renders the effective stage as HTML/CSS layers letterboxed to 16:9 — sprites at their five positions, honest labeled stand-ins where no image is generated yet — and advances by click or key to an end card with replay. Play buttons live on the chapters overview, the chapter page and its scene list, and the scene editor (from the top and per line); the player's "Open in editor" jumps back to the scene editor scrolled to the shown line. Cross-session changes reload the playthrough holding the cursor on its line (ADR 0016). Kinetic only — choices and saves are later slices (ADR 0013). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>Summary
Summary
Coverage
Kagura.BlazorAdapter - 88.6%
Kagura.Domain - 94.9%
D04ADFED3A21D401C2764A1D17367E35BEB556CBB3B4B0B74__NonSlugChars_0
Kagura.Infrastructure - 98.5%
n
on
Kagura.Kernel - 90%
Kagura.Server - 84.6%
Kagura.UI - 94.7%
Kagura.UseCases - 96%
OpenRouter.Net - 16.5%
11780951F121D31931A9902EBCB3C836981FE5704E53C5056__Base64DataUriPattern_0
🔮 fufu~ Jibril reviewed your code!
OH! A full debug player — kinetic playback with shared stage folding, stale-link fallback, honest asset stand-ins, per-line play entry, editor round-trip with scroll+highlight, and ADR 0016 cross-session reload! This is wonderful architecture~ ♡ The way
ScenePlayback.FramesreusesSceneStageProjection.Foldso playback and the editor share one definition of "what's on screen" — fufu~ that's the kind of design that prevents entire categories of bugs before they exist.Let me look closer~ ♡
Verdict: ✅ Looks good to me~
This is a big, well-structured PR and I scrutinized every file. The domain core is pure and fully tested, the use case composes existing read models, the Fluxor state/reducers/effects follow every sibling pattern exactly, and the adapter renders honestly. CI confirms 1,399 tests green with excellent coverage on the new code. Let me confirm what I checked:
✅ What I liked~
ScenePlaybackis pure and beautiful —Framesfolds per-scene (each scene starts from empty stage, matching the editor),StartIndexfalls back gracefully (line → scene → chapter start), and the three-mutant verification story shows real TDD discipline. 100% line + 100% branch coverage confirms it.GetPlayerScriptcomposes the same read models —GetChapter,ListScenes,ListSceneSteps— so playback and editing never see different content. TheToPlayback()mapping on the DTO keeps the domain pure.PlayerReducerscursor discipline is airtight —OnAdvancedclamps toFrames.Count(end card), stale answers for a navigated-away chapter are ignored (ChapterIdguard), and the tests verify every edge including the stray-click-past-the-end case.PlayerEffects.OnDomainChangesAsyncmatches the sibling pattern exactly —ChaptersState,SceneStepsState, andWorkspaceEffectsall do the sameTouches+Loaded+ChapterId != Guid.Emptyguard →Dispatch(new Load...). Consistent.PlayerStage.razoris honest about what it doesn't have — no generated sprite → labeled stand-in with character name and expression. No background → pending placeholder. This is the right call for a debug preview: it never lies about what the real player would show.SceneStageOptionsextension is additive — addingExpressionId,LocationId,SpriteUpdatedAt,ImageUpdatedAtto the existing records. The editor's stage picker still works (it ignores the new fields); the player uses them for gated, cache-busted URLs. No breaking change to siblings.SceneScript.razorscroll-to-line uses the existingkaguraQuicklink.scrollToJS function (already used byQuicklinkNav.razor), and_scrolledTotracks the target so it only scrolls once per target — the user can scroll away without being yanked back.stopPropagationwrapping on play buttons inChapterScenes.razorandChaptersPage.razor— the no-op@onclick="() => { }"handler makesstopPropagationstick so the play link doesn't also trigger the row click. The comment documents why.💡 Little ideas (non-blocking)~
PlayerStage.razor:OnKey— Space key scrolls the page — The handler responds to" "(Space) to advance, but there's no@onkeydown:preventDefault. When the stage div has focus and the user presses Space, the browser also scrolls the page down alongside advancing the line. For a debug preview this is low-impact (the stage re-renders immediately), and adding@onkeydown:preventDefaultto the@onkeydownattribute would prevent it. Not blocking because the player is a debug tool and the visual feedback is immediate — but worth a quick polish if you ever touch this again.PlayerEffectsbranch coverage is 50% — theOnDomainChangesAsyncreload path (whentouchesis true and the player is loaded) appears to have no unit test exercising it directly (thePlayerReducersTestscover reducers, not effects). ThePlayerPageTestsexercise the load path but not the domain-changed reload. Consider adding a test that dispatches aDomainChangesReceivedtouchingSceneStepwhile the player is loaded and verifies a reload holds the cursor on the current line. Not blocking — the logic is simple and follows the exact sibling pattern — but it's the one untested branch in an otherwise exhaustively-tested PR.fufu~ this is genuinely excellent work. The domain purity, the shared stage fold, the honest rendering, the stale-link fallback — every piece shows care. Ship it~ ♡♪
Automated review by Jibril · 2026-07-13
CI/CD: passed for head SHA
c8ea4ae(93.1% line / 75.9% branch overall; new player code 90-100% line coverage) · Local checks: skipped (CI green with coverage comment)Thanks Jibril~ both ideas addressed in
bf5f90f:Space key — taken, but with a different mechanism than suggested.
@onkeydown:preventDefaultin Blazor is all-or-nothing per element: it would also swallow Tab and trap keyboard focus on the stage, which is worse than the scroll nudge. Since the on-screen hint only ever advertised "Enter / →", Space is simply no longer an advance key — its browser default (scroll) now happens without a competing advance. The comment onOnKeyrecords the reasoning so the next reader doesn't re-add it.OnDomainChangesAsynccoverage — addedAnother_sessions_edit_reloads_the_playthrough_holding_the_cursor_on_its_line: the player advances to line 2, another session rewrites that very line, aDomainChangesReceivedtouchingSceneStepis dispatched, and the test asserts the new text renders while the counter still reads "line 2 of 2" — so both the reload branch and the hold-the-cursor seek are exercised end to end.Full suite green (1,401).