MOLOCH - Save State Architecture

Technical Specification Document v1.0


1. OVERVIEW

The Moloch save system is designed to capture meaningful persistent state that enables:

  • Seamless session resumption
  • Cross-playthrough memory (NPCs remember)
  • Replayability with variation tracking
  • Ghost mode aggregation
  • Meta-progression persistence

Core Philosophy: Every player action matters and persists. The game “remembers” across sessions, playthroughs, and even browser instances.


2. WHAT GETS SAVED

2.1 Core Narrative State

FieldDescriptionPersistence
currentNodeIdActive narrative node identifierSession
choiceHistoryComplete chronological choice logSession + Meta
narrativeFlagsBoolean story state markersSession
dialogueHistoryAll seen dialogue (for context)Session
branchAccessWhich story branches visitedMeta

2.2 NPC Relationship System

FieldDescriptionPersistence
npcStatesIndividual NPC relationship dataSession
trustScores-100 to +100 trust per NPCSession
betrayalLogWho betrayed whom, whenSession + Meta
memoryReferencesNPC-specific player historyMeta
greetingVariantsWhich greeting used lastSession

2.3 Well System (Time Pressure)

FieldDescriptionPersistence
waterLevelCurrent well depth (0-100%)Session
drainRateCurrent drainage speedSession
lastUpdateTimeTimestamp for drain calculationSession
criticalMomentsTimes water hit criticalMeta

2.4 Meta-Progression

FieldDescriptionPersistence
playSessionCountTotal times game launchedMeta
endingsDiscoveredArray of unlocked ending IDsMeta
totalPlaytimeCumulative seconds playedMeta
secretFlagsEaster egg unlock statesMeta
playerTendenciesChoice pattern analysisMeta
ghostDataAggregate choice statisticsServer

2.5 System State

FieldDescriptionPersistence
saveVersionSchema version for migrationMeta
firstPlayDateInitial play timestampMeta
lastSaveTimeMost recent save timestampSession
deviceIdAnonymous device fingerprintMeta

3. SAVE STATE SCHEMA (JSON)

