diff --git a/planning_notes/state_save/implementation_plan.md b/planning_notes/state_save/implementation_plan.md new file mode 100644 index 0000000..fd73b22 --- /dev/null +++ b/planning_notes/state_save/implementation_plan.md @@ -0,0 +1,848 @@ +# Game State Persistence System - Implementation Plan + +## Overview + +This system enables variable state to carry over between game scenarios, allowing: +- NPC trust levels to persist across games +- Discussion topics to be tracked (preventing repetition) +- Narrative decisions to have consequences in later scenarios +- Scenarios to be played in any order while maintaining continuity + +**Key Principle**: This is NOT a "save game" feature - it's a cross-scenario narrative persistence layer. + +--- + +## Design Decisions + +### 1. Variable Declaration Strategy: Metadata Properties + +We'll use **Option 1** from the design options - metadata properties in `globalVariables`: + +```json +{ + "globalVariables": { + "npc_trust_guard": { + "default": 0, + "carryOver": true, + "description": "Trust level with security guard" + }, + "topics_discussed_lore": { + "default": [], + "carryOver": true + }, + "temp_room_discovered": { + "default": false, + "carryOver": false + } + } +} +``` + +**Backward Compatibility**: Support simple values for non-carry-over variables: +```json +{ + "globalVariables": { + "npc_trust_guard": { + "default": 0, + "carryOver": true + }, + "temp_session_var": false // Simple value = session-only, not carried over + } +} +``` + +### 2. Storage Strategy + +**Phase 1 (Current Implementation)**: +- **Import**: Load from JSON file via URL parameter `?persistentState=path/to/state.json` +- **Export**: JavaScript function callable from console: `window.exportPersistentState()` +- **Auto-export on Victory**: Hook into victory condition to call export function + +**Phase 2 (Future)**: +- POST to server endpoint when game ends in victory +- Server manages persistent state per player + +### 3. Data Flow + +``` +┌─────────────────────┐ +│ persistent-state.json│ +│ (URL parameter) │ +└──────┬──────────────┘ + │ Load on startup + ↓ +┌─────────────────────┐ ┌──────────────────┐ +│ scenario.json │ │ Persistent State │ +│ .globalVariables │──────┤ (loaded from URL)│ +│ with metadata │ Merge└──────────────────┘ +└──────┬──────────────┘ │ + │ │ + ↓ ↓ +┌──────────────────────────────┐ +│ window.gameState │ +│ .globalVariables │ ←── Synced bidirectionally +│ .persistentStateManager │ during gameplay +└──────┬───────────────────────┘ + │ + ↓ +┌──────────────────────────────┐ +│ Ink Stories (NPCs) │ +│ via NPCConversationState │ +└──────┬───────────────────────┘ + │ + ↓ On victory/export +┌──────────────────────────────┐ +│ Exported JSON │ +│ (console/server endpoint) │ +└──────────────────────────────┘ +``` + +### 4. Merge Strategy + +When loading persistent state: + +1. **Parse scenario.json globalVariables** → Extract defaults and carryOver flags +2. **Load persistent state JSON** (if provided via URL) +3. **Merge**: + - For variables marked `carryOver: true`: + - Use persistent state value if exists + - Otherwise use scenario default + - For variables NOT marked `carryOver: true`: + - Always use scenario default (ignore persistent state) +4. **Initialize** `window.gameState.globalVariables` with merged values + +--- + +## Data Structures + +### Scenario JSON Schema (Enhanced) + +```json +{ + "scenario_brief": "Mission description", + "globalVariables": { + "npc_trust_security_guard": { + "default": 0, + "carryOver": true, + "type": "number", + "description": "Trust level with security guard (0-100)" + }, + "npc_trust_helper_npc": { + "default": 50, + "carryOver": true, + "type": "number" + }, + "topics_lore_cybersec": { + "default": [], + "carryOver": true, + "type": "array", + "description": "Cybersecurity lore topics discussed" + }, + "topics_lore_corporate": { + "default": [], + "carryOver": true, + "type": "array" + }, + "narrative_joined_org": { + "default": false, + "carryOver": true, + "type": "boolean" + }, + "narrative_betrayed_contact": { + "default": false, + "carryOver": true, + "type": "boolean" + }, + "temp_current_mission_phase": { + "default": 1, + "carryOver": false, + "type": "number" + }, + "simple_session_var": "value" // Simple values default to carryOver: false + }, + "startRoom": "reception", + "rooms": { /* ... */ } +} +``` + +### Persistent State JSON Format + +```json +{ + "version": 1, + "timestamp": "2024-11-14T12:34:56.789Z", + "lastScenario": "ceo_exfil", + "variables": { + "npc_trust_security_guard": 75, + "npc_trust_helper_npc": 85, + "topics_lore_cybersec": ["encryption", "zero_days", "social_engineering"], + "topics_lore_corporate": ["mergers", "insider_trading"], + "narrative_joined_org": true, + "narrative_betrayed_contact": false + } +} +``` + +**Key Fields**: +- `version`: Schema version for future migrations (start at 1) +- `timestamp`: When this state was exported +- `lastScenario`: ID of scenario that produced this state (for debugging) +- `variables`: Key-value map of carry-over variables only + +--- + +## Implementation Steps + +### Step 1: Create Persistent State Manager Module + +**File**: `js/systems/persistent-state-manager.js` (NEW) + +**Responsibilities**: +- Load persistent state from URL parameter +- Parse and validate persistent state JSON +- Merge persistent state with scenario defaults +- Extract carry-over metadata from scenario.json +- Export current persistent state as JSON +- Provide console commands for debugging + +**Key Functions**: +```javascript +class PersistentStateManager { + constructor() { + this.version = 1; + this.loadedState = null; + this.carryOverMetadata = new Map(); // varName → { default, type, description } + } + + // Load persistent state from URL param + async loadPersistentStateFromURL(urlParams) { } + + // Extract carry-over variable metadata from scenario + extractCarryOverMetadata(scenarioGlobalVariables) { } + + // Merge persistent state with scenario defaults + mergeWithScenarioDefaults(persistentState, scenarioGlobalVariables) { } + + // Export current persistent state + exportPersistentState(lastScenarioId) { } + + // Download as JSON file + downloadAsJSON(filename) { } + + // Console command: view current state + viewPersistentState() { } + + // Future: POST to server endpoint + async postToServer(endpoint, authToken) { } +} +``` + +### Step 2: Modify game.js to Integrate Persistent State + +**File**: `js/core/game.js` + +**Location**: `preload()` function (around line 411-430) + +**Changes**: + +1. **Load persistent state file** (if URL param provided): + +```javascript +// After loading scenario.json +const urlParams = new URLSearchParams(window.location.search); + +// Load persistent state if provided +let persistentStateFile = urlParams.get('persistentState'); +if (persistentStateFile) { + // Ensure proper path prefix + if (!persistentStateFile.startsWith('persistent-states/')) { + persistentStateFile = `persistent-states/${persistentStateFile}`; + } + + // Ensure .json extension + if (!persistentStateFile.endsWith('.json')) { + persistentStateFile = `${persistentStateFile}.json`; + } + + // Add cache buster + persistentStateFile = `${persistentStateFile}?v=${Date.now()}`; + + // Load the persistent state + this.load.json('persistentStateJSON', persistentStateFile); + console.log('🔄 Loading persistent state from:', persistentStateFile); +} +``` + +**Location**: `create()` function (around line 461-467) + +**Changes**: + +2. **Initialize PersistentStateManager and merge state**: + +```javascript +import { PersistentStateManager } from '../systems/persistent-state-manager.js'; + +// In create() function, BEFORE initializing global variables: + +// Initialize persistent state manager +window.persistentStateManager = new PersistentStateManager(); + +// Load persistent state from cache if it was loaded +const persistentStateJSON = this.cache.json.get('persistentStateJSON'); +if (persistentStateJSON) { + console.log('🔄 Loaded persistent state:', persistentStateJSON); + window.persistentStateManager.loadedState = persistentStateJSON; +} + +// Extract carry-over metadata from scenario +if (gameScenario.globalVariables) { + window.persistentStateManager.extractCarryOverMetadata(gameScenario.globalVariables); +} + +// Merge persistent state with scenario defaults +const mergedVariables = window.persistentStateManager.mergeWithScenarioDefaults( + persistentStateJSON?.variables, + gameScenario.globalVariables +); + +// Initialize global variables with merged values +window.gameState.globalVariables = mergedVariables; +console.log('🌐 Initialized global variables (with persistent state):', window.gameState.globalVariables); +``` + +### Step 3: Enhance NPCConversationStateManager + +**File**: `js/systems/npc-conversation-state.js` + +**No major changes needed** - this module already handles global variable syncing. + +**Optional Enhancement**: Add logging when persistent variables change: + +```javascript +// In syncGlobalVariablesFromStory() around line 267 +if (oldValue !== newValue) { + window.gameState.globalVariables[name] = newValue; + changed.push({ name, value: newValue }); + + // Check if this is a carry-over variable + const isCarryOver = window.persistentStateManager?.carryOverMetadata.has(name); + const logPrefix = isCarryOver ? '🔄' : '🔄'; + console.log(`${logPrefix} Global variable ${name} changed from ${oldValue} to ${newValue}` + + (isCarryOver ? ' [PERSISTENT]' : '')); +} +``` + +### Step 4: Add Export Functions + +**File**: `js/systems/persistent-state-manager.js` + +**Functions to implement**: + +```javascript +/** + * Export current carry-over variables as JSON + * @param {string} lastScenarioId - ID of current scenario + * @returns {Object} Persistent state object + */ +exportPersistentState(lastScenarioId = 'unknown') { + const exported = { + version: this.version, + timestamp: new Date().toISOString(), + lastScenario: lastScenarioId, + variables: {} + }; + + // Extract only carry-over variables + this.carryOverMetadata.forEach((metadata, varName) => { + if (window.gameState.globalVariables.hasOwnProperty(varName)) { + exported.variables[varName] = window.gameState.globalVariables[varName]; + } + }); + + console.log('📤 Exported persistent state:', exported); + return exported; +} + +/** + * Download persistent state as JSON file + * @param {string} filename - Output filename + */ +downloadAsJSON(filename = 'persistent-state.json') { + const scenarioId = window.gameScenario?.scenario_id || 'game'; + const state = this.exportPersistentState(scenarioId); + + const blob = new Blob([JSON.stringify(state, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + + const a = document.createElement('a'); + a.href = url; + a.download = filename; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + + console.log('✅ Downloaded persistent state as:', filename); +} + +/** + * Console command to view persistent state + */ +viewPersistentState() { + console.log('═══════════════════════════════════════'); + console.log('📊 PERSISTENT STATE VIEWER'); + console.log('═══════════════════════════════════════'); + + console.log('\n🔍 Carry-Over Variables:'); + this.carryOverMetadata.forEach((metadata, varName) => { + const currentValue = window.gameState.globalVariables[varName]; + console.log(` ${varName}:`, currentValue, `(default: ${metadata.default})`); + }); + + console.log('\n📦 Full Export Preview:'); + const exported = this.exportPersistentState(); + console.log(JSON.stringify(exported, null, 2)); + + console.log('\n💡 Commands:'); + console.log(' window.exportPersistentState() - Export as JSON'); + console.log(' window.downloadPersistentState() - Download JSON file'); + console.log('═══════════════════════════════════════'); +} +``` + +### Step 5: Add Global Console Commands + +**File**: `js/main.js` + +**Location**: After initializing window.gameState + +**Add**: + +```javascript +// Expose persistent state functions to console +window.exportPersistentState = () => { + if (!window.persistentStateManager) { + console.error('❌ Persistent state manager not initialized'); + return null; + } + const scenarioId = window.gameScenario?.scenario_id || 'game'; + return window.persistentStateManager.exportPersistentState(scenarioId); +}; + +window.downloadPersistentState = (filename) => { + if (!window.persistentStateManager) { + console.error('❌ Persistent state manager not initialized'); + return; + } + window.persistentStateManager.downloadAsJSON(filename); +}; + +window.viewPersistentState = () => { + if (!window.persistentStateManager) { + console.error('❌ Persistent state manager not initialized'); + return; + } + window.persistentStateManager.viewPersistentState(); +}; + +console.log('💾 Persistent State Commands Available:'); +console.log(' window.exportPersistentState() - Export state as JSON object'); +console.log(' window.downloadPersistentState(filename) - Download state as file'); +console.log(' window.viewPersistentState() - View current persistent state'); +``` + +### Step 6: Hook Export to Victory Condition + +**Strategy**: Use the existing event system to trigger export on victory. + +**Possible approaches**: + +1. **Listen for specific victory events** (if they exist): + - `scenario_completed` + - `objective_completed:final` + - etc. + +2. **Hook into item pickup of flag/evidence**: + - Detect when player picks up the final evidence + - Trigger export automatically + +3. **Manual trigger from Ink** (most flexible): + - Add an Ink tag: `#export_persistent_state` + - Process in conversation handlers + +**Recommended**: Approach 3 (Ink tag) + +**File**: `js/minigames/person-chat/person-chat-conversation.js` + +**Location**: `processInkTags()` function (around line 269-457) + +**Add**: + +```javascript +// Around line 450, add new case: +case 'export_persistent_state': + console.log('📤 Exporting persistent state (triggered by Ink)'); + if (window.persistentStateManager) { + const scenarioId = window.gameScenario?.scenario_id || 'game'; + const exported = window.persistentStateManager.exportPersistentState(scenarioId); + console.log('✅ Persistent state exported:', exported); + + // Future: POST to server + // await window.persistentStateManager.postToServer('/api/save-state', authToken); + } + break; +``` + +**Same change in**: `js/minigames/phone-chat/phone-chat-conversation.js` + +### Step 7: Create Example Files + +**File**: `persistent-states/example-continued-game.json` (NEW) + +```json +{ + "version": 1, + "timestamp": "2024-11-14T12:00:00.000Z", + "lastScenario": "ceo_exfil", + "variables": { + "npc_trust_security_guard": 75, + "npc_trust_helper_npc": 85, + "topics_lore_cybersec": ["encryption", "zero_days"], + "narrative_joined_org": true + } +} +``` + +**File**: `persistent-states/README.md` (NEW) + +```markdown +# Persistent Game States + +This directory contains persistent state files that carry variable values across game scenarios. + +## Usage + +Load a persistent state by adding the `persistentState` URL parameter: + +``` +index.html?scenario=ceo_exfil&persistentState=example-continued-game +``` + +## File Format + +See `example-continued-game.json` for the schema. + +## Exporting State + +From the browser console after completing a game: + +```javascript +// View current persistent state +window.viewPersistentState(); + +// Download as JSON file +window.downloadPersistentState('my-progress.json'); +``` +``` + +### Step 8: Update Example Scenario + +**File**: `scenarios/ceo_exfil.json` + +**Add globalVariables section** (if not present) or enhance it: + +```json +{ + "scenario_brief": "...", + "scenario_id": "ceo_exfil", + "globalVariables": { + "npc_trust_helper": { + "default": 50, + "carryOver": true, + "type": "number", + "description": "Trust level with helpful contact" + }, + "topics_discussed_security": { + "default": [], + "carryOver": true, + "type": "array", + "description": "Security topics discussed with helper NPC" + }, + "mission_ceo_completed": { + "default": false, + "carryOver": true, + "type": "boolean", + "description": "Whether CEO exfil mission was completed" + } + }, + "startRoom": "reception", + "rooms": { /* ... */ } +} +``` + +### Step 9: Update Ink Stories to Use Persistent Variables + +**Example**: `scenarios/ink/helper-npc.ink` + +**Add logic that checks persistent state**: + +```ink +=== start === +{topics_discussed_security ? encryption: + // Already discussed encryption in previous game + -> already_know_encryption +- else: + // First time discussing encryption + -> introduce_encryption +} + +=== introduce_encryption === +Let me tell you about encryption fundamentals... +~ topics_discussed_security += "encryption" +~ npc_trust_helper = npc_trust_helper + 10 +-> END + +=== already_know_encryption === +You already know about encryption. Let's talk about something new. ++ [Advanced cryptography] -> advanced_crypto ++ [Social engineering] -> social_engineering +-> END +``` + +--- + +## Implementation Todo List + +### Phase 1: Core Infrastructure (Essential) + +1. **Create PersistentStateManager module** (`js/systems/persistent-state-manager.js`) + - [ ] Create class skeleton + - [ ] Implement `extractCarryOverMetadata()` + - [ ] Implement `mergeWithScenarioDefaults()` + - [ ] Implement `exportPersistentState()` + - [ ] Implement `downloadAsJSON()` + - [ ] Implement `viewPersistentState()` + - [ ] Add validation and error handling + - [ ] Add comprehensive logging + +2. **Modify game.js for persistent state loading** + - [ ] Add persistent state file loading in `preload()` + - [ ] Import PersistentStateManager + - [ ] Initialize manager in `create()` + - [ ] Extract carry-over metadata from scenario + - [ ] Merge persistent state with scenario defaults + - [ ] Replace current globalVariables initialization with merged values + - [ ] Add logging for debugging + +3. **Add global console commands** (`js/main.js`) + - [ ] Expose `window.exportPersistentState()` + - [ ] Expose `window.downloadPersistentState()` + - [ ] Expose `window.viewPersistentState()` + - [ ] Add help text to console on startup + +4. **Create directory structure** + - [ ] Create `persistent-states/` directory + - [ ] Create `persistent-states/README.md` + - [ ] Create example file: `persistent-states/example-continued-game.json` + +### Phase 2: Integration & Triggers (Important) + +5. **Add export triggers to conversation handlers** + - [ ] Add `#export_persistent_state` tag handler to `person-chat-conversation.js` + - [ ] Add same handler to `phone-chat-conversation.js` + - [ ] Test tag triggers export correctly + +6. **Enhance logging in NPCConversationStateManager** (Optional but helpful) + - [ ] Mark persistent variables with [PERSISTENT] in logs + - [ ] Add warnings when persistent variables change unexpectedly + +### Phase 3: Documentation & Examples (Helpful) + +7. **Update scenario example** + - [ ] Add/enhance `globalVariables` in `scenarios/ceo_exfil.json` + - [ ] Add carry-over metadata to key variables + - [ ] Add `scenario_id` field + +8. **Update Ink stories with persistent logic** + - [ ] Update `helper-npc.ink` to check `topics_discussed` array + - [ ] Add trust level modifications + - [ ] Add victory condition with `#export_persistent_state` tag + +9. **Create documentation** + - [ ] Add usage guide to main README + - [ ] Document globalVariables metadata schema + - [ ] Document URL parameter usage + - [ ] Document console commands + +### Phase 4: Testing (Critical) + +10. **Test persistent state loading** + - [ ] Test loading valid persistent state JSON + - [ ] Test loading with missing file (graceful degradation) + - [ ] Test loading with invalid JSON (error handling) + - [ ] Test loading with mismatched variables (partial merge) + +11. **Test variable merging** + - [ ] Test carry-over variables override defaults + - [ ] Test session-only variables use scenario defaults + - [ ] Test simple value syntax (backward compat) + - [ ] Test complex metadata syntax + +12. **Test export functionality** + - [ ] Test `exportPersistentState()` returns correct JSON + - [ ] Test `downloadPersistentState()` creates valid file + - [ ] Test `viewPersistentState()` displays correctly + - [ ] Test export triggered from Ink tag + +13. **Test cross-scenario persistence** + - [ ] Complete scenario A, export state + - [ ] Load scenario B with exported state + - [ ] Verify carry-over variables maintained + - [ ] Verify session variables reset + +### Phase 5: Future Enhancements (Post-MVP) + +14. **Server integration** (Future) + - [ ] Implement `postToServer()` function + - [ ] Add authentication token handling + - [ ] Add retry logic for network failures + - [ ] Add success/failure callbacks + +15. **Advanced features** (Future) + - [ ] Add schema versioning and migration support + - [ ] Add variable type validation + - [ ] Add min/max constraints for numbers + - [ ] Add debug UI panel for viewing/editing state + - [ ] Add "reset to defaults" functionality + +--- + +## Testing Scenarios + +### Test 1: Fresh Start (No Persistent State) +1. Load game without `persistentState` parameter +2. Verify all variables use scenario defaults +3. Complete game and export state +4. Verify exported JSON contains correct values + +### Test 2: Continued Game (With Persistent State) +1. Load game with `persistentState=example-continued-game` +2. Verify carry-over variables loaded from persistent state +3. Verify session-only variables use scenario defaults +4. Modify variables during gameplay +5. Export state and verify changes reflected + +### Test 3: Partial Persistent State +1. Create persistent state with only SOME carry-over variables +2. Load game +3. Verify loaded variables use persistent state +4. Verify missing variables use scenario defaults + +### Test 4: Cross-Scenario Persistence +1. Complete "CEO Exfil" scenario +2. Export persistent state +3. Load "Server Room" scenario with exported state +4. Verify shared variables (e.g., `npc_trust_helper`) persist +5. Verify scenario-specific variables reset + +### Test 5: Backward Compatibility +1. Load scenario using old simple syntax: `"var": value` +2. Verify variables work as session-only +3. Mix simple and metadata syntax +4. Verify both work correctly + +--- + +## File Changes Summary + +### New Files +- `js/systems/persistent-state-manager.js` - Core module +- `persistent-states/example-continued-game.json` - Example state +- `persistent-states/README.md` - Documentation + +### Modified Files +- `js/core/game.js` - Add persistent state loading and merging +- `js/main.js` - Add global console commands +- `js/minigames/person-chat/person-chat-conversation.js` - Add export tag handler +- `js/minigames/phone-chat/phone-chat-conversation.js` - Add export tag handler +- `scenarios/ceo_exfil.json` - Add enhanced globalVariables with metadata +- `scenarios/ink/helper-npc.ink` - Add persistent variable logic (example) + +### Optional Enhancements +- `js/systems/npc-conversation-state.js` - Enhanced logging for persistent vars + +--- + +## Implementation Risks & Mitigations + +### Risk 1: Breaking Existing Games +**Mitigation**: Support both simple and metadata syntax. If `globalVariables[key]` is not an object, treat it as simple value with `carryOver: false`. + +### Risk 2: Persistent State Conflicts +**Mitigation**: Always prefer persistent state for carry-over variables, but validate types before applying. + +### Risk 3: Performance with Large States +**Mitigation**: Limit carry-over variables to essential narrative state. Avoid storing large arrays/objects. + +### Risk 4: Invalid JSON Files +**Mitigation**: Wrap JSON parsing in try-catch, log errors, fall back to defaults gracefully. + +### Risk 5: Security (Future Server Integration) +**Mitigation**: Validate all incoming persistent state on server, sanitize values, enforce rate limits. + +--- + +## Success Criteria + +✅ Can load persistent state from JSON file via URL parameter +✅ Carry-over variables override scenario defaults when loaded +✅ Session-only variables always use scenario defaults +✅ Can export current persistent state via console command +✅ Can download persistent state as JSON file +✅ Export can be triggered from Ink via tag +✅ Backward compatible with simple globalVariables syntax +✅ Clear logging shows when persistent state is loaded/exported +✅ Cross-scenario persistence works (variables carry over between games) +✅ Graceful degradation when persistent state file missing or invalid + +--- + +## Future Considerations + +1. **Server-Side Persistence** + - User authentication and session management + - Cloud storage of persistent state + - Automatic sync on game end + +2. **Multiple Save Slots** + - Allow multiple persistent state "profiles" + - Let players choose which profile to continue + +3. **State Versioning** + - Handle schema changes between game versions + - Provide migration functions for old states + +4. **State Validation** + - Type checking (string, number, boolean, array) + - Range validation (min/max for numbers) + - Required vs optional variables + +5. **Debug Tools** + - In-game UI for viewing/editing persistent state + - Timeline showing variable changes + - Diff view between scenarios + +6. **Analytics** + - Track which narrative paths players take + - Identify popular decision trees + - Balance game difficulty based on persistence data + +--- + +## Notes + +- Keep persistent state **minimal** - only essential narrative variables +- Use **clear naming conventions**: `npc_trust_*`, `topics_*`, `narrative_*` +- Document **all carry-over variables** with descriptions +- Test **cross-scenario compatibility** thoroughly +- Consider **data privacy** for future server integration diff --git a/planning_notes/state_save/review1.md b/planning_notes/state_save/review1.md new file mode 100644 index 0000000..83b66a4 --- /dev/null +++ b/planning_notes/state_save/review1.md @@ -0,0 +1,1090 @@ +# Implementation Plan Review - Persistent State System + +**Reviewer**: Claude (Automated Review) +**Date**: 2024-11-14 +**Plan Version**: v1.0 +**Review Focus**: Architecture, Risk Assessment, Success Factors + +--- + +## Executive Summary + +**Overall Assessment**: ✅ **STRONG PLAN** with minor improvements needed + +The implementation plan is comprehensive, well-structured, and technically sound. It properly integrates with the existing architecture without breaking changes, provides good backward compatibility, and has a clear path from MVP to production. + +**Key Strengths**: +- Non-breaking integration with existing global variables system +- Clear separation of concerns (new module vs modifications) +- Comprehensive error handling strategy +- Good testing coverage outlined + +**Key Risks**: +- Type coercion issues between JavaScript and Ink +- Async loading race conditions +- Metadata parsing edge cases + +**Recommendation**: Proceed with implementation, incorporating the improvements listed below. + +--- + +## Detailed Review + +### 1. Architecture Analysis + +#### ✅ Strengths + +**1.1 Module Separation** +- Creating `PersistentStateManager` as a separate module is excellent design +- Clear single responsibility: manage cross-scenario persistence +- Doesn't pollute existing modules with persistence logic + +**1.2 Integration Points** +- Smart to integrate at `game.js` initialization (single choke point) +- Using existing URL parameter pattern is consistent with scenario loading +- Leveraging existing `NPCConversationStateManager` avoids duplication + +**1.3 Data Flow** +- Linear, predictable flow: Load → Merge → Initialize → Sync → Export +- Clear ownership of each stage +- Good use of existing global state pattern + +#### ⚠️ Concerns + +**1.4 Timing Dependencies** +```javascript +// In game.js create() +const persistentStateJSON = this.cache.json.get('persistentStateJSON'); +if (persistentStateJSON) { + window.persistentStateManager.loadedState = persistentStateJSON; +} +``` + +**Issue**: Assumes `persistentStateJSON` is fully loaded when `create()` runs. + +**Risk**: If Phaser's asset loading is asynchronous and delayed, cache might be empty. + +**Improvement**: Add explicit check and error handling: +```javascript +const persistentStateJSON = this.cache.json.get('persistentStateJSON'); +if (persistentStateJSON) { + console.log('✅ Persistent state loaded from cache'); + window.persistentStateManager.loadedState = persistentStateJSON; +} else if (urlParams.has('persistentState')) { + console.warn('⚠️ Persistent state file specified but not loaded - may be missing or invalid'); + console.warn(' Falling back to scenario defaults'); +} +``` + +**1.5 Global State Mutation** +The plan directly modifies `window.gameState.globalVariables`. This is consistent with current architecture but could cause issues if multiple systems modify state simultaneously. + +**Recommendation**: Add a "seal" mechanism after initialization: +```javascript +window.gameState.persistentStateInitialized = true; +``` + +Then in the merge function, warn if trying to re-initialize: +```javascript +if (window.gameState.persistentStateInitialized) { + console.warn('⚠️ Attempting to re-initialize persistent state - ignoring'); + return window.gameState.globalVariables; +} +``` + +--- + +### 2. Data Structure Review + +#### ✅ Strong Design + +**2.1 Metadata Format** +The chosen metadata format is flexible and future-proof: +```json +{ + "npc_trust_guard": { + "default": 0, + "carryOver": true, + "type": "number", + "description": "Trust level with security guard" + } +} +``` + +**Benefits**: +- Self-documenting (description field) +- Extensible (can add validation rules later) +- Clear intent (carryOver flag) + +**2.2 Backward Compatibility** +Supporting both formats is smart: +```json +{ + "old_var": "value", // Simple - session only + "new_var": { "default": "value", "carryOver": true } // Metadata +} +``` + +#### ⚠️ Potential Issues + +**2.3 Type Ambiguity** +```json +{ + "some_var": { + "default": 0, + "carryOver": true + } +} +``` + +**Problem**: `{ default, carryOver }` could theoretically be a valid variable VALUE if someone wants to store an object with those keys. + +**Likelihood**: Low, but possible + +**Improvement**: Add explicit type checking in `extractCarryOverMetadata()`: +```javascript +extractCarryOverMetadata(scenarioGlobalVariables) { + this.carryOverMetadata.clear(); + + for (const [varName, value] of Object.entries(scenarioGlobalVariables)) { + // Check if this looks like metadata object + if (typeof value === 'object' && + value !== null && + !Array.isArray(value) && + 'default' in value) { + + // This is metadata format + this.carryOverMetadata.set(varName, { + default: value.default, + carryOver: value.carryOver ?? false, + type: value.type, + description: value.description + }); + } else { + // Simple value format - not a carry-over variable + this.carryOverMetadata.set(varName, { + default: value, + carryOver: false, + type: typeof value, + description: null + }); + } + } +} +``` + +This way, ALL variables are tracked in metadata (not just carry-over ones), making merging simpler. + +**2.4 Array and Object Deep Copying** +The plan uses spread operator for merging: +```javascript +window.gameState.globalVariables = { ...gameScenario.globalVariables }; +``` + +**Problem**: Shallow copy means nested objects/arrays are referenced, not cloned. + +**Improvement**: Use deep clone for defaults and persistent state: +```javascript +function deepClone(obj) { + if (obj === null || typeof obj !== 'object') return obj; + if (Array.isArray(obj)) return obj.map(item => deepClone(item)); + const cloned = {}; + for (const [key, value] of Object.entries(obj)) { + cloned[key] = deepClone(value); + } + return cloned; +} + +// Then use: +const defaultValue = deepClone(metadata.default); +``` + +**Alternative**: Use `structuredClone()` if targeting modern browsers: +```javascript +const defaultValue = structuredClone(metadata.default); +``` + +--- + +### 3. Implementation Risks & Mitigations + +#### 🔴 High Priority + +**3.1 Type Coercion Between JavaScript and Ink** + +**Issue**: Ink variables have their own type system. JavaScript arrays might not serialize/deserialize correctly through Ink's `ToJson()` / `LoadJson()`. + +**Example**: +```javascript +// JavaScript +topics_discussed: ["encryption", "social_engineering"] + +// After going through Ink serialization +topics_discussed: "encryption,social_engineering" // Might become string +``` + +**Current Code Investigation Needed**: +Check `npc-conversation-state.js` to see how arrays are currently handled. + +**Mitigation**: +1. Test array persistence thoroughly +2. Consider using comma-separated strings for arrays if Ink doesn't preserve them +3. Or store arrays as JSON strings: `"[\"encryption\",\"social_engineering\"]"` +4. Add type validation in merge function: + +```javascript +mergeWithScenarioDefaults(persistentState, scenarioGlobalVariables) { + const merged = {}; + + this.carryOverMetadata.forEach((metadata, varName) => { + let value = metadata.default; + + // Check if persistent state has this variable + if (persistentState && varName in persistentState) { + const persistentValue = persistentState[varName]; + + // Type validation + if (metadata.type && typeof persistentValue !== metadata.type) { + console.warn(`⚠️ Type mismatch for ${varName}: expected ${metadata.type}, got ${typeof persistentValue}`); + console.warn(` Using default value instead`); + value = metadata.default; + } else { + value = persistentValue; + } + } + + merged[varName] = value; + }); + + return merged; +} +``` + +**3.2 Race Condition: Async JSON Loading** + +**Issue**: Phaser's `this.load.json()` is asynchronous. If persistent state file is slow to load, `create()` might run before it's ready. + +**Current Code**: +```javascript +// preload() +this.load.json('persistentStateJSON', persistentStateFile); + +// create() - runs AFTER preload completes +const persistentStateJSON = this.cache.json.get('persistentStateJSON'); +``` + +**Analysis**: Phaser guarantees `create()` runs after `preload()` completes, so this should be safe. + +**Additional Safety**: Add validation to confirm: +```javascript +if (urlParams.has('persistentState')) { + const persistentStateJSON = this.cache.json.get('persistentStateJSON'); + + if (!persistentStateJSON) { + console.error('❌ Persistent state file failed to load'); + console.error(' File may be missing, malformed, or network error occurred'); + console.error(' Proceeding with scenario defaults'); + } else { + console.log('✅ Persistent state loaded successfully'); + window.persistentStateManager.loadedState = persistentStateJSON; + } +} +``` + +**3.3 Missing File Handling** + +**Issue**: If user specifies `?persistentState=nonexistent.json`, Phaser will log an error but won't crash. + +**Risk**: Silent failure - user might not know their state didn't load. + +**Mitigation**: Add visual feedback (not just console): +```javascript +if (urlParams.has('persistentState') && !persistentStateJSON) { + // Show in-game notification + this.add.text(10, 10, '⚠️ Persistent state failed to load', { + fontSize: '16px', + backgroundColor: '#ff0000', + padding: { x: 10, y: 5 } + }).setDepth(10000).setScrollFactor(0); + + // Auto-hide after 5 seconds + this.time.delayedCall(5000, () => { + // Remove notification + }); +} +``` + +#### 🟡 Medium Priority + +**3.4 Export Trigger Timing** + +**Issue**: If `#export_persistent_state` tag is in middle of conversation, variables might not be in final state yet. + +**Scenario**: +```ink +You've completed the mission! Congratulations! +#export_persistent_state +~ npc_trust_guard = npc_trust_guard + 50 // This happens AFTER export! +``` + +**Mitigation**: Document that export tag should be at END of conversation, after all variable modifications: +```ink +~ npc_trust_guard = npc_trust_guard + 50 +~ mission_complete = true +You've completed the mission! Congratulations! +#export_persistent_state +``` + +Or make export async and delayed: +```javascript +case 'export_persistent_state': + // Delay export to ensure all current-turn variables are synced + setTimeout(() => { + console.log('📤 Exporting persistent state...'); + if (window.persistentStateManager) { + const exported = window.persistentStateManager.exportPersistentState(); + console.log('✅ Exported:', exported); + } + }, 100); // Small delay to ensure sync happens first + break; +``` + +**3.5 Variable Name Collisions** + +**Issue**: Two scenarios might use same variable name for different purposes. + +**Example**: +- Scenario A: `npc_trust_guard` = trust with security guard +- Scenario B: `npc_trust_guard` = trust with prison guard (different NPC) + +**Mitigation Options**: + +1. **Namespacing** (Recommended): +```json +{ + "npc_trust_security_guard_hq": { "default": 0, "carryOver": true }, + "npc_trust_prison_guard_cellblock": { "default": 0, "carryOver": true } +} +``` + +2. **Scenario-specific prefixes**: +```json +{ + "ceo_exfil_npc_trust_guard": { "default": 0, "carryOver": true } +} +``` + +3. **Shared namespace documentation**: Create a central registry of carry-over variables and their canonical meanings. + +**Add to documentation**: +```markdown +## Carry-Over Variable Naming Conventions + +- `npc_trust_{npc_id}` - Trust level with specific NPC (0-100) +- `topics_{category}` - Array of discussed topics in category +- `narrative_{decision}` - Boolean flag for major narrative choices +- `achievement_{name}` - Boolean flag for unlocked achievements + +Always use specific NPC IDs to avoid collisions across scenarios. +``` + +#### 🟢 Low Priority + +**3.6 Performance with Large Arrays** + +**Issue**: If `topics_discussed` array grows very large over many scenarios, performance might degrade. + +**Likelihood**: Low - topics are finite and bounded by content + +**Mitigation**: Add max length validation: +```javascript +// In merge function +if (Array.isArray(value) && value.length > 1000) { + console.warn(`⚠️ Array ${varName} is very large (${value.length} items) - may impact performance`); +} +``` + +**3.7 Security Concerns (Future Server Integration)** + +**Issue**: User could craft malicious JSON to inject values. + +**Example**: +```json +{ + "variables": { + "npc_trust_guard": 9999999, // Cheating + "__proto__": { "isAdmin": true } // Prototype pollution + } +} +``` + +**Mitigation**: +1. Validate all incoming values on server +2. Use `Object.create(null)` for variable storage to prevent prototype pollution +3. Enforce min/max constraints +4. Sanitize before applying to game state + +**For MVP**: Not critical since persistent state is client-side file. + +**For Server Integration**: CRITICAL - add full validation layer. + +--- + +### 4. Missing Pieces & Improvements + +#### 📝 Documentation Gaps + +**4.1 Add JSDoc Comments** +The plan mentions functions but doesn't show full JSDoc. Add comprehensive documentation: + +```javascript +/** + * Persistent State Manager + * + * Manages cross-scenario variable persistence, allowing narrative state + * to carry over between different game scenarios. + * + * @class + * @example + * // In game.js + * const manager = new PersistentStateManager(); + * manager.extractCarryOverMetadata(scenario.globalVariables); + * const merged = manager.mergeWithScenarioDefaults(persistentState, scenario.globalVariables); + */ +class PersistentStateManager { + /** + * Extract carry-over metadata from scenario's globalVariables definition + * + * Parses both simple and metadata formats: + * - Simple: { "var": value } → carryOver: false + * - Metadata: { "var": { default, carryOver, type } } → uses flags + * + * @param {Object} scenarioGlobalVariables - globalVariables from scenario.json + * @returns {void} + * @throws {Error} If scenarioGlobalVariables is not an object + */ + extractCarryOverMetadata(scenarioGlobalVariables) { + // Implementation + } + + // ... etc for all functions +} +``` + +**4.2 Error Codes** +Add structured error codes for better debugging: + +```javascript +const PersistentStateErrors = { + INVALID_JSON: 'PSM_001', + TYPE_MISMATCH: 'PSM_002', + MISSING_METADATA: 'PSM_003', + LOAD_FAILED: 'PSM_004' +}; + +// Then use: +console.error(`[${PersistentStateErrors.LOAD_FAILED}] Failed to load persistent state file`); +``` + +This makes it easier to search logs and create documentation. + +**4.3 Add Migration Example** +Show how to handle schema changes when variables are renamed/restructured: + +```javascript +/** + * Migrate persistent state from older versions + * @param {Object} state - Loaded persistent state + * @returns {Object} Migrated state + */ +migrateState(state) { + const version = state.version || 0; + + // Version 0 → 1: Renamed npc_trust_guard to npc_trust_security_guard + if (version === 0 && state.variables.npc_trust_guard) { + state.variables.npc_trust_security_guard = state.variables.npc_trust_guard; + delete state.variables.npc_trust_guard; + console.log('✅ Migrated state from v0 to v1'); + } + + state.version = this.version; + return state; +} +``` + +Call this before merging: +```javascript +if (persistentStateJSON) { + const migrated = window.persistentStateManager.migrateState(persistentStateJSON); + window.persistentStateManager.loadedState = migrated; +} +``` + +#### 🔧 Technical Improvements + +**4.4 Add Validation Schema** +Instead of just type checking, use a validation schema: + +```javascript +class VariableValidator { + static validate(varName, value, metadata) { + // Type check + if (metadata.type && typeof value !== metadata.type) { + return { valid: false, error: `Type mismatch: expected ${metadata.type}` }; + } + + // Range check for numbers + if (typeof value === 'number') { + if (metadata.min !== undefined && value < metadata.min) { + return { valid: false, error: `Value ${value} below minimum ${metadata.min}` }; + } + if (metadata.max !== undefined && value > metadata.max) { + return { valid: false, error: `Value ${value} above maximum ${metadata.max}` }; + } + } + + // Array length check + if (Array.isArray(value) && metadata.maxLength && value.length > metadata.maxLength) { + return { valid: false, error: `Array too long: ${value.length} > ${metadata.maxLength}` }; + } + + return { valid: true }; + } +} +``` + +**4.5 Add Dry-Run Mode** +Useful for testing - load persistent state but don't apply it: + +```javascript +window.testPersistentState = (stateFile) => { + fetch(stateFile) + .then(r => r.json()) + .then(state => { + console.log('🧪 DRY RUN - Testing persistent state load'); + const merged = window.persistentStateManager.mergeWithScenarioDefaults( + state.variables, + window.gameScenario.globalVariables + ); + console.log('📊 Merged result:', merged); + console.log('✅ Dry run complete - no changes applied'); + }); +}; +``` + +**4.6 Add Diff View** +When loading persistent state, show what changed: + +```javascript +logStateDiff(defaults, merged) { + console.log('📊 Persistent State Diff:'); + console.table({ + ...Object.fromEntries( + Array.from(this.carryOverMetadata.entries()) + .filter(([varName, meta]) => meta.carryOver) + .map(([varName, meta]) => [ + varName, + { + default: meta.default, + loaded: merged[varName], + changed: merged[varName] !== meta.default ? '✅' : '' + } + ]) + ) + }); +} +``` + +#### 🎯 Feature Enhancements + +**4.7 Add Conditional Carry-Over** +Some variables might only carry over under certain conditions: + +```json +{ + "npc_trust_guard": { + "default": 0, + "carryOver": true, + "carryOverCondition": "narrative_guard_survived === true" + } +} +``` + +Implementation: +```javascript +// In merge function +if (metadata.carryOver) { + // Check condition if specified + if (metadata.carryOverCondition) { + const condition = this.evaluateCondition(metadata.carryOverCondition, persistentState); + if (!condition) { + console.log(`⏭️ Skipping carry-over for ${varName} (condition not met)`); + value = metadata.default; + continue; + } + } + // ... proceed with carry-over +} +``` + +**4.8 Add Variable Dependencies** +Some variables might depend on others: + +```json +{ + "character_romance_level": { + "default": 0, + "carryOver": true, + "requires": "character_met === true" + } +} +``` + +If requirement not met, reset to default. + +--- + +### 5. Testing Improvements + +#### ✅ Good Coverage + +The plan includes comprehensive test scenarios. Strong points: +- Fresh start test +- Continued game test +- Partial state test +- Cross-scenario test +- Backward compatibility test + +#### 📝 Suggested Additions + +**5.1 Add Automated Tests** +Create a test suite: + +```javascript +// tests/persistent-state.test.js +describe('PersistentStateManager', () => { + let manager; + + beforeEach(() => { + manager = new PersistentStateManager(); + }); + + test('extracts metadata from simple format', () => { + const scenario = { + simple_var: 42 + }; + manager.extractCarryOverMetadata(scenario); + expect(manager.carryOverMetadata.get('simple_var')).toEqual({ + default: 42, + carryOver: false, + type: 'number', + description: null + }); + }); + + test('extracts metadata from object format', () => { + const scenario = { + complex_var: { + default: 100, + carryOver: true, + type: 'number', + description: 'Test variable' + } + }; + manager.extractCarryOverMetadata(scenario); + expect(manager.carryOverMetadata.get('complex_var').carryOver).toBe(true); + }); + + test('merges persistent state correctly', () => { + const scenario = { + carry_var: { default: 0, carryOver: true }, + session_var: { default: 0, carryOver: false } + }; + const persistent = { + carry_var: 100, + session_var: 999 + }; + + manager.extractCarryOverMetadata(scenario); + const merged = manager.mergeWithScenarioDefaults(persistent, scenario); + + expect(merged.carry_var).toBe(100); // From persistent + expect(merged.session_var).toBe(0); // From default + }); + + test('handles missing persistent state gracefully', () => { + const scenario = { + some_var: { default: 42, carryOver: true } + }; + + manager.extractCarryOverMetadata(scenario); + const merged = manager.mergeWithScenarioDefaults(null, scenario); + + expect(merged.some_var).toBe(42); // Falls back to default + }); + + test('exports only carry-over variables', () => { + manager.carryOverMetadata.set('carry', { carryOver: true, default: 0 }); + manager.carryOverMetadata.set('session', { carryOver: false, default: 0 }); + + window.gameState = { + globalVariables: { + carry: 100, + session: 200 + } + }; + + const exported = manager.exportPersistentState('test_scenario'); + + expect(exported.variables.carry).toBe(100); + expect(exported.variables.session).toBeUndefined(); + }); +}); +``` + +**5.2 Add Integration Tests** +Test the full flow with a real scenario: + +```javascript +// tests/integration/persistent-state.integration.test.js +describe('Persistent State Integration', () => { + test('full flow: load scenario → modify vars → export → reload', async () => { + // 1. Load scenario with carry-over vars + const scenario = loadScenario('test_scenario'); + expect(scenario.globalVariables.npc_trust.carryOver).toBe(true); + + // 2. Initialize game state + initializeGameState(scenario); + expect(window.gameState.globalVariables.npc_trust).toBe(0); + + // 3. Modify variable + window.gameState.globalVariables.npc_trust = 75; + + // 4. Export state + const exported = window.exportPersistentState(); + expect(exported.variables.npc_trust).toBe(75); + + // 5. Reload game with persistent state + resetGameState(); + initializeGameState(scenario, exported.variables); + + // 6. Verify variable persisted + expect(window.gameState.globalVariables.npc_trust).toBe(75); + }); +}); +``` + +**5.3 Add Stress Tests** + +Test with edge cases: +- 1000+ variables +- Very large arrays (10,000 items) +- Deeply nested objects +- Special characters in variable names +- Unicode values + +--- + +### 6. Success Factors + +#### 🎯 What Will Make This Successful + +**6.1 Developer Experience** +✅ **Clear Documentation** - The plan includes good docs +✅ **Helpful Errors** - Logging is comprehensive +✅ **Easy Testing** - Console commands make testing simple +⚠️ **Examples** - Need more real-world Ink examples + +**Recommendation**: Add a complete example scenario showing: +- Variable definition in scenario.json +- Ink script using variables +- Cross-scenario references +- Export trigger placement + +**6.2 Content Creator Experience** + +Add tools for scenario designers: +```javascript +window.listCarryOverVariables = () => { + const table = {}; + window.persistentStateManager.carryOverMetadata.forEach((meta, varName) => { + if (meta.carryOver) { + table[varName] = { + type: meta.type, + default: meta.default, + current: window.gameState.globalVariables[varName], + description: meta.description || 'N/A' + }; + } + }); + console.table(table); +}; +``` + +**6.3 Debugging Support** + +Add verbose mode: +```javascript +window.persistentStateManager.debugMode = true; + +// Then in code: +if (this.debugMode) { + console.log('🔍 [DEBUG] Merging variable:', varName, { + metadata: metadata, + persistentValue: persistentState?.[varName], + finalValue: value + }); +} +``` + +**6.4 Progressive Enhancement** + +The plan supports gradual adoption: +1. Start with simple `carryOver: true` flags +2. Add types later +3. Add validation later +4. Add conditions later + +This is good - allows quick MVP and future enhancements. + +**6.5 Clear Migration Path to Server** + +The plan includes hooks for future server integration: +```javascript +async postToServer(endpoint, authToken) { + // TODO: Implement in Phase 2 +} +``` + +**Improvement**: Add more detail on what server API should look like: + +```javascript +/** + * POST persistent state to server + * + * Expected server endpoint: + * POST /api/persistent-state + * Headers: Authorization: Bearer {token} + * Body: { version, timestamp, lastScenario, variables } + * + * Response: + * { success: true, savedAt: timestamp, stateId: uuid } + * + * @param {string} endpoint - Server API endpoint + * @param {string} authToken - JWT or API key + * @returns {Promise} Server response + */ +async postToServer(endpoint, authToken) { + const scenarioId = window.gameScenario?.scenario_id || 'unknown'; + const state = this.exportPersistentState(scenarioId); + + const response = await fetch(endpoint, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${authToken}` + }, + body: JSON.stringify(state) + }); + + if (!response.ok) { + throw new Error(`Failed to save state: ${response.statusText}`); + } + + return await response.json(); +} +``` + +--- + +### 7. Final Recommendations + +#### 🚀 High Priority (Do Before Implementation) + +1. **Add Deep Clone Function** - Prevent object reference bugs +2. **Add Type Validation** - Catch type mismatches early +3. **Add File Load Error Handling** - Better UX when file missing +4. **Add State Migration Function** - Future-proof for schema changes +5. **Add JSDoc Comments** - Improve maintainability + +#### 🎯 Medium Priority (Do During Implementation) + +6. **Add Automated Tests** - Ensure reliability +7. **Add Debug Mode** - Easier troubleshooting +8. **Add Variable Namespace Documentation** - Prevent collisions +9. **Add Diff Viewer** - See what changed when loading state +10. **Add More Ink Examples** - Show best practices + +#### ⭐ Low Priority (Nice to Have) + +11. **Add Dry-Run Mode** - Test state loading without applying +12. **Add Validation Schema** - Min/max/maxLength constraints +13. **Add Conditional Carry-Over** - Advanced feature for later +14. **Add Stress Tests** - Performance validation +15. **Add Visual Notification** - In-game feedback for load errors + +--- + +## Conclusion + +**Final Verdict**: ✅ **APPROVED FOR IMPLEMENTATION** + +This plan is solid and ready for development. The architecture is sound, integration points are clear, and the implementation path is well-defined. + +**Estimated Implementation Time**: 2-3 days for MVP +- Day 1: Core module + game.js integration +- Day 2: Console commands + export triggers +- Day 3: Testing + documentation + examples + +**Risk Level**: 🟢 **LOW** +- No breaking changes to existing systems +- Good backward compatibility +- Clear rollback path (just don't use persistent state param) + +**Next Steps**: +1. Incorporate high-priority recommendations +2. Create feature branch +3. Implement PersistentStateManager module +4. Test with example scenario +5. Document usage +6. Deploy to production + +--- + +## Appendix: Code Snippets for Key Improvements + +### A1. Enhanced Merge Function with Validation + +```javascript +mergeWithScenarioDefaults(persistentState, scenarioGlobalVariables) { + const merged = {}; + + // First extract metadata if not already done + if (this.carryOverMetadata.size === 0) { + this.extractCarryOverMetadata(scenarioGlobalVariables); + } + + this.carryOverMetadata.forEach((metadata, varName) => { + let value = structuredClone(metadata.default); // Deep clone + + // Only override with persistent state if this is a carry-over variable + if (metadata.carryOver && persistentState && varName in persistentState) { + const persistentValue = persistentState[varName]; + + // Type validation + const expectedType = Array.isArray(metadata.default) ? 'array' : typeof metadata.default; + const actualType = Array.isArray(persistentValue) ? 'array' : typeof persistentValue; + + if (expectedType !== actualType) { + console.warn(`⚠️ [PSM_002] Type mismatch for ${varName}:`); + console.warn(` Expected: ${expectedType}, Got: ${actualType}`); + console.warn(` Using default value: ${metadata.default}`); + } else { + value = structuredClone(persistentValue); // Deep clone + console.log(`✅ Loaded ${varName} from persistent state:`, value); + } + } else if (metadata.carryOver) { + console.log(`ℹ️ No persistent value for ${varName}, using default:`, value); + } + + merged[varName] = value; + }); + + return merged; +} +``` + +### A2. Enhanced Load with Error Handling + +```javascript +// In game.js create() +if (urlParams.has('persistentState')) { + const persistentStateJSON = this.cache.json.get('persistentStateJSON'); + + if (!persistentStateJSON) { + const errorMsg = '⚠️ Persistent state file failed to load'; + console.error(`❌ [PSM_004] ${errorMsg}`); + console.error(' File may be missing, malformed, or network error occurred'); + console.error(' Proceeding with scenario defaults'); + + // Show in-game notification + const notification = this.add.text( + this.cameras.main.width / 2, + 20, + errorMsg, + { + fontSize: '18px', + backgroundColor: '#cc0000', + padding: { x: 15, y: 8 }, + color: '#ffffff' + } + ).setOrigin(0.5, 0).setDepth(10000).setScrollFactor(0); + + this.time.delayedCall(5000, () => notification.destroy()); + } else { + try { + // Validate structure + if (!persistentStateJSON.version || !persistentStateJSON.variables) { + throw new Error('Invalid persistent state structure'); + } + + console.log('✅ Persistent state loaded successfully'); + console.log(` Version: ${persistentStateJSON.version}`); + console.log(` Last Scenario: ${persistentStateJSON.lastScenario}`); + console.log(` Variables: ${Object.keys(persistentStateJSON.variables).length}`); + + window.persistentStateManager.loadedState = persistentStateJSON; + } catch (error) { + console.error('❌ [PSM_001] Invalid persistent state JSON:', error.message); + console.error(' Proceeding with scenario defaults'); + } + } +} +``` + +### A3. Enhanced Export with Metadata + +```javascript +exportPersistentState(lastScenarioId = 'unknown') { + const exported = { + version: this.version, + timestamp: new Date().toISOString(), + lastScenario: lastScenarioId, + variables: {}, + metadata: { // Optional: include metadata for debugging + exportedCount: 0, + sessionOnlyCount: 0 + } + }; + + let exportedCount = 0; + let sessionOnlyCount = 0; + + this.carryOverMetadata.forEach((metadata, varName) => { + if (metadata.carryOver) { + if (window.gameState.globalVariables.hasOwnProperty(varName)) { + exported.variables[varName] = structuredClone( + window.gameState.globalVariables[varName] + ); + exportedCount++; + } + } else { + sessionOnlyCount++; + } + }); + + exported.metadata.exportedCount = exportedCount; + exported.metadata.sessionOnlyCount = sessionOnlyCount; + + console.log(`📤 Exported persistent state:`); + console.log(` Scenario: ${lastScenarioId}`); + console.log(` Carried over: ${exportedCount} variables`); + console.log(` Session-only: ${sessionOnlyCount} variables (not exported)`); + + return exported; +} +``` + +--- + +**Review Complete** ✅