refactor: move the draft into a closure and widen the screen contexts - #68
Merged
Merged
Conversation
**The play screen owns its own state (#42, play half).** `currentDraft`, `scenarioRecallOpen` and `lastSessionId` were module-level `let`s — the app's only mutable state outside the store — because every `renderPlay` call was a fresh invocation that had to find the draft where the last one left it. `renderPlay` now creates that state itself and repaints its own subtree. Four pieces of machinery existed only to work around the old arrangement, and all four are gone: - `ensureDraftFor` and `lastSessionId`, which detected a session change by hand. A new letter is now a new `renderPlay` call, so there is nothing to detect. - The reset-before-`onUpdate` ordering, whose comment warned that getting it backwards stranded the player until they reloaded. Committing discards the whole closure, so there is no ordering left to sequence. - `PlayCtx.repaint`, which pointed at `main.ts`'s full-tree render. The two channels are now named apart: `main.ts`'s `commit()` is durable, the play screen's internal `repaint()` is volatile. - `onUpdate`, a general "mutate the session however you like" hook, narrowed to `onCommit(paragraph)`. Committing is the screen's only store write. The render helpers take a per-invocation view rather than the raw context. `state` is one shared object per letter, deliberately: the roll button's 250ms shake timer reads `v.state.draft` when it fires, not a copy captured when its button was built, so a repaint mid-shake cannot land a roll on a draft nothing will ever see. **Screens stop re-resolving what `main.ts` proved (#46).** `PlayCtx` and `ScoreCtx` carry the resolved `character` and `skill`. Seven `characterById`/ `skillById` calls inside `src/screens/` go, along with the fallbacks that invented five different answers to a question already answered — `'skill'`, `''`, `'The Correspondent'`, an `internalError` panel, and a thrown `Error`. `toMarkdown` takes the resolved pair instead of the whole `CHARACTERS` and `SKILLS` arrays, dropping its own `find` calls and its throw. Two comments corrected while in the file: `attachRollButton` named the recall toggle as a repaint cause, which stopped being true when that toggle became an in-place mutation; and the recall toggle's own comment still claimed a repaint would rewrite localStorage, which stopped being true in the previous milestone. Verified in the browser, since none of this has automated coverage: a fresh letter through a committed paragraph; the recall toggle leaving the roll button node untouched; spending the skill repainting and reading "5 dice, skill applied"; zero localStorage writes across every phase transition and exactly one for the commit; the screen advancing to a fresh "Paragraph II of V" rather than re-showing PARAGRAPH_DONE; and a mid-letter reload restoring the committed paragraph with the skill still spent. `src/screens/play.ts` 709 → 695 lines, with no module-level state left. Closes #42 Closes #46 Co-Authored-By: Claude
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #42 and #46. Second and last PR of milestone 4.
The play screen owns its own state (#42, play half)
currentDraft,scenarioRecallOpenandlastSessionIdwere module-levellets — the app's only mutable state outside the store — because everyrenderPlaycall was a fresh invocation that had to find the draft where the last one left it.renderPlaynow creates that state itself and repaints its own subtree.Four pieces of machinery existed only to work around that, and all four are gone:
ensureDraftForandlastSessionId, which detected a session change by hand. A new letter is now a newrenderPlaycall, so there is nothing to detect.onUpdateordering, whose own comment warned that getting it backwards stranded the player on the done screen until they reloaded. Committing discards the whole closure — there is no ordering left to sequence, so the comment is deleted rather than preserved.PlayCtx.repaint, which pointed atmain.ts's full-tree render. The two channels are now named apart:main.ts'scommit()is durable, the play screen's internalrepaint()is volatile.onUpdate— a general "mutate the session however you like" hook — narrowed toonCommit(paragraph). Committing is the screen's only store write.The trap, handled deliberately. The render helpers take a per-invocation view, and
stateis one shared object per letter rather than a value copied into each render. That matters for exactly one thing: the roll button's 250ms shake timer readsv.state.draftwhen it fires, not a copy captured when its button was built. Capturing it would mean a repaint mid-shake lands the roll on a draft nothing will ever see — which is the bug the module-levelletwas accidentally avoiding.Screens stop re-resolving what
main.tsproved (#46)PlayCtxandScoreCtxcarry the resolvedcharacterandskill. SevencharacterById/skillByIdcalls insidesrc/screens/go, along with the fallbacks that invented five different answers to a question already answered:'skill','','The Correspondent', aninternalErrorpanel, and a thrownError.toMarkdowntakes the resolved pair instead of the wholeCHARACTERS/SKILLSarrays, dropping its ownfindcalls and its throw.Verification
bun run check:ciclean, 77 tests. None of this has automated coverage, so it was driven in the browser:Roll Heart (5 dice, skill applied)"Dip your quill…"/Paragraph II of V— a fresh draft, not a re-shown PARAGRAPH_DONEAugmentation spentstill latched, fresh draft at PICK_WORDThe Courtier — Augmentation unspentfromv.character/v.skillThat fifth row is the one worth naming: it's the failure the deleted comment warned about, and it's now structurally impossible rather than avoided by statement order.
Also
Two comments corrected while in the file —
attachRollButtonnamed the recall toggle as a repaint cause, which stopped being true when that toggle became an in-place mutation; and the recall toggle's own comment still claimed a repaint would rewrite localStorage, which stopped being true in milestone 3.src/screens/play.ts709 → 695 lines, no module-level state left.THEORY.mdandWALKTHROUGH.mduntouched, per the standing rule.Merging closes milestone 4.