{
  "_schema": "moloch-save-v1.0",
  "_timestamp": "2024-01-15T14:32:18.452Z",
  "_checksum": "sha256:abc123...",
  
  "session": {
    "currentNodeId": "act2_well_confrontation",
    "sessionId": "sess_8f3a9b2c",
    "sessionStartTime": "2024-01-15T14:00:00.000Z",
    "sessionPlaytime": 1938,
    
    "choiceHistory": [
      {
        "nodeId": "intro_arrival",
        "choiceId": "choice_trust_no_one",
        "timestamp": "2024-01-15T14:05:22.100Z",
        "gameTime": 300
      },
      {
        "nodeId": "meeting_elder",
        "choiceId": "choice_lie_about_past",
        "timestamp": "2024-01-15T14:12:45.330Z",
        "gameTime": 740
      }
    ],
    
    "narrativeFlags": {
      "knowsAboutRitual": true,
      "suspiciousOfMarcus": true,
      "foundHiddenKey": false,
      "warnedVillagers": true,
      "madeBloodPact": false
    },
    
    "dialogueHistory": {
      "seenNodes": ["intro_arrival", "meeting_elder", "well_discovery"],
      "seenDialogueIds": ["dlg_001", "dlg_002", "dlg_015"],
      "dialogueReadCount": {
        "dlg_001": 1,
        "dlg_015": 2
      }
    }
  },
  
  "npcs": {
    "elder_marcus": {
      "trustScore": 35,
      "relationship": "cautious",
      "betrayedByPlayer": false,
      "betrayedPlayer": false,
      "secretsShared": ["ritual_purpose"],
      "secretsWithheld": ["true_age", "past_failures"],
      "greetingVariant": 2,
      "lastInteraction": "2024-01-15T14:12:45.330Z",
      "memoryReferences": ["player_lied_about_past"]
    },
    "villager_sarah": {
      "trustScore": -20,
      "relationship": "suspicious",
      "betrayedByPlayer": true,
      "betrayedPlayer": false,
      "secretsShared": [],
      "secretsWithheld": ["family_secret"],
      "greetingVariant": 1,
      "lastInteraction": "2024-01-15T14:08:12.500Z",
      "memoryReferences": ["player_betrayed_confidence"]
    },
    "stranger_cain": {
      "trustScore": 60,
      "relationship": "allied",
      "betrayedByPlayer": false,
      "betrayedPlayer": false,
      "secretsShared": ["escape_route", "hidden_supplies"],
      "secretsWithheld": ["true_identity"],
      "greetingVariant": 3,
      "lastInteraction": "2024-01-15T14:20:33.890Z",
      "memoryReferences": ["player_shared_food"]
    }
  },
  
  "well": {
    "waterLevel": 67.5,
    "maxLevel": 100,
    "minLevel": 0,
    "drainRate": 0.5,
    "drainMultiplier": 1.0,
    "lastUpdateTime": "2024-01-15T14:32:18.452Z",
    "criticalThreshold": 15,
    "drainEvents": [
      {
        "time": "2024-01-15T14:15:00.000Z",
        "amount": 5,
        "reason": "choice_consequence"
      }
    ],
    "hasBeenCritical": false
  },
  
  "meta": {
    "playSessionCount": 3,
    "totalPlaytime": 12450,
    "firstPlayDate": "2024-01-10T09:00:00.000Z",
    "endingsDiscovered": ["ending_sacrifice", "ending_escape"],
    "allEndings": ["ending_sacrifice", "ending_escape", "ending_betrayal", "ending_ascension", "ending_damnation", "ending_redemption", "ending_cycle", "ending_truth"],
    
    "secretFlags": {
      "foundDevelopersNote": true,
      "unlockedGhostMode": true,
      "discoveredTrueName": false,
      "sawHiddenEnding": false,
      "achievedSpeedRun": false,
      "completedAllEndings": false
    },
    
    "playerTendencies": {
      "totalChoices": 47,
      "trustChoices": 18,
      "betrayChoices": 12,
      "neutralChoices": 17,
      "preferredApproach": "calculated",
      "riskTolerance": 0.65,
      "explorationScore": 0.78,
      "completionistTendency": true
    },
    
    "branchAccess": {
      "act1": ["village_arrival", "well_discovery", "ritual_prep"],
      "act2": ["confrontation", "investigation"],
      "act3": []
    },
    
    "previousPlaythroughs": [
      {
        "ending": "ending_sacrifice",
        "playDate": "2024-01-10T11:30:00.000Z",
        "duration": 3600,
        "keyChoices": ["choice_sacrifice_self"]
      },
      {
        "ending": "ending_escape",
        "playDate": "2024-01-12T20:15:00.000Z",
        "duration": 4200,
        "keyChoices": ["choice_flee_village"]
      }
    ],
    
    "newGamePlus": {
      "unlocked": true,
      "bonusDialogue": ["dlg_returning_player_1", "dlg_returning_player_2"],
      "knowledgeCarryover": ["knowsRitualTruePurpose", "knowsElderSecret"],
      "attemptedStrategies": ["strategy_persuade_elder", "strategy_sabotage_ritual"]
    }
  },
  
  "settings": {
    "ghostModeEnabled": true,
    "showPercentages": true,
    "audioEnabled": true,
    "textSpeed": "normal",
    "contrastMode": false
  },
  
  "ghost": {
    "aggregateData": {
      "choice_trust_no_one": { "count": 15420, "percentage": 34.5 },
      "choice_trust_elder": { "count": 29340, "percentage": 65.5 },
      "choice_lie_about_past": { "count": 8900, "percentage": 19.8 },
      "choice_tell_truth": { "count": 35860, "percentage": 80.2 }
    },
    "lastSync": "2024-01-15T14:00:00.000Z"
  }
}

4. SAVE TRIGGERS

4.1 Auto-Save System

Trigger Points:

EventPriorityDebounce
After every choice selectionHIGH0ms
On narrative node transitionHIGH0ms
Every 30 seconds of active playMEDIUM5000ms
When entering new act/sectionHIGH0ms
Before critical choice presentationHIGH0ms
When water level changes significantly (>5%)MEDIUM1000ms

Implementation:

