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
| Field | Description | Persistence |
|---|---|---|
currentNodeId | Active narrative node identifier | Session |
choiceHistory | Complete chronological choice log | Session + Meta |
narrativeFlags | Boolean story state markers | Session |
dialogueHistory | All seen dialogue (for context) | Session |
branchAccess | Which story branches visited | Meta |
2.2 NPC Relationship System
| Field | Description | Persistence |
|---|---|---|
npcStates | Individual NPC relationship data | Session |
trustScores | -100 to +100 trust per NPC | Session |
betrayalLog | Who betrayed whom, when | Session + Meta |
memoryReferences | NPC-specific player history | Meta |
greetingVariants | Which greeting used last | Session |
2.3 Well System (Time Pressure)
| Field | Description | Persistence |
|---|---|---|
waterLevel | Current well depth (0-100%) | Session |
drainRate | Current drainage speed | Session |
lastUpdateTime | Timestamp for drain calculation | Session |
criticalMoments | Times water hit critical | Meta |
2.4 Meta-Progression
| Field | Description | Persistence |
|---|---|---|
playSessionCount | Total times game launched | Meta |
endingsDiscovered | Array of unlocked ending IDs | Meta |
totalPlaytime | Cumulative seconds played | Meta |
secretFlags | Easter egg unlock states | Meta |
playerTendencies | Choice pattern analysis | Meta |
ghostData | Aggregate choice statistics | Server |
2.5 System State
| Field | Description | Persistence |
|---|---|---|
saveVersion | Schema version for migration | Meta |
firstPlayDate | Initial play timestamp | Meta |
lastSaveTime | Most recent save timestamp | Session |
deviceId | Anonymous device fingerprint | Meta |
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:
| Event | Priority | Debounce |
|---|---|---|
| After every choice selection | HIGH | 0ms |
| On narrative node transition | HIGH | 0ms |
| Every 30 seconds of active play | MEDIUM | 5000ms |
| When entering new act/section | HIGH | 0ms |
| Before critical choice presentation | HIGH | 0ms |
| When water level changes significantly (>5%) | MEDIUM | 1000ms |
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):
-
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; } -
Backup Save Fallback
- Check
moloch_save_backup(auto-created on every save) - Check
moloch_emergency_save(from beforeunload) - Check IndexedDB archives
- Check
-
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.”
-
Graceful Degradation
- If all recovery fails, start fresh
- Preserve
metadata 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:
| Type | Description | Example |
|---|---|---|
immediate | Current playthrough only | ”You lied to me earlier” |
persistent | Across all playthroughs | ”I remember you from before” |
collective | Aggregate 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:
| Session | NPC Behavior | Unlocks |
|---|---|---|
| 1 | Standard first-play experience | - |
| 2 | Subtle hints of recognition | ”Deja Vu” secret flag |
| 3 | Direct acknowledgment | Bonus dialogue options |
| 5 | Meta-commentary on repetition | ”Groundhog Day” reference |
| 10 | Deep lore references | Developer easter eggs |
| 20+ | Breaking fourth wall | True 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
- Save during choice, force close, resume
- Corrupt save file, verify recovery
- Version downgrade (should fail gracefully)
- Storage quota exceeded
- Multiple rapid saves (debounce)
- 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