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 IDRoleMotivationUnique Ending
villain_elderVillage ElderPreserve tradition at any costending_sacrifice
villain_strangerMysterious StrangerEscape their own dark pastending_betrayal
villain_villagerTrusted VillagerProtect family secretending_damnation
villain_playerThe Player ThemselvesUnknowingly the sourceending_truth
villain_wellThe Well EntityFeed its endless hungerending_ascension
villain_noneNo true villainCycle of misunderstandingending_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:

CategoryFrequencyImpact
ambientEvery 2-3 minutesAtmosphere, minor trust shifts
contextualTriggered by choicesSignificant branch changes
critical1-2 per playthroughMajor revelation or choice
rare~10% chanceUnique 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 LevelVisual StateAudio
100-75%Calm, gentle ripplesAmbient drip
75-50%Slight turbulenceIncreased dripping
50-25%Churning, darkeningLow rumble
25-15%Violent, red-tingedUrgent pulsing
15-5%Critical, almost emptyHeartbeat sync
5-0%Empty, void revealedSilence, 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