class SaveManager {
  constructor() {
    this.saveQueue = [];
    this.isSaving = false;
    this.debounceTimers = new Map();
  }
  
  autoSave(trigger, priority = 'medium') {
    // Debounce handling
    if (this.debounceTimers.has(trigger)) {
      clearTimeout(this.debounceTimers.get(trigger));
    }
    
    const delay = this.getDebounceDelay(priority);
    
    this.debounceTimers.set(trigger, setTimeout(() => {
      this.performSave();
    }, delay));
  }
  
  performSave() {
    const saveData = this.serializeGameState();
    const checksum = this.generateChecksum(saveData);
    
    // Primary: localStorage
    this.saveToLocalStorage(saveData, checksum);
    
    // Secondary: IndexedDB (for larger data)
    this.saveToIndexedDB(saveData, checksum);
    
    // Tertiary: Session backup (in-memory)
    this.sessionBackup = { ...saveData, checksum };
  }
}

4.2 Manual Save

User Interface:

  • Save button in pause menu (if implemented)
  • Keyboard shortcut: Ctrl+S (desktop)
  • Visual confirmation: “Game Saved” toast (1.5s)

Behavior:

  • Creates named save slot
  • Includes timestamp in save name
  • Maximum 5 manual saves (auto-rotate oldest)

4.3 Tab Close / Page Unload

Implementation:

// Beforeunload handler
window.addEventListener('beforeunload', (e) => {
  if (gameState.hasUnsavedChanges) {
    // Attempt synchronous save
    const saveData = JSON.stringify(gameState.serialize());
    localStorage.setItem('moloch_emergency_save', saveData);
    
    // Show browser warning if mid-choice
    if (gameState.inCriticalMoment) {
      e.preventDefault();
      e.returnValue = 'You are in a critical moment. Your progress will be saved, but you may miss important context.';
    }
  }
});
 
// Visibility change (tab switching)
document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    saveManager.performSave();
    
    // Pause well drain while tab inactive
    wellSystem.pauseDrain();
  } else {
    wellSystem.resumeDrain();
  }
});

4.4 Save Storage Strategy

Storage Hierarchy:
┌─────────────────────────────────────────┐
│  L1: In-Memory (sessionBackup)         │ ← Fastest, ephemeral
│     - Current game state               │
├─────────────────────────────────────────┤
│  L2: localStorage (moloch_save_main)   │ ← Primary persistence
│     - Complete save data               │
│     - ~5MB limit                       │
├─────────────────────────────────────────┤
│  L3: IndexedDB (moloch_db)             │ ← Large data, history
│     - Choice history archives          │
│     - Ghost mode cache                 │
│     - Unlimited (browser dependent)    │
├─────────────────────────────────────────┤
│  L4: Server (optional)                 │ ← Cross-device, ghost data
│     - Aggregate statistics             │
│     - Cloud save (if enabled)          │
└─────────────────────────────────────────┘

5. RESUME LOGIC

5.1 State Restoration Flow

Game Launch
    │
    ▼
Check for existing save?
    │
    ├── NO ──► New Game Flow
    │
    └── YES ──► Validate Save Integrity
                    │
                    ├── VALID ──► Restore State
                    │                │
                    │                ├── Restore current node
                    │                ├── Restore NPC states
                    │                ├── Restore well level
                    │                ├── Check version compatibility
                    │                └── Resume gameplay
                    │
                    └── INVALID ──► Recovery Options
                                        │
                                        ├── Attempt repair
                                        ├── Load backup save
                                        └── Offer new game (with apology)

5.2 Save Validation

class SaveValidator {
  validate(saveData) {
    const checks = [
      this.validateSchema(saveData),
      this.validateChecksum(saveData),
      this.validateRequiredFields(saveData),
      this.validateDataTypes(saveData),
      this.validateConsistency(saveData)
    ];
    
    return {
      valid: checks.every(c => c.passed),
      errors: checks.filter(c => !c.passed).map(c => c.error),
      recoverable: this.isRecoverable(checks)
    };
  }
  
