MOLOCH - Replayability System Specification
Technical Design Document v1.0
1. OVERVIEW
Moloch is designed for infinite replayability through:
- Procedural narrative variation
- Persistent cross-session memory
- Social ghost mode integration
- Time-pressure urgency
- Meta-progression rewards
Core Loop: Each playthrough reveals new layers while acknowledging player history.
2. VARIATION ON EACH PLAY
2.1 Rotating Villain System
Concept: The “true antagonist” shifts each playthrough, changing NPC roles, dialogue, and available endings.
Villain Pool:
| Villain ID | Role | Motivation | Unique Ending |
|---|---|---|---|
villain_elder | Village Elder | Preserve tradition at any cost | ending_sacrifice |
villain_stranger | Mysterious Stranger | Escape their own dark past | ending_betrayal |
villain_villager | Trusted Villager | Protect family secret | ending_damnation |
villain_player | The Player Themselves | Unknowingly the source | ending_truth |
villain_well | The Well Entity | Feed its endless hunger | ending_ascension |
villain_none | No true villain | Cycle of misunderstanding | ending_redemption |
Selection Algorithm:
class VillainRotationSystem {
constructor(metaData) {
this.discoveredEndings = metaData.endingsDiscovered;
this.playSessionCount = metaData.playSessionCount;
this.previousVillains = metaData.previousPlaythroughs.map(p => p.villain);
}
selectVillain() {
const candidates = this.getCandidateVillains();
const weights = this.calculateWeights(candidates);
return this.weightedRandomSelect(candidates, weights);
}
getCandidateVillains() {
// First 3 plays: guaranteed different villains
if (this.playSessionCount <= 3) {
return VILLAIN_POOL.filter(v => !this.previousVillains.includes(v.id));
}
// After 3 plays: all villains available, weighted toward unseen
return VILLAIN_POOL;
}
calculateWeights(candidates) {
return candidates.map(villain => {
let weight = 1.0;
// Boost unseen villains
if (!this.previousVillains.includes(villain.id)) {
weight *= 2.5;
}
// Reduce recently played villains
const lastPlayed = this.previousVillains.lastIndexOf(villain.id);
if (lastPlayed !== -1) {
const playsAgo = this.previousVillains.length - lastPlayed;
weight *= Math.min(playsAgo / 3, 1.0);
}
// Special: villain_player only after 5+ plays
if (villain.id === 'villain_player' && this.playSessionCount < 5) {
weight = 0;
}
// Special: villain_well only after seeing 3+ endings
if (villain.id === 'villain_well' && this.discoveredEndings.length < 3) {
weight = 0;
}
return weight;
});
}
}Villain Impact on Gameplay:
const VILLAIN_CONFIG = {
villain_elder: {
// Changes NPC trust curves
trustModifiers: {
elder_marcus: +30, // Starts trusted
villager_sarah: -10 // Suspicious of elder
},
// Hides specific clues
hiddenClues: ['stranger_true_identity'],
// Unique dialogue trees
exclusiveDialogue: ['dlg_elder_defends_tradition'],
// Affects which endings possible
availableEndings: ['ending_sacrifice', 'ending_cycle', 'ending_redemption'],
// Changes well behavior
wellModifier: { drainRate: 0.8 } // Slower drain (elder controls ritual)
},
villain_stranger: {
trustModifiers: {
stranger_cain: +20, // Seems helpful at first
elder_marcus: -20 // Elder distrusts outsider
},
hiddenClues: ['elder_ritual_knowledge'],
exclusiveDialogue: ['dlg_stranger_seeks_ally'],
availableEndings: ['ending_betrayal', 'ending_escape', 'ending_truth'],
wellModifier: { drainRate: 1.2 } // Faster drain (stranger accelerates)
}
};2.2 Random Event System
Event Categories:
| Category | Frequency | Impact |
|---|---|---|
ambient | Every 2-3 minutes | Atmosphere, minor trust shifts |
contextual | Triggered by choices | Significant branch changes |
critical | 1-2 per playthrough | Major revelation or choice |
rare | ~10% chance | Unique scenes, easter eggs |
Event Examples:
const RANDOM_EVENTS = {
// Ambient events
'event_wind_whispers': {
category: 'ambient',
condition: (state) => state.well.waterLevel < 50,
text: "The wind carries whispers from the well... voices you almost recognize.",
effect: (state) => { state.npcs.all.trustScore -= 2; }
},
// Contextual events
'event_villager_argument': {
category: 'contextual',
trigger: 'after_choice_betray_confidence',
text: "You overhear two villagers arguing about trust... and your name is mentioned.",
choices: [
{ id: 'confront', text: 'Confront them', effect: 'reveal_betrayal' },
{ id: 'listen', text: 'Listen from shadows', effect: 'gain_info' },
{ id: 'ignore', text: 'Walk away', effect: 'status_quo' }
]
},
// Critical events
'event_well_speaks': {
category: 'critical',
condition: (state) => state.well.waterLevel < 15 && state.playtime > 1800,
text: "THE WELL SPEAKS. Its voice is your own, but twisted. 'How many times will you fail?'",
effect: (state) => { state.meta.secretFlags.heardWellSpeak = true; }
},
// Rare events
'event_developer_ghost': {
category: 'rare',
chance: 0.05,
condition: (state) => state.meta.playSessionCount > 10,
text: "For a moment, the screen flickers. You see code beneath the world. A message: 'We made this for you.'",
effect: (state) => { state.meta.secretFlags.foundDevelopersNote = true; }
}
};2.3 Dynamic Dialogue Unlocks
Unlock Conditions:
const DIALOGUE_UNLOCKS = {
// Based on history
'dlg_returning_reference': {
unlock: (meta) => meta.playSessionCount > 1,
template: "You've been here {{sessionCount}} times now. What pattern are you trying to break?"
},
// Based on endings seen
'dlg_ending_wisdom': {
unlock: (meta) => meta.endingsDiscovered.length >= 2,
template: "You've seen how this ends. Yet you return. Hope? Or merely habit?"
},
// Based on choice patterns
'dlg_trust_issues': {
unlock: (meta) => {
const betrayals = meta.playerTendencies.betrayChoices;
const total = meta.playerTendencies.totalChoices;
return betrayals / total > 0.4;
},
template: "You betray easily. I wonder... will you betray yourself?"
},
// Based on playtime
'dlg_time_observation': {
unlock: (meta) => meta.totalPlaytime > 36000, // 10+ hours
template: "You've spent {{hours}} hours in this place. What are you searching for?"
},
// Based on specific combinations
'dlg_secret_knowledge': {
unlock: (meta) => {
return meta.endingsDiscovered.includes('ending_sacrifice') &&
meta.endingsDiscovered.includes('ending_betrayal') &&
meta.secretFlags.discoveredTrueName;
},
template: "You know the truth of sacrifice AND betrayal. You know MY name. What will you do with this knowledge?"
}
};3. GHOST MODE SPECIFICATION
3.1 Overview
Ghost Mode reveals aggregate player behavior without spoiling outcomes. Players see faint “ghosts” of others’ choices.
3.2 Data Aggregation
Collection Strategy:
const GHOST_DATA_SCHEMA = {
// Per-choice statistics
choiceStats: {
'choice_trust_elder': {
totalSelections: 15420,
firstPlaySelections: 8900,
returningPlaySelections: 6520,
avgWaterLevelAtChoice: 67.3,
commonNextChoice: 'choice_accept_ritual'
}
},
// Path statistics
pathStats: {
'path_sacrifice_route': {
completionRate: 0.23,
avgPlaytime: 4200,
commonEndings: ['ending_sacrifice', 'ending_cycle']
}
},
// Temporal data
timeStats: {
avgSessionLength: 3600,
commonQuitPoints: ['node_well_confrontation', 'node_betrayal_reveal'],
completionRate: 0.67
}
};Privacy Protection:
- All data anonymized (no player IDs)
- Minimum sample size: 100 players before showing stats
- No timestamps that could identify sessions
- Aggregate only - no individual choice trails
3.3 Visual Design
Ghost Appearance:
/* Ghost choice indicator */
.ghost-choice {
opacity: 0.3;
color: #8a8a8a;
font-style: italic;
position: relative;
}
.ghost-choice::before {
content: '👤';
opacity: 0.4;
margin-right: 8px;
}
/* Percentage display */
.ghost-percentage {
font-size: 0.8em;
color: #666;
margin-left: 12px;
}
/* Ghost trail effect */
@keyframes ghost-fade {
0% { opacity: 0.4; }
50% { opacity: 0.2; }
100% { opacity: 0.4; }
}
.ghost-trail {
animation: ghost-fade 3s infinite;
}UI Mockup:
[Choice A] Trust the Elder
👤 65% chose this
[Choice B] Trust the Stranger
👤 23% chose this
[Choice C] Trust No One
👤 12% chose this
3.4 Display Modes
const GHOST_MODES = {
OFF: {
id: 'off',
description: 'No ghost data shown',
display: () => null
},
SUBTLE: {
id: 'subtle',
description: 'Faint ghost indicators only',
display: (choiceId, stats) => {
if (stats.percentage > 50) {
return { indicator: '👤', opacity: 0.2 };
}
return null;
}
},
STANDARD: {
id: 'standard',
description: 'Percentage shown for all choices',
display: (choiceId, stats) => {
return {
indicator: '👤',
percentage: Math.round(stats.percentage),
opacity: 0.3
};
}
},
DETAILED: {
id: 'detailed',
description: 'Full statistics including trends',
display: (choiceId, stats) => {
return {
indicator: '👤',
percentage: Math.round(stats.percentage),
trend: stats.trend, // 'rising', 'falling', 'stable'
sampleSize: stats.totalSelections,
opacity: 0.4
};
}
}
};3.5 Toggle System
class GhostModeManager {
constructor() {
this.currentMode = this.loadPreference() || 'standard';
this.isEnabled = this.loadEnabledState() ?? true;
}
toggle() {
this.isEnabled = !this.isEnabled;
this.saveEnabledState();
this.emit('ghostModeChanged', this.isEnabled);
}
setMode(mode) {
if (GHOST_MODES[mode]) {
this.currentMode = mode;
this.savePreference();
this.emit('ghostModeChanged', mode);
}
}
cycleMode() {
const modes = Object.keys(GHOST_MODES);
const currentIndex = modes.indexOf(this.currentMode);
const nextIndex = (currentIndex + 1) % modes.length;
this.setMode(modes[nextIndex]);
}
getDisplayForChoice(choiceId) {
if (!this.isEnabled) return null;
const stats = this.fetchGhostStats(choiceId);
if (!stats || stats.sampleSize < 100) return null;
return GHOST_MODES[this.currentMode].display(choiceId, stats);
}
}3.6 Anti-Spoiler Protection
const SPOILER_PROTECTION = {
// Don't show ghost data for choices that lead directly to endings
isEndingChoice: (choiceId) => {
return ENDING_CHOICE_IDS.includes(choiceId);
},
// Delay ghost data for critical story moments
shouldDelayGhost: (nodeId, playtime) => {
const criticalNodes = ['reveal_true_villain', 'climax_confrontation'];
if (criticalNodes.includes(nodeId)) {
// Only show after player has made their choice
return true;
}
return false;
},
// Blur percentages near major reveals
blurThreshold: (waterLevel) => {
if (waterLevel < 10) {
return 0.5; // 50% opacity max near end
}
return 1.0;
}
};4. TIME PRESSURE MECHANICS
4.1 Well Drain System
Core Concept: The well drains in REAL TIME during play, creating genuine urgency.
Drain Mechanics:
class WellSystem {
constructor() {
this.waterLevel = 100;
this.baseDrainRate = 0.5; // units per second
this.drainMultiplier = 1.0;
this.isPaused = false;
this.lastUpdate = Date.now();
}
update() {
if (this.isPaused) return;
const now = Date.now();
const deltaTime = (now - this.lastUpdate) / 1000;
this.lastUpdate = now;
const drainAmount = this.baseDrainRate * this.drainMultiplier * deltaTime;
this.waterLevel = Math.max(0, this.waterLevel - drainAmount);
this.checkThresholds();
}
// Choice-based drain acceleration
applyChoiceDrain(choiceId) {
const drainMap = {
'choice_investigate_well': 5, // Takes time
'choice_quick_decision': 1, // Fast
'choice_ponder_deeply': 10, // Slow, contemplative
'choice_rush_action': 2, // Hurried
'choice_manipulate_ritual': 8, // Complex
'choice_flee_immediately': 3 // Urgent
};
const drain = drainMap[choiceId] || 3;
this.waterLevel -= drain;
this.recordDrainEvent(choiceId, drain);
}
// Villain-specific modifiers
applyVillainModifier(villainId) {
const modifiers = {
'villain_elder': 0.8, // Controlled, slower
'villain_stranger': 1.2, // Chaotic, faster
'villain_well': 1.5, // The well hungers
'villain_player': 1.0 // Neutral
};
this.drainMultiplier = modifiers[villainId] || 1.0;
}
}4.2 Visual Indicators
Water Level Display:
.well-indicator {
position: fixed;
bottom: 20px;
right: 20px;
width: 60px;
height: 200px;
background: #1a1a1a;
border: 2px solid #444;
border-radius: 4px;
overflow: hidden;
}
.well-water {
position: absolute;
bottom: 0;
width: 100%;
background: linear-gradient(
to top,
#0d3b2e 0%,
#1a5f4a 50%,
#2d8b6a 100%
);
transition: height 0.3s ease;
}
/* Critical state */
.well-water.critical {
background: linear-gradient(
to top,
#3b0d0d 0%,
#5f1a1a 50%,
#8b2d2d 100%
);
animation: pulse-critical 1s infinite;
}
@keyframes pulse-critical {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
/* Percentage text */
.well-percentage {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 14px;
font-weight: bold;
color: white;
text-shadow: 0 0 4px rgba(0,0,0,0.8);
}State-Based Visuals:
| Water Level | Visual State | Audio |
|---|---|---|
| 100-75% | Calm, gentle ripples | Ambient drip |
| 75-50% | Slight turbulence | Increased dripping |
| 50-25% | Churning, darkening | Low rumble |
| 25-15% | Violent, red-tinged | Urgent pulsing |
| 15-5% | Critical, almost empty | Heartbeat sync |
| 5-0% | Empty, void revealed | Silence, then… |
4.3 Urgency Escalation
Narrative Urgency Integration:
const URGENCY_NARRATIVE = {
// At 75% - subtle hints
'urgency_75': {
condition: (well) => well.waterLevel <= 75 && well.waterLevel > 74.5,
dialogue: "The water seems... lower than before. Or is that just your imagination?",
oneTime: true
},
// At 50% - clear concern
'urgency_50': {
condition: (well) => well.waterLevel <= 50 && well.waterLevel > 49.5,
dialogue: "The well is half-empty. Time is running short. You can feel it.",
oneTime: true
},
// At 25% - panic sets in
'urgency_25': {
condition: (well) => well.waterLevel <= 25 && well.waterLevel > 24.5,
dialogue: "THE WATER FALLS. Whatever is coming, it comes soon.",
oneTime: true,
effect: (state) => { state.npcs.all.trustScore -= 10; } // Panic affects relationships
},
// At 10% - desperate
'urgency_10': {
condition: (well) => well.waterLevel <= 10 && well.waterLevel > 9.5,
dialogue: "Almost gone. Almost time. What will you do with your final moments?",
oneTime: true
},
// At 0% - the end
'urgency_0': {
condition: (well) => well.waterLevel <= 0,
dialogue: "The well is dry. The waiting is over.",
effect: (state) => { this.triggerEndingSequence(state); }
}
};4.4 Choice Time Cost Display
const CHOICE_TIME_COSTS = {
'choice_investigate_well': {
display: '⏱️ ~30 seconds',
actualDrain: 5,
description: 'Take time to examine the well carefully'
},
'choice_quick_decision': {
display: '⏱️ ~5 seconds',
actualDrain: 1,
description: 'Act on instinct'
},
'choice_ponder_deeply': {
display: '⏱️ ~1 minute',
actualDrain: 10,
description: 'Consider all implications'
}
};
// UI Display
function renderChoice(choice) {
const timeCost = CHOICE_TIME_COSTS[choice.id];
return `
<div class="choice">
<span class="choice-text">${choice.text}</span>
${timeCost ? `<span class="time-cost">${timeCost.display}</span>` : ''}
</div>
`;
}4.5 Pause Behavior
// Tab inactive = pause drain
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
wellSystem.pause();
saveGame(); // Save on tab switch
} else {
wellSystem.resume();
}
});
// Menu open = pause drain (optional)
function openMenu() {
wellSystem.pause();
showMenu();
}
function closeMenu() {
hideMenu();
wellSystem.resume();
}5. NEW GAME+ FEATURES
5.1 Unlock Conditions
const NEW_GAME_PLUS = {
unlockCondition: (meta) => {
// Unlock after first completion
return meta.endingsDiscovered.length >= 1;
},
// Or unlock immediately for returning players
immediateUnlock: (meta) => meta.playSessionCount > 1
};5.2 Knowledge Carryover
What Carries Over:
const CARRYOVER_SYSTEM = {
// Meta-knowledge (always carried)
persistentKnowledge: [
'knowsRitualExists',
'knowsVillagersDistrustOutsiders',
'knowsWellIsSignificant'
],
// Previous playthrough knowledge (conditional)
conditionalKnowledge: {
'knowsElderSecret': {
condition: (meta) => meta.previousPlaythroughs.some(
p => p.keyChoices.includes('choice_learn_elder_secret')
),
unlockDialogue: 'dlg_recognize_elder_secret'
},
'knowsStrangerTrueName': {
condition: (meta) => meta.secretFlags.discoveredTrueName,
unlockDialogue: 'dlg_use_true_name'
},
'knowsRitualTruePurpose': {
condition: (meta) => meta.endingsDiscovered.includes('ending_sacrifice'),
unlockDialogue: 'dlg_understand_ritual'
}
},
// Failed strategy knowledge
attemptedStrategies: {
carryover: true,
usage: 'NPCs reference previous attempts',
example: "You tried to persuade the Elder last time. It didn't work then either."
}
};5.3 New Dialogue Options
const NEW_GAME_PLUS_DIALOGUE = {
// Returning player greetings
'dlg_returning_greeting': {
condition: (meta) => meta.playSessionCount > 1,
variants: [
"Back again? The well welcomes familiar faces.",
"{{sessionCount}} times now. What are you seeking?",
"The cycle continues. Do you remember what happened before?",
"You've walked this path. Will you walk it differently?"
]
},
// Knowledge-based options
'dlg_knowledge_option': {
condition: (state, meta) => {
return meta.previousPlaythroughs.some(p =>
p.keyChoices.includes('choice_learn_secret')
);
},
text: "[Use your knowledge] 'I know what you're hiding.'",
effect: 'force_truth_reveal'
},
// Meta-commentary
'dlg_meta_commentary': {
condition: (meta) => meta.playSessionCount > 5,
text: "You've seen how this ends. Multiple times. Yet you persist. Is it hope? Obsession? Or something else?"
}
};5.4 Hidden Path Unlocks
const HIDDEN_PATHS = {
'path_true_ending': {
unlockCondition: (meta) => {
return meta.endingsDiscovered.length >= 6 &&
meta.secretFlags.discoveredTrueName;
},
description: 'The path to understanding the true nature of the cycle',
entryPoint: 'node_well_heart',
requirements: ['knowsAllVillains', 'knowsTrueName', 'hasSeenEnough']
},
'path_break_cycle': {
unlockCondition: (meta) => {
return meta.playSessionCount >= 5 &&
meta.playerTendencies.totalChoices > 100;
},
description: 'Attempt to do what no one has done before',
entryPoint: 'node_final_choice_modified',
requirements: ['persistentEnough', 'learnedPatterns']
},
'path_developer_room': {
unlockCondition: (meta) => {
return meta.secretFlags.foundDevelopersNote &&
meta.playSessionCount >= 10;
},
description: 'A hidden message from the creators',
entryPoint: 'node_behind_the_code',
requirements: ['foundTheSign', 'earnedAccess']
}
};5.5 Retry Failed Strategies
class StrategyRetrySystem {
constructor(meta) {
this.attemptedStrategies = meta.newGamePlus?.attemptedStrategies || [];
this.failedStrategies = this.identifyFailures();
}
identifyFailures() {
return this.attemptedStrategies.filter(strategy => {
// A strategy "failed" if it didn't lead to desired outcome
return strategy.outcome !== strategy.intendedOutcome;
});
}
canRetry(strategyId) {
return this.failedStrategies.some(s => s.id === strategyId);
}
getRetryDialogue(strategyId) {
const strategy = this.failedStrategies.find(s => s.id === strategyId);
return `You tried to ${strategy.description} before. It didn't work then. But circumstances have changed...`;
}
getModifiedOutcome(strategyId, currentState) {
// Some strategies work on retry with different context
const baseOutcome = this.getBaseOutcome(strategyId);
const contextModifier = this.calculateContextModifier(currentState);
return this.applyModifier(baseOutcome, contextModifier);
}
}6. ACHIEVEMENT / DISCOVERY SYSTEM
6.1 Endings Catalog
const ENDINGS = {
// Standard endings
'ending_sacrifice': {
name: 'The Sacrifice',
description: 'You gave yourself to save others',
difficulty: 'medium',
hint: 'Trust can demand the ultimate price',
unlockCondition: (state) => state.npcs.player.sacrificedSelf
},
'ending_escape': {
name: 'The Escape',
description: 'You fled, leaving them to their fate',
difficulty: 'easy',
hint: 'Sometimes survival means running',
unlockCondition: (state) => state.npcs.player.fled
},
'ending_betrayal': {
name: 'The Betrayal',
description: 'You chose yourself over all others',
difficulty: 'medium',
hint: 'Trust is a weapon in the right hands',
unlockCondition: (state) => state.npcs.player.betrayedAll
},
'ending_ascension': {
name: 'The Ascension',
description: 'You became one with the well',
difficulty: 'hard',
hint: 'The well offers power to those who listen',
unlockCondition: (state) => state.npcs.player.acceptedWellPower
},
'ending_damnation': {
name: 'The Damnation',
description: 'Your sins caught up with you',
difficulty: 'medium',
hint: 'Betrayal has consequences',
unlockCondition: (state) => state.npcs.player.wasBetrayed && state.npcs.player.betrayedOthers
},
'ending_redemption': {
name: 'The Redemption',
description: 'You broke the cycle of mistrust',
difficulty: 'hard',
hint: 'Forgiveness is harder than vengeance',
unlockCondition: (state) => state.npcs.all.every(n => n.forgiven)
},
'ending_cycle': {
name: 'The Cycle',
description: 'Nothing changed. Nothing ever does.',
difficulty: 'easy',
hint: 'Some patterns repeat endlessly',
unlockCondition: (state) => state.npcs.player.maintainedStatusQuo
},
'ending_truth': {
name: 'The Truth',
description: 'You discovered what really happened',
difficulty: 'hard',
hint: 'Question everything, especially yourself',
unlockCondition: (state) => state.npcs.player.discoveredTrueVillain
},
// Secret endings
'ending_developer': {
name: 'Behind the Curtain',
description: 'You found the creators',
difficulty: 'secret',
hint: 'Look beyond the story',
unlockCondition: (meta) => meta.secretFlags.foundDevelopersNote,
hidden: true
},
'ending_true': {
name: 'The True Ending',
description: 'You understood everything',
difficulty: 'secret',
hint: 'See all, know all, then choose',
unlockCondition: (meta) => meta.endingsDiscovered.length >= 8,
hidden: true
}
};6.2 Secret Interactions
const SECRET_INTERACTIONS = {
'secret_well_whispers': {
name: 'Listener',
description: 'Heard the well speak',
condition: (meta) => meta.secretFlags.heardWellSpeak,
hint: 'Listen when the water runs low'
},
'secret_true_name': {
name: 'Namer',
description: 'Discovered the true name',
condition: (meta) => meta.secretFlags.discoveredTrueName,
hint: 'Names have power. Find the right one.'
},
'secret_all_villains': {
name: 'Know Thy Enemy',
description: 'Experienced all villain rotations',
condition: (meta) => {
const villainsSeen = new Set(
meta.previousPlaythroughs.map(p => p.villain)
);
return villainsSeen.size >= 6;
},
hint: 'Evil wears many faces'
},
'secret_speed_run': {
name: 'Rushed',
description: 'Completed in under 30 minutes',
condition: (meta, session) => session.sessionPlaytime < 1800,
hint: 'Time is always running'
},
'secret_completionist': {
name: 'Completionist',
description: 'Discovered all endings',
condition: (meta) => meta.endingsDiscovered.length >= 10,
hint: 'Every path leads somewhere'
},
'secret_patient': {
name: 'Patient Zero',
description: 'Played 20+ times',
condition: (meta) => meta.playSessionCount >= 20,
hint: 'Persistence is its own reward'
},
'secret_ghost_mode': {
name: 'Among the Ghosts',
description: 'Enabled ghost mode',
condition: (meta) => meta.secretFlags.unlockedGhostMode,
hint: 'You are not alone'
},
'secret_no_save': {
name: 'Pure',
description: 'Completed without closing the tab',
condition: (session) => session.noTabClose,
hint: 'Commit to your choices'
}
};6.3 Progress Display
const PROGRESS_DISPLAY = {
// Endings progress
renderEndingsProgress: (meta) => {
const discovered = meta.endingsDiscovered;
const total = Object.keys(ENDINGS).length;
const percentage = (discovered.length / total) * 100;
return {
discovered: discovered.length,
total: total,
percentage: Math.round(percentage),
discoveredList: discovered.map(e => ({
id: e,
name: ENDINGS[e].name,
description: ENDINGS[e].description
})),
lockedCount: total - discovered.length,
hints: discovered.length < total ? this.getHints(discovered) : []
};
},
// Secrets progress
renderSecretsProgress: (meta) => {
const secrets = Object.values(SECRET_INTERACTIONS);
const unlocked = secrets.filter(s => s.condition(meta));
return {
unlocked: unlocked.length,
total: secrets.length,
unlockedList: unlocked.map(s => ({
name: s.name,
description: s.description
})),
lockedHints: secrets
.filter(s => !unlocked.includes(s))
.map(s => s.hint)
};
}
};6.4 Speed Run Recognition
const SPEED_RUN_SYSTEM = {
// Categories
categories: {
'any_ending': {
name: 'Any Ending',
description: 'Reach any ending as fast as possible'
},
'true_ending': {
name: 'True Ending',
description: 'Reach the true ending as fast as possible'
},
'all_endings': {
name: 'All Endings',
description: 'See every ending (tracked across sessions)'
}
},
// Recognition thresholds
thresholds: {
'any_ending': {
gold: 900, // 15 minutes
silver: 1800, // 30 minutes
bronze: 3600 // 1 hour
},
'true_ending': {
gold: 3600, // 1 hour
silver: 7200, // 2 hours
bronze: 10800 // 3 hours
}
},
// Display
renderSpeedBadge: (time, category) => {
const thresholds = SPEED_RUN_SYSTEM.thresholds[category];
if (time <= thresholds.gold) {
return { medal: '🥇', label: 'Gold', class: 'speed-gold' };
} else if (time <= thresholds.silver) {
return { medal: '🥈', label: 'Silver', class: 'speed-silver' };
} else if (time <= thresholds.bronze) {
return { medal: '🥉', label: 'Bronze', class: 'speed-bronze' };
}
return null;
}
};6.5 Post-Credits Rewards
const POST_CREDITS = {
// Based on ending achieved
endingRewards: {
'ending_sacrifice': {
unlock: 'bonus_dialogue_sacrifice_wisdom',
content: 'The Elder speaks of those who gave everything...'
},
'ending_true': {
unlock: 'developer_commentary',
content: 'The creators share their intentions...'
}
},
// Based on play history
historyRewards: (meta) => {
const rewards = [];
if (meta.playSessionCount >= 5) {
rewards.push({
unlock: 'pattern_recognition_hint',
content: 'You\'ve seen enough to recognize patterns...'
});
}
if (meta.endingsDiscovered.length >= 4) {
rewards.push({
unlock: 'villain_prediction',
content: 'You can sense who the true villain might be...'
});
}
return rewards;
},
// Personalized message
personalizedMessage: (meta, session) => {
const patterns = analyzePlayPatterns(meta);
return {
title: `Player ${meta.deviceId.slice(-6)}`,
message: generatePersonalMessage(patterns),
stats: {
totalTime: formatTime(meta.totalPlaytime),
sessions: meta.playSessionCount,
endings: meta.endingsDiscovered.length,
tendency: meta.playerTendencies.preferredApproach
}
};
}
};7. META-PROGRESSION
7.1 What Carries Over
const CARRYOVER_RULES = {
// Always preserved
persistent: [
'endingsDiscovered',
'secretFlags',
'totalPlaytime',
'playSessionCount',
'firstPlayDate',
'playerTendencies',
'previousPlaythroughs'
],
// Preserved with conditions
conditional: {
'branchAccess': {
preserve: true,
resetEachSession: false
},
'npcMemory': {
preserve: true,
// Reset specific memories each session
reset: ['immediate_betrayals', 'session_specific_trust']
}
},
// Never preserved (fresh each play)
ephemeral: [
'currentNodeId',
'choiceHistory',
'narrativeFlags',
'well.waterLevel',
'npcs.trustScore'
]
};7.2 Player Tendency Analysis
class TendencyAnalyzer {
constructor(meta) {
this.tendencies = meta.playerTendencies;
this.history = meta.previousPlaythroughs;
}
analyze() {
return {
approach: this.determineApproach(),
riskTolerance: this.calculateRiskTolerance(),
trustPattern: this.analyzeTrustPattern(),
exploration: this.calculateExploration(),
completionism: this.detectCompletionism(),
speedPreference: this.detectSpeedPreference()
};
}
determineApproach() {
const { trustChoices, betrayChoices, neutralChoices } = this.tendencies;
const total = this.tendencies.totalChoices;
const trustRatio = trustChoices / total;
const betrayRatio = betrayChoices / total;
if (trustRatio > 0.6) return 'trusting';
if (betrayRatio > 0.4) return 'calculated';
if (betrayRatio > 0.6) return 'manipulative';
if (Math.abs(trustRatio - betrayRatio) < 0.1) return 'balanced';
return 'cautious';
}
calculateRiskTolerance() {
// Based on choices made at low water levels
const riskyChoices = this.history.flatMap(h =>
h.keyChoices.filter(c => CHOICE_RISK_LEVEL[c] === 'high')
);
return Math.min(riskyChoices.length / 10, 1.0);
}
analyzeTrustPattern() {
// Do they trust the same NPCs consistently?
const trustPatterns = this.history.map(h => h.trustedNPC);
const consistency = this.calculateConsistency(trustPatterns);
return {
consistent: consistency > 0.7,
preferredNPC: this.mostFrequent(trustPatterns),
consistencyScore: consistency
};
}
calculateExploration() {
const uniqueNodes = new Set(
this.history.flatMap(h => h.visitedNodes)
);
const totalPossibleNodes = 150; // Estimated total
return uniqueNodes.size / totalPossibleNodes;
}
detectCompletionism() {
const endings = this.tendencies.endingsDiscovered?.length || 0;
const secrets = Object.values(this.tendencies.secretFlags || {}).filter(Boolean).length;
return endings >= 5 || secrets >= 5;
}
}7.3 Game “Learning” System
class GameLearningSystem {
constructor(meta) {
this.meta = meta;
this.analyzer = new TendencyAnalyzer(meta);
}
// Adapt game based on player patterns
adaptGame() {
const tendencies = this.analyzer.analyze();
return {
// Adjust difficulty subtly
difficultyModifier: this.calculateDifficulty(tendencies),
// Personalize NPC behavior
npcPersonalities: this.personalizeNPCs(tendencies),
// Unlock relevant hints
hintSystem: this.configureHints(tendencies),
// Customize ghost mode
ghostDisplay: this.configureGhostMode(tendencies)
};
}
personalizeNPCs(tendencies) {
const personalization = {};
// If player is consistently trusting, add betrayal opportunities
if (tendencies.approach === 'trusting') {
personalization.trustOpportunities = 'increased';
personalization.betrayalConsequences = 'amplified';
}
// If player is manipulative, NPCs become more guarded
if (tendencies.approach === 'manipulative') {
personalization.initialTrust = 'reduced';
personalization.npcSuspicion = 'increased';
}
// If completionist, add more secrets
if (tendencies.completionism) {
personalization.secretDensity = 'high';
personalization.easterEggs = 'enabled';
}
return personalization;
}
configureHints(tendencies) {
// Don't spoil, but guide based on patterns
if (tendencies.exploration < 0.3) {
return { hintFrequency: 'increased', type: 'exploration' };
}
if (tendencies.riskTolerance < 0.3) {
return { hintFrequency: 'subtle', type: 'risk_assessment' };
}
return { hintFrequency: 'normal', type: 'general' };
}
}7.4 Personalized Post-Credits
const PERSONALIZED_ENDINGS = {
generate: (meta, session) => {
const analyzer = new TendencyAnalyzer(meta);
const tendencies = analyzer.analyze();
return {
// Opening
opening: generateOpening(tendencies, meta),
// Statistics
stats: generatePersonalStats(meta, session),
// Pattern recognition
patterns: generatePatternInsight(tendencies),
// Next play suggestions
suggestions: generateSuggestions(meta, tendencies),
// Special message if applicable
special: generateSpecialMessage(meta)
};
}
};
function generateOpening(tendencies, meta) {
const openings = {
trusting: "You trust easily. The world needs more like you... or perhaps fewer.",
calculated: "Every choice weighed, every outcome considered. But did you enjoy the journey?",
manipulative: "You played them all like pieces on a board. Did anyone win?",
balanced: "You walked the middle path. Sometimes wisdom, sometimes indecision.",
cautious: "You hesitated at every turn. The well doesn't wait for certainty."
};
return openings[tendencies.approach] || openings.balanced;
}
function generatePersonalStats(meta, session) {
return {
sessionTime: formatTime(session.sessionPlaytime),
totalTime: formatTime(meta.totalPlaytime),
thisEnding: session.endingId,
endingsSeen: `${meta.endingsDiscovered.length}/10`,
completion: `${Math.round((meta.endingsDiscovered.length / 10) * 100)}%`,
playStyle: meta.playerTendencies.preferredApproach
};
}
function generateSuggestions(meta, tendencies) {
const suggestions = [];
if (meta.endingsDiscovered.length < 3) {
suggestions.push("Try trusting someone you didn't before.");
}
if (!meta.secretFlags.discoveredTrueName) {
suggestions.push("Listen for names. They hold power.");
}
if (tendencies.approach === 'trusting') {
suggestions.push("Consider: not everyone deserves your trust.");
}
if (tendencies.approach === 'manipulative') {
suggestions.push("What would happen if you were truly honest, just once?");
}
if (meta.playSessionCount >= 5) {
suggestions.push("You've seen enough. Look for what's hidden in plain sight.");
}
return suggestions;
}
function generateSpecialMessage(meta) {
if (meta.playSessionCount >= 20) {
return "You've become part of this place. We made this for you. Thank you for staying.";
}
if (meta.endingsDiscovered.length >= 8) {
return "You understand now. But understanding is not the same as acceptance.";
}
return null;
}8. IMPLEMENTATION CHECKLIST
Core Systems
- Save state schema implementation
- Villain rotation algorithm
- Random event system
- Ghost mode data aggregation
- Well drain mechanics
- New Game+ unlock system
- Achievement tracking
- Tendency analysis
UI Components
- Well indicator display
- Ghost mode toggle
- Progress dashboard
- Post-credits screen
- Choice time cost display
Data Pipeline
- Anonymous telemetry collection
- Aggregate statistics computation
- Privacy-compliant storage
- Real-time ghost data sync
Testing
- Villain distribution balance
- Well drain pacing
- Ghost mode accuracy
- Save migration paths
- Cross-session memory
Document Version: 1.0 Last Updated: 2024 For: Moloch Interactive Experience