Final Verification Wave found two real blockers: 1. T58 RequestChoiceModal.tsx was marked complete but did NOT exist on disk. 2. request-choice locked 6-kind enum was shipped as 5 (missing 'coin-flip'). Remediation: - Build RequestChoiceModal.tsx with role=dialog, aria-modal=true, ESC/backdrop close, kind-specific input UI for all 6 kinds (rps / coin-flip / piece / square / column / row); 4 tests - Add 'coin-flip' to: - request-choice primitive paramsSchema enum - PendingChoice.kind union (schema.ts + util/pending-choices.ts) - WS protocol ChoiceKindSchema (server/protocol.ts) - choice-timeout.firstDefaultForKind (defaults to 'heads') - broadcast.isValidChoiceValue (accepts 'heads' | 'tails') - AutoChoiceResolver: deterministic alternating heads/tails for coin-flip T68 e2e tests remain .skip()'d pending UI integration (modal-into-GameView wiring + activate-descriptor UI) — a follow-up task. The sentinel test asserts the gap exists so when integration lands, skips lift in the documented order. Tests: 2740 -> 2744 (+4). bun run check exit 0.
65 lines
3.1 KiB
Markdown
65 lines
3.1 KiB
Markdown
# Architectural Decisions — Visual Modifier Builder
|
||
|
||
## DSL Extensions
|
||
|
||
### `PrimitiveApplyContext` extension (T1)
|
||
- Add `target?: TargetResolver` (default `'self'` if omitted)
|
||
- Add `event?: PrimitiveEvent` (carries trigger-specific metadata)
|
||
- TargetResolver = `'self' | 'attacker' | 'defender' | { squares: Square[] } | { relation: 'ally' | 'enemy', filter?: { pieceType?: PieceType } }`
|
||
- Centralized resolver: `resolveTargets(ctx, target): readonly EntityId[]` — returns `[ctx.pieceId]` for default 'self'
|
||
- Existing 14 primitives: keep using `ctx.pieceId` directly when target is 'self' (default) — byte-identical behavior
|
||
|
||
### Trigger semantic decisions (locked)
|
||
- `on-promotion`: fires AFTER PieceType flip. event = `{promotedFrom: 'pawn', promotedTo: PieceType}`
|
||
- `on-check-received`: edge-triggered (transition only); royal pieces only
|
||
- `on-check-delivered`: attributes to revealing piece in discovered check
|
||
- `on-move`: fires whenever Position WME changes (captures fire BOTH on-capture and on-move)
|
||
- `on-moved-onto-square`: discriminated union `{kind:'squares', squares[]} | {kind:'predicate', file?, rank?}`
|
||
- `on-captured`: fires BEFORE retraction; nested primitives have access to defender attrs via `ctx.event = {attackerId, defenderId}`
|
||
- `on-turn-end`: fires for mover at end of their turn, BEFORE opponent's on-turn-start
|
||
|
||
## UI Decisions
|
||
|
||
### Mode toggle (T22)
|
||
- localStorage key: `houserules:custom-modifier-editor-mode:v1`
|
||
- Default: 'form' (preserves existing behavior on fresh install)
|
||
- Toggle in editor header next to Templates/Load/Save
|
||
- Visual mode replaces center+right panels (palette stays in left sidebar)
|
||
|
||
### dnd-kit choice (T18)
|
||
- Use `@dnd-kit/core + sortable + utilities` (3 packages)
|
||
- `PointerSensor` + `KeyboardSensor` (a11y mandatory)
|
||
- `DragOverlay` for smooth nested drag preview
|
||
- Each trigger's children list = nested SortableContext
|
||
- Depth-4 drop attempt = no-op + toast (use existing Sonner)
|
||
|
||
### Preview pane (T17)
|
||
- 3 tabs: Narrative / JSON / Board
|
||
- All 3 views memoized on descriptor identity
|
||
- BoardDiagramView shows squares from on-moved-onto-square filters, aura radius, poisoned squares
|
||
- Empty descriptor = "No board effect" placeholder
|
||
- Plain pre/JSON formatting (no react-syntax-highlighter — bundle bloat)
|
||
|
||
### `narrate.ts` (T15) — pure module
|
||
- Zero session/engine access
|
||
- Static kind→narrator function map (no PRIMITIVE_REGISTRY lookups)
|
||
- Cycle guard via visited Set
|
||
- Length cap 4000 chars with truncation suffix
|
||
- Perf budget: <1ms on 50-node descriptor
|
||
|
||
## Test Strategy
|
||
|
||
### TDD per primitive (T5–T11)
|
||
- RED: write test first, runs but fails
|
||
- GREEN: minimal implementation to pass
|
||
- One commit per primitive (atomic)
|
||
|
||
### Pure refactor (T14 — ParamField)
|
||
- Snapshot test BEFORE extraction (golden DOM)
|
||
- Extract verbatim from CustomModifierEditor.tsx:625-872
|
||
- Snapshot test AFTER extraction must match byte-equal
|
||
- Existing e2e tests must pass unchanged
|
||
|
||
### Backward compat (T20)
|
||
- Fixture: descriptor using ONLY 15 legacy kinds, includes 2 nested triggers
|
||
- 4 round-trip assertions: parse, validate, apply, serialize → byte-equal
|