  validateSchema(data) {
    const required = ['_schema', 'session', 'npcs', 'well', 'meta'];
    const missing = required.filter(field => !(field in data));
    
    return {
      passed: missing.length === 0,
      error: missing.length > 0 ? `Missing fields: ${missing.join(', ')}` : null
    };
  }
  
  validateChecksum(data) {
    const storedChecksum = data._checksum;
    const computedChecksum = this.generateChecksum(data);
    
    return {
      passed: storedChecksum === computedChecksum,
      error: storedChecksum !== computedChecksum ? 'Checksum mismatch - save may be corrupted' : null
    };
  }
}

5.3 Corrupted Save Recovery

Recovery Strategies (in order):

  1. Auto-Repair Attempt

    attemptRepair(corruptedSave) {
      const repaired = { ...corruptedSave };
      
      // Fix missing fields with defaults
      if (!repaired.session) repaired.session = this.getDefaultSession();
      if (!repaired.npcs) repaired.npcs = this.getDefaultNPCs();
      if (!repaired.well) repaired.well = this.getDefaultWell();
      
      // Fix data type issues
      if (typeof repaired.well.waterLevel === 'string') {
        repaired.well.waterLevel = parseFloat(repaired.well.waterLevel);
      }
      
      // Recompute checksum
      repaired._checksum = this.generateChecksum(repaired);
      
      return repaired;
    }
  2. Backup Save Fallback

    • Check moloch_save_backup (auto-created on every save)
    • Check moloch_emergency_save (from beforeunload)
    • Check IndexedDB archives
  3. Partial Recovery

    • Restore meta-progression (endings, secrets)
    • Start new session but acknowledge returning player
    • Show message: “Your journey’s echoes remain, though the path has shifted.”
  4. Graceful Degradation

    • If all recovery fails, start fresh
    • Preserve meta data if readable
    • Unlock “Lost and Found” secret flag

5.4 Version Migration

Migration System:

const SCHEMA_VERSIONS = {
  '1.0': {
    next: '1.1',
    migrate: (data) => data // Base version
  },
  '1.1': {
    next: null,
    migrate: (data) => {
      // Example: Add new field in v1.1
      return {
        ...data,
        meta: {
          ...data.meta,
          newGamePlus: data.meta.newGamePlus || { unlocked: false }
        }
      };
    }
  }
};
 
class MigrationManager {
  migrate(saveData) {
    const currentVersion = saveData._schema?.split('-v')[1] || '1.0';
    const targetVersion = CURRENT_SCHEMA_VERSION;
    
    if (currentVersion === targetVersion) return saveData;
    
    let migrated = { ...saveData };
    let version = currentVersion;
    
    while (version !== targetVersion && SCHEMA_VERSIONS[version]) {
      console.log(`Migrating save from v${version}...`);
      migrated = SCHEMA_VERSIONS[version].migrate(migrated);
      migrated._schema = `moloch-save-v${SCHEMA_VERSIONS[version].next}`;
      version = SCHEMA_VERSIONS[version].next;
    }
    
    return migrated;
  }
}

6. CROSS-SESSION MEMORY

6.1 NPC Memory System

Memory Categories:

TypeDescriptionExample
immediateCurrent playthrough only”You lied to me earlier”
persistentAcross all playthroughs”I remember you from before”
collectiveAggregate player behavior”Many have tried what you’re attempting”

Implementation:

class NPCMemorySystem {
  constructor(npcId, saveData) {
    this.npcId = npcId;
    this.sessionMemory = saveData.npcs[npcId]?.memoryReferences || [];
    this.metaMemory = saveData.meta.previousPlaythroughs || [];
    this.playSessionCount = saveData.meta.playSessionCount;
  }
  
  generateGreeting() {
    const variants = [];
    
    // First-time greeting
    if (this.playSessionCount === 1) {
      variants.push(this.getGreeting('first_meeting'));
    }
    
    // Returning player recognition
    if (this.playSessionCount > 1) {
      variants.push(this.getGreeting('returning_player', {
        sessionCount: this.playSessionCount
      }));
    }
    
    // Specific memory callback
    if (this.sessionMemory.includes('player_betrayed_confidence')) {
      variants.push(this.getGreeting('betrayal_remembered'));
    }
    
    // Previous ending reference
    const lastEnding = this.metaMemory[this.metaMemory.length - 1]?.ending;
    if (lastEnding) {
      variants.push(this.getGreeting('ending_reference', { ending: lastEnding }));
    }
    
    return this.selectVariant(variants);
  }
}

