1145 lines
39 KiB
Markdown
1145 lines
39 KiB
Markdown
# Story 3.1: Save & Load Scene Presets
|
||
|
||
**Status:** ready-for-dev
|
||
|
||
**Epic:** 3 - Scene-Aware Camera Automation (Scene Presets)
|
||
|
||
**Story Key:** 3-1-save-and-load-scene-presets
|
||
|
||
**Created:** 2026-05-23
|
||
|
||
**Last Updated:** 2026-05-23
|
||
|
||
---
|
||
|
||
## Story Header
|
||
|
||
| Field | Value |
|
||
|-------|-------|
|
||
| **Epic** | 3 - Scene-Aware Camera Automation (Scene Presets) |
|
||
| **Story ID** | 3.1 |
|
||
| **Story Key** | 3-1-save-and-load-scene-presets |
|
||
| **Title** | Save & Load Scene Presets |
|
||
| **Status** | ready-for-dev |
|
||
| **Priority** | High (first story in Epic 3) |
|
||
| **Assigned Agent** | DEV (Amelia) |
|
||
| **Created** | 2026-05-23 |
|
||
|
||
---
|
||
|
||
## 📋 Story Requirements
|
||
|
||
### User Story
|
||
|
||
**As a** GM,
|
||
**I want to** save the current camera layout as a named preset and load it at any time,
|
||
**So that** I can instantly reproduce proven camera arrangements without reconfiguring them from scratch.
|
||
|
||
### Persona Alignment
|
||
|
||
- **Primary:** Marcus (Veteran GM) - Preps meticulously, wants zero friction during play, needs camera automation he can configure once
|
||
- **Secondary:** Jake (Streamer) - Needs complete independence between scenes for broadcast production
|
||
|
||
### Acceptance Criteria (BDD Format)
|
||
|
||
#### AC-1: Save Preset via Director's Board
|
||
**Given** the Director's Board is open
|
||
**When** the GM clicks "Save Preset…" in the board footer
|
||
**Then** a prompt appears for a preset name
|
||
**And** on confirmation, the current Visibility Matrix is captured and stored on the current Scene document flag `{ _version: 1, presets: {...} }`
|
||
|
||
#### AC-2: Duplicate Name Handling
|
||
**Given** a preset name already exists
|
||
**When** the GM saves with the same name
|
||
**Then** the GM is asked to confirm overwrite before the preset is replaced
|
||
|
||
#### AC-3: Maximum Presets Limit
|
||
**Given** the world already has 50 presets
|
||
**When** the GM attempts to save a 51st
|
||
**Then** an error message shows: "Maximum of 50 presets reached. Delete an existing preset to save a new one."
|
||
|
||
#### AC-4: Load Preset via Director's Board
|
||
**Given** saved presets exist
|
||
**When** the GM clicks "Load Preset…" in the Director's Board footer
|
||
**Then** a list of available presets is shown
|
||
**And** selecting one overwrites the current Visibility Matrix and broadcasts to all clients within 500ms
|
||
|
||
#### AC-5: Notification on Preset Load
|
||
**Given** a preset is loaded
|
||
**When** all clients receive the broadcast
|
||
**Then** a notification fires: "GM applied preset: [Preset Name]" via `ui.notifications`
|
||
|
||
#### AC-6: Offline Participant State Persistence
|
||
**Given** a participant is offline when a preset is loaded
|
||
**When** they reconnect
|
||
**Then** they receive the state from the loaded preset (not the previous live state)
|
||
|
||
#### AC-7: Rename Preset Validation
|
||
**Given** the GM renames a preset
|
||
**When** the new name conflicts with an existing preset
|
||
**Then** an error is shown and the rename is rejected
|
||
|
||
### Functional Requirements Covered
|
||
|
||
- **FR-15:** GM saves a named Scene Preset from the current Visibility Matrix
|
||
- **FR-16:** GM loads a Scene Preset at any time, overriding the current Visibility Matrix
|
||
|
||
### Success Criteria
|
||
|
||
- [ ] All 7 acceptance criteria pass manual testing
|
||
- [ ] All unit tests pass (target: +40-50 new tests)
|
||
- [ ] `npm run lint` exits 0
|
||
- [ ] `npm run typecheck` exits 0
|
||
- [ ] Code review passes with no critical findings
|
||
|
||
---
|
||
|
||
## 🎯 Developer Context Section
|
||
|
||
### Epic Context
|
||
|
||
**Epic 3: Scene-Aware Camera Automation (Scene Presets)** builds on the foundation established in Epics 1 and 2:
|
||
|
||
- **Epic 1** delivered core visibility control (right-click toggle, state persistence, AV tile indicators)
|
||
- **Epic 2** delivered player notifications and the Director's Board (bulk actions, keyboard shortcuts)
|
||
- **Epic 3** extends this with **scene-aware automation** - presets that can be saved, loaded manually, and eventually auto-applied on scene transitions
|
||
|
||
This epic represents **Level 3** of the Progressive Enhancement Architecture, moving from manual control to automation.
|
||
|
||
### Cross-Epic Dependencies
|
||
|
||
| Dependency | Source Epic | Status |
|
||
|------------|-------------|--------|
|
||
| Visibility Matrix persistence | Epic 1 (Story 1.3) | ✅ Complete |
|
||
| StateStore.setMatrix() method | Epic 1 (Story 1.4) | ✅ Available |
|
||
| Director's Board UI | Epic 2 (Story 2.2) | ✅ Complete |
|
||
| Bulk operation patterns | Epic 2 (Story 2.3) | ✅ Available |
|
||
| NotificationBus | Epic 2 (Story 2.1) | ✅ Available |
|
||
| Socket broadcast infrastructure | Epic 1 (Story 1.3) | ✅ Available |
|
||
|
||
### Story Sequence in Epic 3
|
||
|
||
```
|
||
Story 3.1 (Save & Load) → Story 3.2 (Auto-Apply & ConfirmationBar) → Story 3.3 (Import/Export)
|
||
```
|
||
|
||
**This is Story 3.1** - the foundation for the entire Epic 3 feature set. Stories 3.2 and 3.3 build directly on the ScenePresetManager created here.
|
||
|
||
### Business Value
|
||
|
||
- **For Marcus (Veteran GM):** Pre-configure camera layouts during prep, apply instantly during sessions without disrupting flow
|
||
- **For Jake (Streamer):** Professional scene transitions with consistent camera states for broadcast
|
||
- **For All GMs:** Reduces cognitive load during sessions by automating repetitive setup
|
||
|
||
---
|
||
|
||
## 🏗️ Technical Requirements
|
||
|
||
### Core Components to Create
|
||
|
||
| Component | File | Purpose |
|
||
|-----------|------|---------|
|
||
| ScenePresetManager | `src/core/ScenePresetManager.js` | Manages preset CRUD, Scene flag storage |
|
||
| PresetSaveDialog | `src/ui/gm/PresetSaveDialog.js` | Modal for preset name entry |
|
||
| PresetLoadDialog | `src/ui/gm/PresetLoadDialog.js` | Modal for preset selection |
|
||
| PresetManager UI | Extend DirectorsBoard footer | Save/Load buttons and preset list |
|
||
|
||
### Data Flow
|
||
|
||
```
|
||
GM Action (Save) → ScenePresetManager.save() → Scene.setFlag() → Confirmation
|
||
↓
|
||
GM Action (Load) → ScenePresetManager.load() → StateStore.setMatrix() → Socket broadcast
|
||
↓
|
||
→ NotificationBus → ui.notifications
|
||
```
|
||
|
||
### Persistence Strategy
|
||
|
||
**Storage Location:** Scene document flags (not world settings)
|
||
- **Key:** `scene.getFlag('video-view-manager', 'presets')`
|
||
- **Schema:** `{ _version: 1, presets: { [name: string]: ScenePreset } }`
|
||
- **Rationale:** Presets are Scene-specific; different scenes need different camera layouts
|
||
|
||
**ScenePreset Structure:**
|
||
```javascript
|
||
{
|
||
_version: 1,
|
||
name: "string (unique)",
|
||
matrix: { userId: VisibilityState, ... },
|
||
createdAt: number (timestamp),
|
||
updatedAt: number (timestamp)
|
||
}
|
||
```
|
||
|
||
### Socket Messages (New Events)
|
||
|
||
Based on existing contract in `src/contracts/socket-message.js`:
|
||
- `PRESET_APPLY: "scrying-pool.preset.apply"` - GM requests preset apply
|
||
- `PRESET_APPLIED: "scrying-pool.preset.applied"` - Authoritative confirmation
|
||
|
||
**Note:** These events are already defined in the contract but not yet implemented in SocketHandler.
|
||
|
||
---
|
||
|
||
## 🏛️ Architecture Compliance
|
||
|
||
### Import Boundary Rules (HARD - ESLint Enforced)
|
||
|
||
```
|
||
src/core/ScenePresetManager.js → may import: src/contracts/, src/utils/ ONLY
|
||
src/ui/gm/PresetSaveDialog.js → may import: src/core/, src/contracts/, src/utils/ ONLY
|
||
src/ui/gm/PresetLoadDialog.js → may import: src/core/, src/contracts/, src/utils/ ONLY
|
||
```
|
||
|
||
**❌ FORBIDDEN:**
|
||
- No imports from `src/foundry/` inside `src/core/` or `src/ui/`
|
||
- No direct `game.*` access in testable modules
|
||
- No circular dependencies
|
||
|
||
### Constructor Pattern (Side-Effect-Free)
|
||
|
||
All new classes MUST follow the established pattern:
|
||
```javascript
|
||
export class ScenePresetManager {
|
||
constructor(adapter, stateStore, socketHandler) {
|
||
// Store dependencies, NO side effects
|
||
this._adapter = adapter;
|
||
this._stateStore = stateStore;
|
||
this._socketHandler = socketHandler;
|
||
// Initialize internal state
|
||
this._presets = new Map();
|
||
}
|
||
|
||
init() {
|
||
// Register hooks, load initial state
|
||
}
|
||
|
||
teardown() {
|
||
// Clean up hooks, timers, listeners
|
||
}
|
||
}
|
||
```
|
||
|
||
### Dependency Injection Contract
|
||
|
||
**Required Injections for ScenePresetManager:**
|
||
```javascript
|
||
{
|
||
scenes: {
|
||
current: () => Scene|null,
|
||
getFlag: (sceneId, key) => any,
|
||
setFlag: (sceneId, key, value) => Promise<any>
|
||
},
|
||
users: {
|
||
isGM: () => boolean,
|
||
current: () => User|null,
|
||
all: () => User[]
|
||
},
|
||
hooks: {
|
||
on: (event, handler) => number,
|
||
off: (event, handler) => void,
|
||
callAll: (event, data) => void
|
||
}
|
||
}
|
||
```
|
||
|
||
### State Authority
|
||
|
||
- **ScenePresetManager** owns preset CRUD operations
|
||
- **StateStore** owns Visibility Matrix state (ScenePresetManager calls StateStore.setMatrix())
|
||
- **SocketHandler** owns socket broadcast (ScenePresetManager calls SocketHandler.emit())
|
||
- **ScryingPoolController** owns revision tracking and PendingOp lifecycle
|
||
|
||
---
|
||
|
||
## 📦 Library & Framework Requirements
|
||
|
||
### Existing Dependencies (Already Available)
|
||
|
||
| Library | Version | Purpose |
|
||
|---------|---------|---------|
|
||
| FoundryVTT v14 API | N/A | Native module API |
|
||
| Font Awesome 6 | Bundled | Icons |
|
||
| Handlebars | Bundled | Templates |
|
||
| LESS 4.6.4 | Bundled | CSS preprocessing |
|
||
|
||
### New Dependencies
|
||
|
||
**None** - This story uses only existing FoundryVTT v14 APIs and the module's existing infrastructure.
|
||
|
||
### Contract Files (Already Exist)
|
||
|
||
- ✅ `src/contracts/scene-preset.js` - Already implemented with createScenePreset(), isValidScenePreset()
|
||
- ✅ `src/contracts/socket-message.js` - Already defines PRESET_APPLY and PRESET_APPLIED events
|
||
- ✅ `src/contracts/visibility-matrix.js` - Already defines matrix validation
|
||
|
||
---
|
||
|
||
## 📁 File Structure Requirements
|
||
|
||
### New Files to Create
|
||
|
||
```
|
||
📁 src/core/
|
||
└── ScenePresetManager.js # NEW - Core preset management
|
||
|
||
📁 src/ui/gm/
|
||
├── PresetSaveDialog.js # NEW - Save preset modal
|
||
└── PresetLoadDialog.js # NEW - Load preset modal
|
||
|
||
📁 tests/unit/core/
|
||
└── ScenePresetManager.test.js # NEW - Unit tests
|
||
|
||
📁 tests/unit/ui/gm/
|
||
├── PresetSaveDialog.test.js # NEW - Unit tests
|
||
└── PresetLoadDialog.test.js # NEW - Unit tests
|
||
|
||
📁 templates/
|
||
├── preset-save-dialog.hbs # NEW - Handlebars template
|
||
└── preset-load-dialog.hbs # NEW - Handlebars template
|
||
|
||
📁 styles/components/
|
||
├── _preset-save-dialog.less # NEW - Dialog styles
|
||
└── _preset-load-dialog.less # NEW - Dialog styles
|
||
```
|
||
|
||
### Modified Files
|
||
|
||
```
|
||
📄 module.js # Add ScenePresetManager wiring
|
||
📄 src/ui/gm/DirectorsBoard.js # Add Save/Load buttons to footer
|
||
📄 templates/directors-board.hbs # Enable Save/Load buttons
|
||
📄 lang/en.json # Add i18n keys for preset UI
|
||
📄 styles/components/_directors-board.less # Style footer buttons
|
||
```
|
||
|
||
### File Ownership & Responsibilities
|
||
|
||
| File | Owns | Collaborates With |
|
||
|------|------|------------------|
|
||
| ScenePresetManager.js | Preset CRUD, Scene flag storage | StateStore, SocketHandler, FoundryAdapter |
|
||
| PresetSaveDialog.js | Name input, validation, confirmation | ScenePresetManager |
|
||
| PresetLoadDialog.js | Preset listing, selection, load confirmation | ScenePresetManager |
|
||
| DirectorsBoard.js | UI integration, button wiring | ScenePresetManager |
|
||
|
||
---
|
||
|
||
## 🧪 Testing Requirements
|
||
|
||
### Unit Test Coverage Targets
|
||
|
||
| Module | Test File | Target Tests | Coverage Focus |
|
||
|--------|-----------|---------------|----------------|
|
||
| ScenePresetManager | ScenePresetManager.test.js | 30-40 | CRUD, validation, edge cases |
|
||
| PresetSaveDialog | PresetSaveDialog.test.js | 15-20 | Input validation, confirmation, cancel |
|
||
| PresetLoadDialog | PresetLoadDialog.test.js | 15-20 | Listing, selection, load, cancel |
|
||
| **Total** | | **60-80** | All public methods, edge cases |
|
||
|
||
### Test Patterns to Follow
|
||
|
||
**From Story 2.1 (NotificationBus):**
|
||
```javascript
|
||
// Use vi.useFakeTimers() for any time-based logic
|
||
beforeEach(() => vi.useFakeTimers());
|
||
afterEach(() => vi.useRealTimers());
|
||
```
|
||
|
||
**From Story 2.2 (DirectorsBoard):**
|
||
```javascript
|
||
// Conditional base class pattern for ApplicationV2 test compatibility
|
||
const _AppBase = typeof foundry !== 'undefined' ? ... : class _FallbackApp { ... };
|
||
```
|
||
|
||
**From Story 2.3 (Bulk Actions):**
|
||
```javascript
|
||
// Mock all dependencies with vi.fn()
|
||
const adapter = {
|
||
scenes: { current: vi.fn(), getFlag: vi.fn(), setFlag: vi.fn() },
|
||
users: { isGM: vi.fn(() => true), current: vi.fn(), all: vi.fn() },
|
||
hooks: { on: vi.fn(() => 42), off: vi.fn(), callAll: vi.fn() }
|
||
};
|
||
```
|
||
|
||
### Test Categories
|
||
|
||
**ScenePresetManager Tests:**
|
||
- [ ] Constructor validation (throws on invalid adapter)
|
||
- [ ] init() - loads presets from current scene
|
||
- [ ] teardown() - cleans up hooks and state
|
||
- [ ] save() - creates preset, persists to scene flag
|
||
- [ ] save() - duplicate name detection
|
||
- [ ] save() - max presets (50) enforcement
|
||
- [ ] load() - applies preset matrix via StateStore.setMatrix()
|
||
- [ ] load() - emits socket message
|
||
- [ ] delete() - removes preset from scene flag
|
||
- [ ] rename() - updates preset name with conflict detection
|
||
- [ ] list() - returns all presets for current scene
|
||
- [ ] Validation - all inputs validated before processing
|
||
|
||
**PresetSaveDialog Tests:**
|
||
- [ ] Renders with name input field
|
||
- [ ] Validates preset name (non-empty, no special chars?)
|
||
- [ ] Confirms overwrite when duplicate
|
||
- [ ] Calls onSave callback with name
|
||
- [ ] Calls onCancel callback
|
||
- [ ] Keyboard support (Enter to save, Escape to cancel)
|
||
- [ ] Accessibility (focus trap, ARIA labels)
|
||
|
||
**PresetLoadDialog Tests:**
|
||
- [ ] Renders list of presets
|
||
- [ ] Handles empty preset list
|
||
- [ ] Selection triggers load
|
||
- [ ] Calls onLoad callback with preset name
|
||
- [ ] Calls onCancel callback
|
||
- [ ] Keyboard navigation (arrow keys, Enter)
|
||
- [ ] Accessibility (focus trap, ARIA labels)
|
||
|
||
### Integration Testing
|
||
|
||
**Critical:** Based on Epic 2 retrospective findings, add explicit integration tests:
|
||
- [ ] ScenePresetManager.save() produces matrix compatible with StateStore.setMatrix()
|
||
- [ ] Preset loading triggers correct socket events
|
||
- [ ] NotificationBus receives and processes preset-applied events
|
||
|
||
---
|
||
|
||
## 🔄 Previous Story Intelligence
|
||
|
||
**No previous stories in Epic 3** - This is the first story.
|
||
|
||
### Lessons from Epic 2 to Apply
|
||
|
||
**✅ DO REPEAT:**
|
||
|
||
1. **TDD Discipline (Stories 2.1-2.3)**
|
||
- Write failing tests first (TDD red)
|
||
- Implement to pass tests (TDD green)
|
||
- Refactor while tests pass (TDD refactor)
|
||
- **Pattern:** All stories had 20+ tests each
|
||
|
||
2. **Side-Effect-Free Constructors (All Stories)**
|
||
- No hooks registration in constructor
|
||
- No state mutation in constructor
|
||
- init() method for setup, teardown() for cleanup
|
||
- **Pattern:** Enabled reliable testing
|
||
|
||
3. **Import Boundary Enforcement (All Stories)**
|
||
- ESLint `import/no-restricted-paths` catches violations
|
||
- Strict layer separation (core/ → contracts/, utils/)
|
||
- **Pattern:** Zero boundary violations in Epic 2
|
||
|
||
4. **Deferred Debt Proactive Resolution (Story 2.1)**
|
||
- Addressed 4 deferred items from Epic 1
|
||
- Prevented debt accumulation
|
||
- **Pattern:** Address deferred work early
|
||
|
||
5. **Event Delegation on Root (Story 2.2, 2.3)**
|
||
- Single listener on app root element
|
||
- Uses `data-action` attributes for routing
|
||
- Survives re-renders
|
||
- **Pattern:** Robust event handling
|
||
|
||
**⚠️ DO NOT REPEAT:**
|
||
|
||
1. **Cross-Story Interface Mismatches (Story 2.2 → 2.3)**
|
||
- `_dispatchToggle()` used wrong calling convention
|
||
- Caught in Story 2.3, cost development time
|
||
- **Mitigation:** Add integration test step to checklist
|
||
|
||
2. **Late Lifecycle Management (All Stories)**
|
||
- `teardown()` methods added as fixes
|
||
- Should be designed in from start
|
||
- **Mitigation:** Include teardown in class design template
|
||
|
||
3. **Race Conditions in Bulk Operations (Story 2.3)**
|
||
- State capture wasn't atomic
|
||
- Caused inconsistent snapshots
|
||
- **Mitigation:** Always capture complete state snapshot before iteration
|
||
|
||
### Patterns Established in Epic 2
|
||
|
||
1. **Bulk Operation Pattern**
|
||
```javascript
|
||
// Capture snapshot first
|
||
const snapshot = new Map(users.map(u => [u.id, this._stateStore.getState(u.id)]));
|
||
// Then iterate
|
||
for (const user of users) {
|
||
const targetState = snapshot.get(user.id);
|
||
this._controller.action('source', user.id, targetState, opId, baseRevision);
|
||
}
|
||
```
|
||
|
||
2. **Undo/Restore State Machine**
|
||
```javascript
|
||
this._undoSnapshot = null; // Initial state
|
||
// After bulk action:
|
||
this._undoSnapshot = new Map(...);
|
||
// After undo:
|
||
this._undoSnapshot = null; // Single-step only
|
||
```
|
||
|
||
3. **Atomic State Capture**
|
||
```javascript
|
||
// WRONG: States may change between calls
|
||
for (const user of users) {
|
||
const state = this._stateStore.getState(user.id); // ❌ Race condition
|
||
}
|
||
|
||
// RIGHT: Capture all states first
|
||
const allStates = users.map(u => this._stateStore.getState(u.id));
|
||
for (let i = 0; i < users.length; i++) {
|
||
const state = allStates[i]; // ✅ Consistent snapshot
|
||
}
|
||
```
|
||
|
||
---
|
||
|
||
## 📊 Git Intelligence Summary
|
||
|
||
### Recent Commit Patterns
|
||
|
||
```
|
||
25b98ce 2026-05-23 Mark Story 2.3 code review as done
|
||
cc5b04d 2026-05-23 Update sprint status: Story 2.3 code review completed and merged
|
||
7918792 2026-05-23 Fix Story 2.3 code review findings
|
||
- Removed duplicate ParticipantCard.js
|
||
- Fixed lint in ScryingPoolStrip.js
|
||
```
|
||
|
||
### Code Patterns from Recent Commits
|
||
|
||
1. **Import Boundary Enforcement**
|
||
- ESLint catches violations at lint time
|
||
- No runtime enforcement needed
|
||
|
||
2. **Test File Structure**
|
||
- Mirror source structure: `tests/unit/{path}/*.test.js`
|
||
- Use `vi.fn()` for mocks
|
||
- Use `beforeEach`/`afterEach` for setup/teardown
|
||
|
||
3. **Error Handling**
|
||
- Validate all inputs
|
||
- Use TypeError for invalid arguments
|
||
- Wrap external calls in try-catch
|
||
|
||
4. **Code Review Findings Pattern**
|
||
- Most issues caught in first review
|
||
- Typical fixes: import boundary violations, missing validation, lint errors
|
||
|
||
---
|
||
|
||
## 🌐 Latest Technical Information
|
||
|
||
### FoundryVTT v14 API Considerations
|
||
|
||
**Scene Flags:**
|
||
- `Scene.setFlag(scope, key, value)` - Async, returns Promise
|
||
- `Scene.getFlag(scope, key)` - Returns value or undefined
|
||
- `Scene.unsetFlag(scope, key)` - Async, removes flag
|
||
- **Scope:** Use `'video-view-manager'` for all module flags
|
||
|
||
**Hooks:**
|
||
- `Hooks.on('updateScene', (scene, data) => {...})` - Fires when scene is activated (Story 3.2 will use this)
|
||
- **Note:** OQ-5 in architecture flags this as a timing concern - need to verify hook fires at correct time
|
||
|
||
**Dialog API:**
|
||
- `new Dialog({ title, content, buttons, default }).render(true)`
|
||
- For simple prompts: `Dialog.prompt({ title, label, callback, reject })`
|
||
- **Pattern:** Use native Foundry Dialog, not custom implementations
|
||
|
||
### Contract Validation
|
||
|
||
**ScenePreset Contract (Already Implemented):**
|
||
```javascript
|
||
// src/contracts/scene-preset.js
|
||
- SCENE_PRESET_VERSION = 1
|
||
- MAX_PRESETS_PER_WORLD = 50
|
||
- createScenePreset(name, matrix, now)
|
||
- isValidScenePreset(data) // Throws TypeError on violation
|
||
```
|
||
|
||
**Socket Message Contract (Already Implemented):**
|
||
```javascript
|
||
// src/contracts/socket-message.js
|
||
- SOCKET_EVENTS.PRESET_APPLY = "scrying-pool.preset.apply"
|
||
- SOCKET_EVENTS.PRESET_APPLIED = "scrying-pool.preset.applied"
|
||
- MAX_PAYLOAD_BYTES = 4096
|
||
```
|
||
|
||
### Performance Considerations
|
||
|
||
- **Max Presets:** 50 per world (enforced)
|
||
- **Max Payload:** 4096 bytes (enforced by SocketHandler)
|
||
- **Matrix Size:** With 50 participants × ~50 bytes per entry ≈ 2500 bytes (safe)
|
||
- **Scene Flag Size:** 50 presets × ~200 bytes ≈ 10KB per scene (acceptable)
|
||
|
||
---
|
||
|
||
## 📚 Project Context Reference
|
||
|
||
### Source Documents
|
||
|
||
| Document | Location | Relevant Sections |
|
||
|----------|----------|-------------------|
|
||
| PRD | `_bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/prd.md` | FR-15, FR-16, FR-17, FR-18, FR-19 |
|
||
| Architecture | `_bmad-output/planning-artifacts/architecture.md` | Data Architecture, Core Decisions, OQ-5, OQ-6 |
|
||
| Epics | `_bmad-output/planning-artifacts/epics.md` | Epic 3, Story 3.1-3.3, FR Coverage Map |
|
||
| UX Design | `_bmad-output/planning-artifacts/ux-design-specification.md` | UX-DR14, UX-DR15, UX-DR16, UX-DR19 |
|
||
| Epic 1 Retro | `_bmad-output/implementation-artifacts/epic-1-retro-2026-05-22.md` | Action items, lessons learned |
|
||
| Epic 2 Retro | `_bmad-output/implementation-artifacts/epic-2-retro-2026-05-23.md` | Action items, patterns |
|
||
| Deferred Work | `_bmad-output/implementation-artifacts/deferred-work.md` | Open items, technical debt |
|
||
|
||
### Related Stories
|
||
|
||
| Story | Relevance |
|
||
|-------|-----------|
|
||
| 1-3 (Data Layer) | StateStore.setMatrix() method used for preset apply |
|
||
| 1-4 (Core Logic) | ScryingPoolController.action() pattern for state changes |
|
||
| 2-2 (Director's Board) | UI patterns for modal dialogs |
|
||
| 2-3 (Bulk Actions) | Bulk state change patterns, undo logic |
|
||
|
||
### Architecture Decisions to Reference
|
||
|
||
1. **State Persistence Boundary (Architecture §Data Architecture)**
|
||
- World settings: `scrying-pool.visibilityMatrix`
|
||
- Scene flags: `{ _version: 1, presets: {...} }`
|
||
- Client settings: `notificationVerbosity`
|
||
- User flags: `firstBadgeEncounter`, `directorsBoardState`
|
||
|
||
2. **Socket Reconciliation (Architecture §Core Decisions)**
|
||
- PendingOp lifecycle: create → emit → timeout/retry → confirm/revert
|
||
- Authoritative echo pattern
|
||
- 3s timeout, 1 retry, then revert
|
||
|
||
3. **Dependency Injection (Architecture §Structural Constraint)**
|
||
- Testable modules: zero direct `game.*` access
|
||
- All Foundry deps constructor-injected via FoundryAdapter
|
||
|
||
---
|
||
|
||
## ✅ Story Completion Status
|
||
|
||
**Status:** ready-for-dev
|
||
|
||
**Ultimate Context Engine Analysis:** Completed
|
||
|
||
### Developer Readiness Checklist
|
||
|
||
- [x] Story requirements extracted from epics.md
|
||
- [x] Acceptance criteria in BDD format
|
||
- [x] Technical requirements identified
|
||
- [x] Architecture compliance verified
|
||
- [x] Import boundaries defined
|
||
- [x] Constructor patterns established
|
||
- [x] Dependency injection contract defined
|
||
- [x] File structure planned
|
||
- [x] Testing requirements specified
|
||
- [x] Previous story intelligence incorporated
|
||
- [x] Git intelligence analyzed
|
||
- [x] Latest tech information included
|
||
- [x] Project context referenced
|
||
|
||
### Next Steps for Developer
|
||
|
||
1. **Review this story file** thoroughly
|
||
2. **Run** `dev-story` workflow with `story_key=3-1-save-and-load-scene-presets`
|
||
3. **Or manually implement** following the tasks below
|
||
|
||
### Implementation Order (Recommended)
|
||
|
||
```
|
||
1. Create ScenePresetManager.js (Core logic)
|
||
2. Create PresetSaveDialog.js (UI component)
|
||
3. Create PresetLoadDialog.js (UI component)
|
||
4. Extend DirectorsBoard.js (UI integration)
|
||
5. Add templates
|
||
6. Add styles
|
||
7. Add i18n keys
|
||
8. Wire in module.js
|
||
9. Write unit tests
|
||
10. Run lint, typecheck, test
|
||
```
|
||
|
||
---
|
||
|
||
## 📝 Tasks / Subtasks
|
||
|
||
### Task 1: Create `src/core/ScenePresetManager.js`
|
||
|
||
**Files:** `src/core/ScenePresetManager.js`, `tests/unit/core/ScenePresetManager.test.js`
|
||
|
||
**Subtasks:**
|
||
- [ ] 1.1: Write TDD red tests in `ScenePresetManager.test.js` — constructor validation, init, teardown
|
||
- [ ] 1.2: Implement `ScenePresetManager` class with constructor `(adapter, stateStore, socketHandler)`
|
||
- [ ] 1.3: Implement `init()` — registers hooks if needed, loads current scene presets
|
||
- [ ] 1.4: Implement `teardown()` — unregisters hooks, clears state
|
||
- [ ] 1.5: Implement `save(name, matrix)` — validates name, checks max presets, creates preset, saves to scene flag
|
||
- [ ] 1.6: Implement `load(name)` — retrieves preset, applies via StateStore.setMatrix(), emits socket message
|
||
- [ ] 1.7: Implement `delete(name)` — removes preset from scene flag
|
||
- [ ] 1.8: Implement `rename(oldName, newName)` — validates newName, checks conflicts, updates
|
||
- [ ] 1.9: Implement `list()` — returns all presets for current scene
|
||
- [ ] 1.10: Implement `_getScenePresets()` — reads from current scene flag, validates schema
|
||
- [ ] 1.11: Implement `_saveScenePresets(presets)` — writes to current scene flag with validation
|
||
- [ ] 1.12: Green all ScenePresetManager tests
|
||
|
||
**Acceptance Criteria:** AC-1, AC-2, AC-3, AC-4, AC-5, AC-6, AC-7
|
||
|
||
---
|
||
|
||
### Task 2: Create `src/ui/gm/PresetSaveDialog.js`
|
||
|
||
**Files:** `src/ui/gm/PresetSaveDialog.js`, `src/ui/gm/PresetLoadDialog.js`, `tests/unit/ui/gm/PresetSaveDialog.test.js`, `tests/unit/ui/gm/PresetLoadDialog.test.js`
|
||
|
||
**Subtasks:**
|
||
- [ ] 2.1: Write TDD red tests for PresetSaveDialog
|
||
- [ ] 2.2: Implement conditional base class pattern for ApplicationV2 compatibility
|
||
- [ ] 2.3: Implement `PresetSaveDialog` extending base class
|
||
- [ ] 2.4: Implement `static DEFAULT_OPTIONS` and `static PARTS`
|
||
- [ ] 2.5: Implement `constructor(scenePresetManager, adapter)`
|
||
- [ ] 2.6: Implement `async _prepareContext()` — returns context for template
|
||
- [ ] 2.7: Implement `_onRender()` — wire form submit, cancel button
|
||
- [ ] 2.8: Implement `_onSubmit(event)` — validate name, call manager.save(), close on success
|
||
- [ ] 2.9: Implement `_onCancel()` — close dialog
|
||
- [ ] 2.10: Implement keyboard support (Enter=save, Escape=cancel)
|
||
- [ ] 2.11: Implement focus trap for accessibility
|
||
- [ ] 2.12: Green all PresetSaveDialog tests
|
||
|
||
**Acceptance Criteria:** AC-1, AC-2
|
||
|
||
---
|
||
|
||
### Task 3: Create `src/ui/gm/PresetLoadDialog.js`
|
||
|
||
**Files:** `src/ui/gm/PresetLoadDialog.js`, `tests/unit/ui/gm/PresetLoadDialog.test.js`
|
||
|
||
**Subtasks:**
|
||
- [ ] 3.1: Write TDD red tests for PresetLoadDialog
|
||
- [ ] 3.2: Implement `PresetLoadDialog` extending same base class
|
||
- [ ] 3.3: Implement `async _prepareContext()` — calls manager.list(), returns { presets }
|
||
- [ ] 3.4: Implement `_onRender()` — wire preset selection, load button, cancel button
|
||
- [ ] 3.5: Implement `_onLoad(presetName)` — call manager.load(presetName), close on success
|
||
- [ ] 3.6: Implement `_onCancel()` — close dialog
|
||
- [ ] 3.7: Implement empty state handling (no presets message)
|
||
- [ ] 3.8: Implement keyboard navigation (arrow keys, Enter=load, Escape=cancel)
|
||
- [ ] 3.9: Implement focus trap for accessibility
|
||
- [ ] 3.10: Green all PresetLoadDialog tests
|
||
|
||
**Acceptance Criteria:** AC-4, AC-5
|
||
|
||
---
|
||
|
||
### Task 4: Extend `src/ui/gm/DirectorsBoard.js`
|
||
|
||
**Files:** `src/ui/gm/DirectorsBoard.js`, `tests/unit/ui/gm/DirectorsBoard.test.js`
|
||
|
||
**Subtasks:**
|
||
- [ ] 4.1: Import ScenePresetManager in DirectorsBoard constructor
|
||
- [ ] 4.2: Add `this._presetManager` field
|
||
- [ ] 4.3: Update `_prepareContext()` to include preset count
|
||
- [ ] 4.4: Update `_onRender()` to wire Save/Load buttons in footer
|
||
- [ ] 4.5: Implement `_onSavePreset()` — opens PresetSaveDialog
|
||
- [ ] 4.6: Implement `_onLoadPreset()` — opens PresetLoadDialog
|
||
- [ ] 4.7: Enable Save/Load buttons in template (remove disabled attribute)
|
||
- [ ] 4.8: Add i18n keys for button labels
|
||
- [ ] 4.9: Update tests for new footer functionality
|
||
|
||
**Acceptance Criteria:** AC-1, AC-4
|
||
|
||
---
|
||
|
||
### Task 5: Create Templates
|
||
|
||
**Files:** `templates/preset-save-dialog.hbs`, `templates/preset-load-dialog.hbs`
|
||
|
||
**Subtasks:**
|
||
- [ ] 5.1: Create `preset-save-dialog.hbs` — form with name input, Save/Cancel buttons
|
||
- [ ] 5.2: All labels via `{{localize "video-view-manager.presets.save.*"}}` keys
|
||
- [ ] 5.3: Add ARIA labels for accessibility
|
||
- [ ] 5.4: Create `preset-load-dialog.hbs` — list of presets, Load/Cancel buttons
|
||
- [ ] 5.5: Show empty state when no presets exist
|
||
- [ ] 5.6: All labels via `{{localize "video-view-manager.presets.load.*"}}` keys
|
||
- [ ] 5.7: Add ARIA labels for accessibility
|
||
|
||
---
|
||
|
||
### Task 6: Add Styles
|
||
|
||
**Files:** `styles/components/_preset-save-dialog.less`, `styles/components/_preset-load-dialog.less`, `styles/components/_directors-board.less`
|
||
|
||
**Subtasks:**
|
||
- [ ] 6.1: Create `_preset-save-dialog.less` — form styling, input field, buttons
|
||
- [ ] 6.2: All selectors scoped under `.scrying-pool`
|
||
- [ ] 6.3: Use `--sp-*` tokens only (no direct Foundry tokens)
|
||
- [ ] 6.4: Create `_preset-load-dialog.less` — list styling, preset items, buttons
|
||
- [ ] 6.5: Style empty state message
|
||
- [ ] 6.6: Update `_directors-board.less` — enable Save/Load buttons in footer
|
||
|
||
---
|
||
|
||
### Task 7: Wire in `module.js`
|
||
|
||
**Files:** `module.js`
|
||
|
||
**Subtasks:**
|
||
- [ ] 7.1: Import `ScenePresetManager`
|
||
- [ ] 7.2: Import `PresetSaveDialog` and `PresetLoadDialog`
|
||
- [ ] 7.3: Declare `let scenePresetManager;` at module scope
|
||
- [ ] 7.4: Declare `let presetSaveDialog;` and `let presetLoadDialog;`
|
||
- [ ] 7.5: In `Hooks.once('ready')`, after `directorsBoard.init()`:
|
||
- [ ] 7.5.1: Construct `scenePresetManager = new ScenePresetManager(adapter, stateStore, socketHandler)`
|
||
- [ ] 7.5.2: Call `scenePresetManager.init()`
|
||
- [ ] 7.5.3: Construct dialogs with manager reference
|
||
- [ ] 7.6: Update header comment to include Story 3.1 wiring
|
||
|
||
---
|
||
|
||
### Task 8: Add i18n Keys
|
||
|
||
**Files:** `lang/en.json`
|
||
|
||
**Subtasks:**
|
||
- [ ] 8.1: Add `video-view-manager.presets.save.title` = "Save Scene Preset"
|
||
- [ ] 8.2: Add `video-view-manager.presets.save.nameLabel` = "Preset Name"
|
||
- [ ] 8.3: Add `video-view-manager.presets.save.nameHint` = "Enter a unique name for this camera layout"
|
||
- [ ] 8.4: Add `video-view-manager.presets.save.saveButton` = "Save"
|
||
- [ ] 8.5: Add `video-view-manager.presets.save.cancelButton` = "Cancel"
|
||
- [ ] 8.6: Add `video-view-manager.presets.save.duplicateWarning` = "A preset with this name already exists. Overwrite?"
|
||
- [ ] 8.7: Add `video-view-manager.presets.save.maxReached` = "Maximum of 50 presets reached. Delete an existing preset to save a new one."
|
||
- [ ] 8.8: Add `video-view-manager.presets.load.title` = "Load Scene Preset"
|
||
- [ ] 8.9: Add `video-view-manager.presets.load.emptyMessage` = "No presets saved yet."
|
||
- [ ] 8.10: Add `video-view-manager.presets.load.loadButton` = "Load"
|
||
- [ ] 8.11: Add `video-view-manager.presets.load.cancelButton` = "Cancel"
|
||
- [ ] 8.12: Add `video-view-manager.presets.load.confirmDelete` = "Are you sure you want to delete this preset?"
|
||
- [ ] 8.13: Add `video-view-manager.presets.notifications.applied` = "GM applied preset: {name}"
|
||
|
||
---
|
||
|
||
### Task 9: Socket Handler Extensions (if needed)
|
||
|
||
**Files:** `src/core/SocketHandler.js` (if preset events need special handling)
|
||
|
||
**Subtasks:**
|
||
- [ ] 9.1: Evaluate if PRESET_APPLY/PRESET_APPLIED need special handling
|
||
- [ ] 9.2: If yes, extend SocketHandler to support preset events
|
||
- [ ] 9.3: Add tests for preset socket handling
|
||
|
||
**Note:** The socket message contract already defines these events. Check if SocketHandler needs updates to handle them.
|
||
|
||
---
|
||
|
||
### Task 10: Pipeline Verification
|
||
|
||
**Subtasks:**
|
||
- [ ] 10.1: `npm run lint` exits 0 for all modified/new files
|
||
- [ ] 10.2: `npm run typecheck` exits 0
|
||
- [ ] 10.3: `npm run test` exits 0 (target: 412 + 60-80 = 472-492 tests)
|
||
- [ ] 10.4: Manual testing of all acceptance criteria
|
||
|
||
---
|
||
|
||
## 🎯 Dev Agent Guardrails
|
||
|
||
### Critical Architecture Rules
|
||
|
||
1. **NO direct `game.*` access in testable modules**
|
||
- ScenePresetManager, PresetSaveDialog, PresetLoadDialog must be fully testable
|
||
- All Foundry API access via injected adapter
|
||
|
||
2. **Import boundaries MUST be respected**
|
||
- `src/core/` → `src/contracts/`, `src/utils/` ONLY
|
||
- `src/ui/` → `src/core/`, `src/contracts/`, `src/utils/` ONLY
|
||
- ESLint will catch violations
|
||
|
||
3. **Side-effect-free constructors**
|
||
- NO hook registration in constructors
|
||
- NO state mutation in constructors
|
||
- Use init() for setup, teardown() for cleanup
|
||
|
||
4. **All user-facing strings MUST use i18n**
|
||
- Use `adapter.i18n.localize()` or `{{localize "..."}}` in templates
|
||
- NO hardcoded English strings
|
||
|
||
5. **All interactive elements MUST be accessible**
|
||
- Keyboard navigation
|
||
- ARIA labels
|
||
- Focus management
|
||
|
||
6. **All socket payloads MUST be validated**
|
||
- Use contract validators (isValidScenePreset, etc.)
|
||
- Validate before sending AND receiving
|
||
|
||
---
|
||
|
||
## 📌 Implementation Notes
|
||
|
||
### ScenePresetManager Design
|
||
|
||
```javascript
|
||
// src/core/ScenePresetManager.js
|
||
import { createScenePreset, isValidScenePreset, MAX_PRESETS_PER_WORLD } from '../contracts/scene-preset.js';
|
||
import { SOCKET_EVENTS, createSocketIntentMessage } from '../contracts/socket-message.js';
|
||
|
||
/**
|
||
* Manages scene preset CRUD operations.
|
||
* Persists presets to Scene document flags.
|
||
* Emits socket messages for preset apply operations.
|
||
*
|
||
* Import rule: may only import from src/contracts/ and src/utils/.
|
||
*/
|
||
export class ScenePresetManager {
|
||
/**
|
||
* @param {import('../foundry/FoundryAdapter.js').FoundryAdapter} adapter
|
||
* @param {import('./StateStore.js').StateStore} stateStore
|
||
* @param {import('./SocketHandler.js').SocketHandler} socketHandler
|
||
*/
|
||
constructor(adapter, stateStore, socketHandler) {
|
||
this._adapter = adapter;
|
||
this._stateStore = stateStore;
|
||
this._socketHandler = socketHandler;
|
||
this._presetsCache = new Map(); // name → ScenePreset
|
||
}
|
||
|
||
init() {
|
||
// No hooks needed for basic CRUD
|
||
// Hooks would be for auto-apply (Story 3.2)
|
||
this._loadCurrentScenePresets();
|
||
}
|
||
|
||
teardown() {
|
||
this._presetsCache.clear();
|
||
}
|
||
|
||
/**
|
||
* Saves the current Visibility Matrix as a named preset.
|
||
* @param {string} name - Preset name (non-empty string)
|
||
* @returns {Promise<ScenePreset>}
|
||
* @throws {TypeError} If name is invalid or max presets reached
|
||
*/
|
||
async save(name) {
|
||
// 1. Validate name
|
||
// 2. Check max presets
|
||
// 3. Get current matrix from StateStore
|
||
// 4. Create preset
|
||
// 5. Add to scene flag
|
||
// 6. Persist
|
||
// 7. Return preset
|
||
}
|
||
|
||
/**
|
||
* Loads a preset by name, applying its matrix.
|
||
* @param {string} name - Preset name
|
||
* @returns {Promise<void>}
|
||
* @throws {TypeError} If preset not found
|
||
*/
|
||
async load(name) {
|
||
// 1. Get preset from scene flag
|
||
// 2. Apply matrix via StateStore.setMatrix()
|
||
// 3. Emit socket message (PRESET_APPLY)
|
||
// 4. Emit notification via adapter.notifications
|
||
// 5. Update cache
|
||
}
|
||
|
||
// ... delete, rename, list methods
|
||
}
|
||
```
|
||
|
||
### Dialog UI Patterns
|
||
|
||
Follow the established pattern from DirectorsBoard:
|
||
- Conditional base class for ApplicationV2 compatibility
|
||
- Event delegation on root element
|
||
- Side-effect-free constructor
|
||
- init() for hook registration
|
||
- teardown() for cleanup
|
||
|
||
### Preset Storage Format
|
||
|
||
```javascript
|
||
// On Scene document flag:
|
||
{
|
||
"_version": 1,
|
||
"presets": {
|
||
"Combat View": {
|
||
"_version": 1,
|
||
"name": "Combat View",
|
||
"matrix": {
|
||
"user1": "active",
|
||
"user2": "hidden",
|
||
"user3": "active"
|
||
},
|
||
"createdAt": 1716451200000,
|
||
"updatedAt": 1716451200000
|
||
},
|
||
"Social Scene": {
|
||
"_version": 1,
|
||
"name": "Social Scene",
|
||
"matrix": {
|
||
"user1": "active",
|
||
"user2": "active",
|
||
"user3": "active"
|
||
},
|
||
"createdAt": 1716537600000,
|
||
"updatedAt": 1716537600000
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### Notification Patterns
|
||
|
||
Use existing NotificationBus or direct adapter.notifications:
|
||
```javascript
|
||
// For preset applied notification (AC-5):
|
||
this._adapter.notifications.info(
|
||
game.i18n.localize('video-view-manager.presets.notifications.applied')
|
||
.replace('{name}', presetName)
|
||
);
|
||
```
|
||
|
||
---
|
||
|
||
## 🔍 References
|
||
|
||
### Story-Specific References
|
||
- **Epics:** [Source: _bmad-output/planning-artifacts/epics.md#Story 3.1]
|
||
- **PRD:** FR-15, FR-16 [Source: _bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/prd.md#4.4]
|
||
- **Architecture:** Scene Preset JSON schema [Source: _bmad-output/planning-artifacts/architecture.md#Data Architecture]
|
||
- **UX Design:** UX-DR14, UX-DR15, UX-DR16 [Source: _bmad-output/planning-artifacts/ux-design-specification.md]
|
||
- **Contracts:** scene-preset.js, socket-message.js [Source: src/contracts/]
|
||
|
||
### Dependency References
|
||
- **StateStore.setMatrix():** [Source: src/core/StateStore.js:118-142]
|
||
- **ScryingPoolController.action():** [Source: src/core/ScryingPoolController.js:100-160]
|
||
- **DirectorsBoard pattern:** [Source: src/ui/gm/DirectorsBoard.js]
|
||
- **SocketHandler.emit():** [Source: src/core/SocketHandler.js:56-95]
|
||
- **NotificationBus pattern:** [Source: src/notifications/NotificationBus.js]
|
||
|
||
### Previous Story Patterns
|
||
- **Modal dialog pattern:** [Source: Story 2.2 - DirectorsBoard as ApplicationV2]
|
||
- **Bulk operation pattern:** [Source: Story 2.3 - showAll/hideAll/spotlight]
|
||
- **State machine pattern:** [Source: Story 2.3 - _undoSnapshot/_spotlightSnapshot]
|
||
- **TDD pattern:** [Source: All Epic 2 stories]
|
||
|
||
---
|
||
|
||
## 📝 Dev Notes
|
||
|
||
### File Location Notes
|
||
|
||
**New Directory:** None - all files fit in existing directory structure
|
||
|
||
**Import Boundary Reminder:**
|
||
```
|
||
src/core/ScenePresetManager.js → may import: src/contracts/, src/utils/ ONLY
|
||
src/ui/gm/PresetSaveDialog.js → may import: src/core/, src/contracts/, src/utils/ ONLY
|
||
src/ui/gm/PresetLoadDialog.js → may import: src/core/, src/contracts/, src/utils/ ONLY
|
||
```
|
||
|
||
### Constructor Pattern Reminder
|
||
|
||
All new classes must be side-effect-free:
|
||
```javascript
|
||
// ✅ CORRECT
|
||
export class ScenePresetManager {
|
||
constructor(adapter, stateStore, socketHandler) {
|
||
this._adapter = adapter; // Store only, no side effects
|
||
this._stateStore = stateStore;
|
||
this._socketHandler = socketHandler;
|
||
this._presetsCache = new Map(); // Initialize internal state
|
||
}
|
||
|
||
init() {
|
||
// Side effects here
|
||
this._loadCurrentScenePresets();
|
||
}
|
||
}
|
||
|
||
// ❌ WRONG - Side effects in constructor
|
||
constructor(adapter) {
|
||
this._hookId = Hooks.on('someEvent', this._handler.bind(this)); // Side effect!
|
||
}
|
||
```
|
||
|
||
### Test Mock Patterns
|
||
|
||
Use the established mock patterns from Epic 2:
|
||
```javascript
|
||
// From tests/helpers/foundryAdapterMock.js (create if doesn't exist)
|
||
export function createFoundryAdapterMock(overrides = {}) {
|
||
return {
|
||
scenes: {
|
||
current: vi.fn(() => ({ id: 'scene1', getFlag: vi.fn(), setFlag: vi.fn() })),
|
||
...overrides.scenes,
|
||
},
|
||
users: {
|
||
isGM: vi.fn(() => true),
|
||
current: vi.fn(() => ({ id: 'gm1', name: 'GM' })),
|
||
all: vi.fn(() => []),
|
||
...overrides.users,
|
||
},
|
||
hooks: {
|
||
on: vi.fn(() => 42),
|
||
off: vi.fn(),
|
||
callAll: vi.fn(),
|
||
...overrides.hooks,
|
||
},
|
||
socket: {
|
||
emit: vi.fn(),
|
||
on: vi.fn(),
|
||
off: vi.fn(),
|
||
...overrides.socket,
|
||
},
|
||
notifications: {
|
||
info: vi.fn(),
|
||
warn: vi.fn(),
|
||
error: vi.fn(),
|
||
...overrides.notifications,
|
||
},
|
||
i18n: {
|
||
localize: vi.fn((key) => key),
|
||
...overrides.i18n,
|
||
},
|
||
};
|
||
}
|
||
```
|
||
|
||
### Keyboard Shortcut Considerations
|
||
|
||
**Note:** Story 3.1 does NOT require new keyboard shortcuts. The Save/Load actions are triggered via UI buttons. Keyboard shortcuts for presets (if any) would be in Story 3.2 or 3.3.
|
||
|
||
### Accessibility Requirements
|
||
|
||
All dialogs must:
|
||
- Have `role="dialog"` or use native `<dialog>` element
|
||
- Be focus-trapped
|
||
- Support Escape to close
|
||
- Have all interactive elements keyboard-accessible
|
||
- Have appropriate ARIA labels
|
||
|
||
### Performance Considerations
|
||
|
||
- Preset list rendering should be efficient (virtualize if >20 presets)
|
||
- Matrix serialization/deserialization should be fast
|
||
- Scene flag reads/writes are async - handle promises correctly
|
||
|
||
---
|
||
|
||
## 🎯 Completion Checklist
|
||
|
||
- [ ] All tasks 1-10 completed
|
||
- [ ] All acceptance criteria pass manual testing
|
||
- [ ] All unit tests pass (60-80 new tests)
|
||
- [ ] `npm run lint` exits 0
|
||
- [ ] `npm run typecheck` exits 0
|
||
- [ ] `npm run test` exits 0
|
||
- [ ] Code review completed with no critical findings
|
||
- [ ] Story file updated with actual implementation notes
|
||
- [ ] Sprint status updated to "in-progress" when development starts
|
||
- [ ] Sprint status updated to "review" when ready for code review
|
||
- [ ] Sprint status updated to "done" after code review passes
|
||
|
||
---
|
||
|
||
*Generated by BMad Method Ultimate Story Context Engine - 2026-05-23*
|