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