6.2 “I Remember You” Moments

Trigger Conditions:

const MEMORY_TRIGGERS = {
  // Returning after specific ending
  post_sacrifice: {
    condition: (meta) => meta.endingsDiscovered.includes('ending_sacrifice'),
    dialogue: "You gave everything once before. What will you sacrifice this time?",
    weight: 0.8
  },
  
  // Multiple betrayals across playthroughs
  serial_betrayer: {
    condition: (meta) => {
      const betrayalCount = meta.previousPlaythroughs.filter(
        p => p.keyChoices.includes('choice_betray')
      ).length;
      return betrayalCount >= 2;
    },
    dialogue: "I've watched you betray trust time and again. Why should this time be different?",
    weight: 0.9
  },
  
  // Completionist recognition
  completionist: {
    condition: (meta) => meta.endingsDiscovered.length >= 6,
    dialogue: "You've walked so many paths... yet here you are again. Searching for what remains hidden?",
    weight: 1.0
  },
  
  // Speed runner
  speed_demon: {
    condition: (meta) => meta.secretFlags.achievedSpeedRun,
    dialogue: "You move with such urgency. What are you rushing toward? Or away from?",
    weight: 0.7
  }
};

6.3 Dynamic Dialogue Insertion

Template System:

const DYNAMIC_TEMPLATES = {
  returning_player: [
    "Ah, you've returned. Session {{sessionCount}} for you, isn't it?",
    "The well remembers those who've peered into it before.",
    "Back again? Some cannot resist the call.",
    "{{sessionCount}} times now. What are you seeking that you haven't found?"
  ],
  
  ending_reference: {
    ending_sacrifice: "You gave yourself once. Was it worth it?",
    ending_escape: "You ran before. What brings you back to this cursed place?",
    ending_betrayal: "I've seen how you treat allies. Choose your companions wisely this time.",
    ending_ascension: "You reached for godhood and fell. Hubris, perhaps?"
  },
  
  betrayal_remembered: [
    "Your word meant nothing last time.",
    "I haven't forgotten what you did. Have you?",
    "Trust is earned, and you spent yours poorly."
  ]
};

6.4 Session Count Integration

Progressive Recognition:

SessionNPC BehaviorUnlocks
1Standard first-play experience-
2Subtle hints of recognition”Deja Vu” secret flag
3Direct acknowledgmentBonus dialogue options
5Meta-commentary on repetition”Groundhog Day” reference
10Deep lore referencesDeveloper easter eggs
20+Breaking fourth wallTrue ending hints

7. IMPLEMENTATION NOTES

7.1 Performance Considerations

  • Save size limit: Target <100KB per save
  • Compression: Use LZ-string for localStorage
  • Throttling: Max 1 save per second except critical moments
  • Async operations: All saves non-blocking

7.2 Privacy & Security

  • No PII stored
  • Device ID is hashed fingerprint, not identifiable
  • Ghost data is anonymized aggregate only
  • Optional: Allow players to opt-out of ghost data contribution

7.3 Testing Scenarios

  1. Save during choice, force close, resume
  2. Corrupt save file, verify recovery
  3. Version downgrade (should fail gracefully)
  4. Storage quota exceeded
  5. Multiple rapid saves (debounce)
  6. Tab switch during critical moment

8. API REFERENCE

// SaveManager public API
interface SaveManager {
  save(): Promise<void>;
  load(): Promise<SaveData | null>;
  validate(data: SaveData): ValidationResult;
  migrate(data: SaveData): SaveData;
  export(): string; // Base64 encoded save for sharing
  import(encoded: string): SaveData;
  clear(): void;
}
 
// NPCMemory public API
interface NPCMemory {
  recordInteraction(type: string, data: any): void;
  getGreeting(): string;
  getDialogueModifiers(): Modifier[];
  hasMemory(key: string): boolean;
}

Document Version: 1.0 Last Updated: 2024 For: Moloch Interactive Experience