const { useState, useEffect, useRef } = React; // ============ AUDIO SETUP ============ const createGuitarSynth = () => { const synth = new Tone.PolySynth(Tone.Synth, { oscillator: { type: 'triangle' }, envelope: { attack: 0.02, decay: 0.3, sustain: 0.2, release: 0.8 } }).toDestination(); synth.volume.value = -6; return synth; }; const createVoiceSynth = () => { const synth = new Tone.Synth({ oscillator: { type: 'square' }, envelope: { attack: 0.01, decay: 0.1, sustain: 0.3, release: 0.1 } }).toDestination(); synth.volume.value = -12; return synth; }; // ============ GAME DATA ============ // Skin tones with shading const skinTones = [ { base: '#8b5a2b', shadow: '#6b4423', highlight: '#a67c52' }, { base: '#c68642', shadow: '#a66a2a', highlight: '#daa060' }, { base: '#e0ac69', shadow: '#c89050', highlight: '#f0c890' }, { base: '#f1c27d', shadow: '#d4a665', highlight: '#ffe0a0' }, { base: '#ffdbac', shadow: '#e0b890', highlight: '#fff0d0' }, { base: '#d4a574', shadow: '#b08050', highlight: '#e8c090' }, { base: '#6b4423', shadow: '#4a2f18', highlight: '#8b5a2b' }, { base: '#3b2414', shadow: '#2a180e', highlight: '#5c3a20' } ]; const hairColors = [ { base: '#1a1a1a', highlight: '#333' }, { base: '#2c1810', highlight: '#4a2820' }, { base: '#8b4513', highlight: '#a65520' }, { base: '#d4a574', highlight: '#e8c090' }, { base: '#c0c0c0', highlight: '#e0e0e0' }, { base: '#ff6b35', highlight: '#ff8855' }, { base: '#4a0080', highlight: '#6a20a0' }, { base: '#006080', highlight: '#0090b0' } ]; const hairStyles = ['bald', 'short', 'spiky', 'long', 'mohawk', 'ponytail', 'buzzcut', 'curly', 'slick']; const facialHairTypes = ['none', 'stubble', 'beard', 'bigbeard', 'mustache']; const accessoryTypes = ['none', 'glasses', 'eyepatch', 'scar', 'bandana', 'mask', 'earring']; const expressionTypes = ['neutral', 'stern', 'worried', 'angry', 'tired']; // Backgrounds affect starting skills const backgrounds = [ { id: 'military', name: 'Military', skills: { security: 3, scavenging: 2, maintenance: 1 }, desc: 'Combat trained' }, { id: 'medical', name: 'Medical', skills: { medical: 4, farming: 1 }, desc: 'Healthcare background' }, { id: 'engineer', name: 'Engineer', skills: { maintenance: 4, engineering: 3 }, desc: 'Technical expertise' }, { id: 'farmer', name: 'Farmer', skills: { farming: 4, cooking: 2 }, desc: 'Agricultural knowledge' }, { id: 'chef', name: 'Chef', skills: { cooking: 4, farming: 1 }, desc: 'Culinary arts' }, { id: 'teacher', name: 'Teacher', skills: { community: 3, medical: 1 }, desc: 'Education background' }, { id: 'mechanic', name: 'Mechanic', skills: { maintenance: 3, engineering: 2 }, desc: 'Repair skills' }, { id: 'survivalist', name: 'Survivalist', skills: { scavenging: 4, security: 2 }, desc: 'Wasteland expert' }, { id: 'artist', name: 'Artist', skills: { community: 4 }, desc: 'Creative soul' }, { id: 'unknown', name: 'Unknown', skills: {}, desc: 'Won\'t say' } ]; // Jobs with slots and requirements const jobDefinitions = { scavenger: { name: 'Scavenger', icon: '🎒', maxSlots: 5, skill: 'scavenging', desc: 'Venture outside to find supplies', dangerLevel: 'high', foodConsumption: 1.5 }, security: { name: 'Security', icon: '🛡️', maxSlots: 5, skill: 'security', desc: 'Protect the bunker from threats', dangerLevel: 'medium', foodConsumption: 1.3 }, medic: { name: 'Medic', icon: '💊', maxSlots: 3, skill: 'medical', desc: 'Treat injuries and illness', dangerLevel: 'low', foodConsumption: 1.0 }, engineer: { name: 'Engineer', icon: '🔧', maxSlots: 4, skill: 'engineering', desc: 'Keep power and water running', dangerLevel: 'low', foodConsumption: 1.1 }, cook: { name: 'Cook', icon: '🍳', maxSlots: 3, skill: 'cooking', desc: 'Prepare meals and preserve food', dangerLevel: 'low', foodConsumption: 1.0 }, farmer: { name: 'Farmer', icon: '🌱', maxSlots: 4, skill: 'farming', desc: 'Grow food in the hydroponic bay', dangerLevel: 'low', foodConsumption: 1.2 }, maintenance: { name: 'Maintenance', icon: '🔨', maxSlots: 4, skill: 'maintenance', desc: 'Repair and maintain the bunker', dangerLevel: 'low', foodConsumption: 1.2 }, community: { name: 'Community Org', icon: '🎭', maxSlots: 2, skill: 'community', desc: 'Organize events and boost morale', dangerLevel: 'low', foodConsumption: 1.0 } }; // Scavenging regions const scavengingRegions = [ { id: 'perimeter', name: 'Bunker Perimeter', danger: 0.02, foodYield: [2, 8], waterYield: [1, 5], desc: 'Safe but nearly empty' }, { id: 'nearby', name: 'Nearby Ruins', danger: 0.08, foodYield: [5, 15], waterYield: [3, 10], desc: 'Close, mostly picked over' }, { id: 'suburbs', name: 'Outer Suburbs', danger: 0.15, foodYield: [10, 25], waterYield: [6, 15], desc: 'Abandoned homes and stores' }, { id: 'industrial', name: 'Industrial District', danger: 0.25, foodYield: [15, 30], waterYield: [10, 25], desc: 'Warehouses and factories' }, { id: 'downtown', name: 'Downtown Core', danger: 0.35, foodYield: [20, 45], waterYield: [12, 30], desc: 'Dense urban zone, hostile territory' }, { id: 'hospital', name: 'St. Mary\'s Hospital', danger: 0.45, foodYield: [25, 50], waterYield: [15, 35], desc: 'Medical supplies but infested' }, { id: 'military', name: 'Fort Keller', danger: 0.55, foodYield: [35, 70], waterYield: [20, 45], desc: 'Military base, extreme danger' } ]; // ============ DIFFICULTY (tunes daily pressure + expedition risk) ============ const BUNKER_DIFFICULTY = { easy: { label: 'Easier', blurb: 'Softer random threats, gentler drains, safer scavenger runs.', problemNewChance: 0.22, problemEscalationMult: 0.88, factionAttackBase: 0.05, factionAttackPerDay: 0.0035, factionAttackCap: 0.22, horrorEventBase: 0.07, horrorEventPerLevel: 0.013, horrorLevelGain: 0.7, dilemmaChance: 0.05, visitorOvernightDeath: 0.2, visitorPatienceDeath: 0.3, relationshipEvent: 0.18, cultConvert: 0.14, nightEvent: 0.14, dreamChance: 0.1, noteChance: 0.09, rivalDiscover: 0.11, rivalRaid: 0.11, moodIntervalEvent: 0.38, weatherChangeChance: 0.22, confessionChance: 0.06, voiceChance: 0.22, foodDecayMult: 0.85, waterDecayMult: 0.85, powerDrainMult: 0.88, attackCasualtyChance: 0.12, scavDeathDangerMult: 0.7, expeditionEventMult: 0.72, thingSnatchAdd: -0.006 }, normal: { label: 'Normal', blurb: 'Original balance.', problemNewChance: 0.35, problemEscalationMult: 1, factionAttackBase: 0.08, factionAttackPerDay: 0.005, factionAttackCap: 0.3, horrorEventBase: 0.1, horrorEventPerLevel: 0.02, horrorLevelGain: 1, dilemmaChance: 0.08, visitorOvernightDeath: 0.3, visitorPatienceDeath: 0.4, relationshipEvent: 0.25, cultConvert: 0.2, nightEvent: 0.2, dreamChance: 0.15, noteChance: 0.12, rivalDiscover: 0.15, rivalRaid: 0.15, moodIntervalEvent: 0.5, weatherChangeChance: 0.3, confessionChance: 0.08, voiceChance: 0.3, foodDecayMult: 1, waterDecayMult: 1, powerDrainMult: 1, attackCasualtyChance: 0.2, scavDeathDangerMult: 1, expeditionEventMult: 1, thingSnatchAdd: 0 }, hard: { label: 'Hard', blurb: 'More incidents, faster horror ramp, deadlier expeditions.', problemNewChance: 0.42, problemEscalationMult: 1.12, factionAttackBase: 0.1, factionAttackPerDay: 0.0065, factionAttackCap: 0.36, horrorEventBase: 0.12, horrorEventPerLevel: 0.026, horrorLevelGain: 1.2, dilemmaChance: 0.1, visitorOvernightDeath: 0.38, visitorPatienceDeath: 0.48, relationshipEvent: 0.3, cultConvert: 0.26, nightEvent: 0.26, dreamChance: 0.19, noteChance: 0.14, rivalDiscover: 0.2, rivalRaid: 0.22, moodIntervalEvent: 0.6, weatherChangeChance: 0.36, confessionChance: 0.1, voiceChance: 0.38, foodDecayMult: 1.12, waterDecayMult: 1.1, powerDrainMult: 1.12, attackCasualtyChance: 0.3, scavDeathDangerMult: 1.3, expeditionEventMult: 1.22, thingSnatchAdd: 0.012 } }; // Problem types - 30 total const problemTypes = [ // INFRASTRUCTURE (10) { id: 'pipe_leak', category: 'infrastructure', name: 'Pipe Leak', desc: 'Water pipes are leaking in Section B', effect: { water: -5 }, requiredJob: 'maintenance', urgency: 'medium' }, { id: 'power_fluctuation', category: 'infrastructure', name: 'Power Fluctuation', desc: 'Electrical systems unstable, lights flickering', effect: { power: -8 }, requiredJob: 'engineer', urgency: 'high' }, { id: 'air_filter', category: 'infrastructure', name: 'Air Filter Clogged', desc: 'Air quality declining, people coughing', effect: { morale: -3 }, requiredJob: 'maintenance', urgency: 'medium' }, { id: 'door_malfunction', category: 'infrastructure', name: 'Blast Door Malfunction', desc: 'Main entrance not sealing properly', effect: { morale: -5 }, requiredJob: 'engineer', urgency: 'critical' }, { id: 'sewage_backup', category: 'infrastructure', name: 'Sewage Backup', desc: 'Waste system overflowing in lower level', effect: { morale: -10 }, requiredJob: 'maintenance', urgency: 'high' }, { id: 'ventilation_noise', category: 'infrastructure', name: 'Ventilation Noise', desc: 'Loud rattling keeping everyone awake', effect: { morale: -4 }, requiredJob: 'maintenance', urgency: 'low' }, { id: 'lighting_failure', category: 'infrastructure', name: 'Lighting Failure', desc: 'Section C plunged into darkness', effect: { morale: -5, power: -3 }, requiredJob: 'engineer', urgency: 'medium' }, { id: 'structural_crack', category: 'infrastructure', name: 'Structural Crack', desc: 'Concerning crack appeared in wall', effect: { morale: -8 }, requiredJob: 'maintenance', urgency: 'high' }, { id: 'generator_strain', category: 'infrastructure', name: 'Generator Strain', desc: 'Main generator making grinding sounds', effect: { power: -12 }, requiredJob: 'engineer', urgency: 'critical' }, { id: 'rust_damage', category: 'infrastructure', name: 'Rust Damage', desc: 'Corrosion spreading on critical supports', effect: { morale: -3 }, requiredJob: 'maintenance', urgency: 'medium' }, // PEOPLE (10) { id: 'illness', category: 'people', name: 'Illness Outbreak', desc: 'Several people showing flu symptoms', effect: { morale: -8 }, requiredJob: 'medic', urgency: 'high' }, { id: 'conflict', category: 'people', name: 'Personal Conflict', desc: 'Two residents at each other\'s throats', effect: { morale: -6 }, requiredJob: 'community', urgency: 'medium' }, { id: 'depression', category: 'people', name: 'Depression Spreading', desc: 'Hopelessness taking hold of residents', effect: { morale: -10 }, requiredJob: 'community', urgency: 'high' }, { id: 'theft', category: 'people', name: 'Theft Reported', desc: 'Personal items going missing', effect: { food: -5, morale: -5 }, requiredJob: 'security', urgency: 'medium' }, { id: 'injury', category: 'people', name: 'Work Injury', desc: 'Someone hurt themselves badly', effect: { morale: -4 }, requiredJob: 'medic', urgency: 'high' }, { id: 'insomnia', category: 'people', name: 'Insomnia Epidemic', desc: 'Nobody can sleep properly', effect: { morale: -6 }, requiredJob: 'medic', urgency: 'medium' }, { id: 'paranoia', category: 'people', name: 'Paranoid Behavior', desc: 'Someone convinced we have a traitor', effect: { morale: -7 }, requiredJob: 'community', urgency: 'medium' }, { id: 'grief', category: 'people', name: 'Grief Crisis', desc: 'Someone struggling with loss', effect: { morale: -5 }, requiredJob: 'community', urgency: 'medium' }, { id: 'food_fight', category: 'people', name: 'Ration Dispute', desc: 'Accusations of unfair food distribution', effect: { morale: -8, food: -3 }, requiredJob: 'community', urgency: 'high' }, { id: 'infection', category: 'people', name: 'Wound Infection', desc: 'Old injury getting worse', effect: { morale: -4 }, requiredJob: 'medic', urgency: 'critical' }, // FUNCTION (10) { id: 'hydroponic_failure', category: 'function', name: 'Hydroponic Failure', desc: 'Plants wilting in grow room', effect: { food: -12 }, requiredJob: 'farmer', urgency: 'high' }, { id: 'water_contamination', category: 'function', name: 'Water Contamination', desc: 'Water supply has strange color', effect: { water: -15 }, requiredJob: 'engineer', urgency: 'critical' }, { id: 'food_spoilage', category: 'function', name: 'Food Spoilage', desc: 'Refrigeration unit failing', effect: { food: -18 }, requiredJob: 'maintenance', urgency: 'high' }, { id: 'radio_broken', category: 'function', name: 'Radio Broken', desc: 'Can\'t receive outside signals', effect: { morale: -4 }, requiredJob: 'engineer', urgency: 'low' }, { id: 'nutrient_imbalance', category: 'function', name: 'Nutrient Imbalance', desc: 'Hydroponic solution needs adjustment', effect: { food: -6 }, requiredJob: 'farmer', urgency: 'medium' }, { id: 'pest_infestation', category: 'function', name: 'Pest Infestation', desc: 'Rats found in food storage', effect: { food: -10, morale: -5 }, requiredJob: 'maintenance', urgency: 'high' }, { id: 'water_pump', category: 'function', name: 'Water Pump Failure', desc: 'Pump making death rattles', effect: { water: -10 }, requiredJob: 'engineer', urgency: 'critical' }, { id: 'cooking_equipment', category: 'function', name: 'Stove Malfunction', desc: 'Main cooking equipment broken', effect: { morale: -5 }, requiredJob: 'maintenance', urgency: 'medium' }, { id: 'seed_blight', category: 'function', name: 'Seed Blight', desc: 'Disease affecting seed stock', effect: { food: -8 }, requiredJob: 'farmer', urgency: 'high' }, { id: 'water_heater', category: 'function', name: 'No Hot Water', desc: 'Water heater completely dead', effect: { morale: -6 }, requiredJob: 'engineer', urgency: 'medium' } ]; // Newspaper templates const newsTemplates = { headlines: [ "DAY {day} IN THE BUNKER", "BUNKER DAILY - DAY {day}", "THE UNDERGROUND TIMES", "SECTOR 7 GAZETTE", "THE SHELTER CHRONICLE" ], gossip: [ "{name} was seen talking to themselves in the storage room.", "Rumor has it {name} has been hoarding supplies.", "{name} and {name2} were arguing about something yesterday.", "Someone heard {name} crying in their bunk last night.", "{name} seems unusually cheerful lately. Suspicious?", "People are saying {name} knows more than they're letting on.", "{name} has been spending a lot of time near the exit.", "Did {name} really used to be {background}? Some doubt it.", "{name} was caught sneaking extra rations.", "{name} claims to have seen something outside. Nobody believes them." ], goodNews: [ "Morale is up after a successful community event!", "The hydroponic garden is thriving.", "No incidents reported in security yesterday.", "A child was heard laughing today. A rare sound.", "Someone found an old music player. Spirits lifted.", "The water tastes cleaner than usual today.", "A scavenging team returned with extra supplies!", "Two residents resolved their differences peacefully." ], badNews: [ "Rations are running lower than expected.", "Strange noises heard from outside last night.", "The air feels thicker today. Filters need checking.", "Another sleepless night for many residents.", "Tensions are rising. Watch your words.", "The generator made a worrying sound this morning.", "Someone's been having nightmares. Screaming at night.", "Food supplies are showing signs of spoilage." ] }; // Personality types with traits const personalityTypes = [ { id: 'optimist', name: 'Optimist', icon: '😊', baseMood: 70, moodResistance: 0.7, desc: 'Sees the bright side', goodTrait: true, moraleBoost: 2 }, { id: 'pessimist', name: 'Pessimist', icon: '😟', baseMood: 40, moodResistance: 0.4, desc: 'Expects the worst', goodTrait: false, moraleDrain: 2 }, { id: 'aggressive', name: 'Aggressive', icon: '😠', baseMood: 50, moodResistance: 0.5, desc: 'Quick to anger', goodTrait: false, conflictChance: 0.2 }, { id: 'paranoid', name: 'Paranoid', icon: '👀', baseMood: 45, moodResistance: 0.3, desc: 'Trusts no one', goodTrait: false, detectsThreats: 0.25 }, { id: 'lazy', name: 'Lazy', icon: '😴', baseMood: 60, moodResistance: 0.8, desc: 'Avoids hard work', goodTrait: false, workPenalty: 0.3 }, { id: 'hardworking', name: 'Hardworking', icon: '💪', baseMood: 55, moodResistance: 0.5, desc: 'Always busy', goodTrait: true, workBonus: 0.3 }, { id: 'social', name: 'Social', icon: '🗣️', baseMood: 65, moodResistance: 0.6, desc: 'Loves company', goodTrait: true, needsPeople: true }, { id: 'loner', name: 'Loner', icon: '🤐', baseMood: 50, moodResistance: 0.6, desc: 'Prefers solitude', goodTrait: null, hatesCrowds: true }, { id: 'greedy', name: 'Greedy', icon: '🤑', baseMood: 50, moodResistance: 0.5, desc: 'Wants more than their share', goodTrait: false, hoards: true }, { id: 'generous', name: 'Generous', icon: '🎁', baseMood: 60, moodResistance: 0.6, desc: 'Shares freely', goodTrait: true, shares: true }, { id: 'coward', name: 'Coward', icon: '😰', baseMood: 45, moodResistance: 0.3, desc: 'Avoids danger', goodTrait: false, fleeChance: 0.15, survivalPenalty: 0.1 }, { id: 'brave', name: 'Brave', icon: '🦁', baseMood: 55, moodResistance: 0.7, desc: 'Faces danger head-on', goodTrait: true, survivalBonus: 0.15 }, { id: 'honest', name: 'Honest', icon: '😇', baseMood: 55, moodResistance: 0.6, desc: 'Always truthful', goodTrait: true, reliableAdvice: true }, { id: 'manipulative', name: 'Manipulative', icon: '🎭', baseMood: 50, moodResistance: 0.5, desc: 'Twists the truth', goodTrait: false, unreliableAdvice: true }, { id: 'calm', name: 'Calm', icon: '😌', baseMood: 60, moodResistance: 0.9, desc: 'Unshakeable', goodTrait: true, stabilizes: true }, { id: 'volatile', name: 'Volatile', icon: '🌋', baseMood: 50, moodResistance: 0.2, desc: 'Unpredictable', goodTrait: false, moodSwings: true }, { id: 'nurturing', name: 'Nurturing', icon: '💚', baseMood: 60, moodResistance: 0.6, desc: 'Cares for the weak', goodTrait: true, healBonus: 0.2 }, { id: 'stoic', name: 'Stoic', icon: '🗿', baseMood: 55, moodResistance: 0.95, desc: 'Emotionally unflappable', goodTrait: null }, { id: 'cheerful', name: 'Cheerful', icon: '🌟', baseMood: 70, moodResistance: 0.7, desc: 'Always smiling', goodTrait: true, moraleBoost: 3 }, { id: 'bitter', name: 'Bitter', icon: '😒', baseMood: 35, moodResistance: 0.3, desc: 'Holds grudges', goodTrait: false, moraleDrain: 3 } ]; // Camera feed locations const cameraFeeds = [ { id: 'entrance', name: 'Main Entrance', icon: '🚪', jobFilter: null, desc: 'Bunker entrance and visitor queue' }, { id: 'quarters', name: 'Living Quarters', icon: '🛏️', jobFilter: null, desc: 'Where residents rest and sleep' }, { id: 'hydroponics', name: 'Hydroponic Bay', icon: '🌱', jobFilter: 'farmer', desc: 'Food growing operations' }, { id: 'generator', name: 'Generator Room', icon: '⚡', jobFilter: 'engineer', desc: 'Power generation systems' }, { id: 'kitchen', name: 'Kitchen', icon: '🍳', jobFilter: 'cook', desc: 'Food preparation area' }, { id: 'medbay', name: 'Medical Bay', icon: '💊', jobFilter: 'medic', desc: 'Healthcare facilities' }, { id: 'security', name: 'Security Station', icon: '🛡️', jobFilter: 'security', desc: 'Monitoring and defense' }, { id: 'maintenance', name: 'Maintenance Hub', icon: '🔧', jobFilter: 'maintenance', desc: 'Repair and upkeep' }, { id: 'recreation', name: 'Recreation Room', icon: '🎭', jobFilter: 'community', desc: 'Morale and activities' }, { id: 'wasteland', name: 'Wasteland Feed', icon: '📡', jobFilter: 'scavenger', desc: 'Scavenger body cams' } ]; // Mood consequences based on mood level const getMoodConsequence = (mood, personality) => { if (mood < 20) { return { level: 'critical', effects: ['may_fight', 'may_refuse_work', 'may_sabotage', 'may_leave'], color: '#ff4444' }; } else if (mood < 40) { return { level: 'low', effects: ['reduced_efficiency', 'complaints', 'spreads_negativity'], color: '#cc8844' }; } else if (mood < 60) { return { level: 'neutral', effects: [], color: '#888888' }; } else if (mood < 80) { return { level: 'good', effects: ['bonus_efficiency', 'helpful'], color: '#88cc88' }; } else { return { level: 'excellent', effects: ['inspires_others', 'bonus_efficiency', 'prevents_conflicts'], color: '#44ff44' }; } }; // Mood event triggers based on quality of life const moodEvents = { critical: [ { type: 'fight', text: '{name} got into a violent altercation!', effect: { morale: -8 } }, { type: 'sabotage', text: '{name} damaged equipment in a rage!', effect: { power: -10 } }, { type: 'steal', text: '{name} was caught hoarding food!', effect: { food: -8 } }, { type: 'leave', text: '{name} is threatening to leave!', effect: { morale: -5 } } ], low: [ { type: 'complain', text: '{name} is loudly complaining about conditions.', effect: { morale: -3 } }, { type: 'slack', text: '{name} has been neglecting their duties.', effect: {} }, { type: 'argue', text: '{name} argued with another resident.', effect: { morale: -2 } } ], good: [ { type: 'help', text: '{name} helped someone with their work.', effect: { morale: 2 } }, { type: 'efficient', text: '{name} worked extra efficiently today.', effect: {} } ], excellent: [ { type: 'inspire', text: '{name} gave an inspiring speech!', effect: { morale: 5 } }, { type: 'share', text: '{name} shared their rations with others.', effect: { morale: 3 } }, { type: 'fix', text: '{name} fixed something in their spare time.', effect: { power: 3 } } ] }; // ============ DIALOGUE SYSTEM ============ // How people address you based on identity const getAddressStyle = (identity, loyalty) => { if (identity === 'tex') { if (loyalty > 70) return randomFrom(['Tex', 'Mr. Santos', 'Tex, buddy', 'Hey Tex']); if (loyalty > 40) return randomFrom(['Tex', 'Mr. Santos', 'Sir']); return randomFrom(['Santos', 'Tex', '...']); } else { if (loyalty > 70) return randomFrom(['Mr. Banks', 'Sir', 'Director Banks', 'Boss']); if (loyalty > 40) return randomFrom(['Banks', 'Sir', 'Mr. Banks']); return randomFrom(['...Sir', 'B-Banks', 'Director']); } }; // ============ HORROR FACTIONS & THREATS ============ const factions = [ { id: 'raiders', name: 'The Scavenger Horde', icon: '⚔️', desc: 'Brutal survivalists who take what they want', attackPower: 3, horrorLevel: 1, attackMessages: [ 'Raiders spotted approaching the bunker!', 'A war party has been seen in the area!', 'Scavengers are testing our perimeter!' ] }, { id: 'hollow', name: 'The Hollow', icon: '👁️', desc: 'A cult that believes emptying oneself brings enlightenment', attackPower: 2, horrorLevel: 3, attackMessages: [ 'Strange chanting echoes from the wasteland...', 'Figures in grey robes have been spotted watching the bunker.', 'Someone painted symbols on our door overnight. They\'re watching.' ], infiltratorTraits: ['Speaks of "the emptying"', 'Avoids eye contact', 'Whispers to themselves'] }, { id: 'changed', name: 'The Changed', icon: '🧬', desc: 'Radiation-twisted humans who worship mutation as evolution', attackPower: 4, horrorLevel: 4, attackMessages: [ 'Inhuman screams echo across the wasteland at night.', 'Something... wrong... was seen crawling near the vents.', 'The Changed are migrating. They\'re coming this way.' ], infiltratorTraits: ['Wears heavy clothing to hide skin', 'Twitches occasionally', 'Fascinated by radiation'] }, { id: 'remnant', name: 'Military Remnant', icon: '🎖️', desc: 'Former soldiers who believe they should control all survivors', attackPower: 5, horrorLevel: 1, attackMessages: [ 'Armed soldiers spotted on recon near the bunker.', 'Military vehicles heard in the distance.', 'A drone was seen surveilling our position.' ] }, { id: 'whisper', name: 'The Whisper', icon: '🌑', desc: 'No one knows what they want. Those who meet them don\'t come back right.', attackPower: 2, horrorLevel: 5, attackMessages: [ 'The radio picked up... something. It wasn\'t words.', 'Three residents reported the same nightmare last night.', 'Someone wrote "THEY ARE ALREADY INSIDE" on the wall. No one remembers doing it.' ], infiltratorTraits: ['Pauses mid-sentence as if listening', 'Knows things they shouldn\'t', 'Never blinks'] }, { id: 'feeders', name: 'The Feeders', icon: '🦷', desc: 'Cannibals who see other survivors as livestock', attackPower: 3, horrorLevel: 4, attackMessages: [ 'Human bones found near the bunker entrance.', 'A survivor was found drained of blood near our perimeter.', 'They\'re hungry. And they know we\'re here.' ], infiltratorTraits: ['Stares at people\'s necks', 'Asks about food supplies too eagerly', 'Smiles wrong'] } ]; // Specific threat/infiltrator types const threatTypes = [ { id: 'spy', name: 'Spy', desc: 'Gathering information for an outside group', action: 'intel', faction: 'any' }, { id: 'saboteur', name: 'Saboteur', desc: 'Plans to destroy critical systems', action: 'sabotage', faction: 'any' }, { id: 'cultist', name: 'Cultist', desc: 'Wants to convert or sacrifice residents', action: 'convert', faction: ['hollow', 'whisper', 'changed'] }, { id: 'assassin', name: 'Assassin', desc: 'Targeting bunker leadership', action: 'assassinate', faction: ['remnant', 'raiders'] }, { id: 'thief', name: 'Thief', desc: 'Will steal resources and flee', action: 'theft', faction: 'any' }, { id: 'recruiter', name: 'Recruiter', desc: 'Turning residents against leadership', action: 'recruit', faction: ['hollow', 'whisper'] }, { id: 'sleeper', name: 'Sleeper Agent', desc: 'Waiting for a signal to act', action: 'wait', faction: ['remnant', 'whisper'] }, { id: 'bomber', name: 'Bomber', desc: 'Carrying hidden explosives', action: 'explode', faction: ['raiders', 'remnant'] } ]; // Internal incidents that can occur const incidentTypes = [ { id: 'theft_food', name: 'Food Theft', severity: 2, evidence: 'Missing rations noted', effect: { food: -10 } }, { id: 'theft_water', name: 'Water Theft', severity: 2, evidence: 'Water reserves lower than expected', effect: { water: -10 } }, { id: 'sabotage_power', name: 'Power Sabotage', severity: 4, evidence: 'Wiring deliberately cut', effect: { power: -20 } }, { id: 'sabotage_water', name: 'Water Contamination', severity: 5, evidence: 'Foreign substance in water supply', effect: { water: -30, healthDamage: 10 } }, { id: 'assault', name: 'Assault', severity: 3, evidence: 'Victim identified attacker', effect: { morale: -10 } }, { id: 'conspiracy', name: 'Spreading Dissent', severity: 2, evidence: 'Overheard anti-leadership talk', effect: { morale: -5 } }, { id: 'cult_ritual', name: 'Cult Activity', severity: 4, evidence: 'Strange symbols found', effect: { morale: -15 } }, { id: 'recruitment', name: 'Faction Recruitment', severity: 3, evidence: 'Secret meetings detected', effect: { morale: -5 } }, { id: 'murder', name: 'Murder', severity: 5, evidence: 'Body discovered', effect: { morale: -25 } }, { id: 'signal', name: 'Unauthorized Signal', severity: 4, evidence: 'Radio transmission detected', effect: {} }, { id: 'poison', name: 'Poisoning Attempt', severity: 5, evidence: 'Toxin found in food', effect: { food: -20 } }, { id: 'hoarding', name: 'Resource Hoarding', severity: 2, evidence: 'Hidden cache discovered', effect: { food: -5, water: -5 } }, { id: 'vandalism', name: 'Disturbing Vandalism', severity: 2, evidence: 'Threatening messages written', effect: { morale: -5 } }, { id: 'missing', name: 'Person Missing', severity: 4, evidence: 'Last seen near suspect', effect: { morale: -10 } } ]; // Horror events that can randomly occur const horrorEvents = [ { id: 'radio_whispers', text: '📻 The radio crackles to life at 3 AM. Through the static, you hear your name.', effect: { morale: -5 }, requiresFaction: null }, { id: 'shared_nightmare', text: '😱 Multiple residents report the same nightmare: a door that shouldn\'t be opened.', effect: { morale: -8 }, requiresFaction: 'whisper' }, { id: 'scratching', text: '🔊 Scratching sounds come from inside the walls. Nothing is found.', effect: { morale: -3 }, requiresFaction: null }, { id: 'symbols', text: '✒️ Unknown symbols appear on the walls overnight. They seem to move when you\'re not looking.', effect: { morale: -10 }, requiresFaction: 'hollow' }, { id: 'visitor_knows', text: '👁️ A visitor you\'ve never met greets you by name and asks about your family. You never told anyone about them.', effect: { morale: -7 }, requiresFaction: 'whisper' }, { id: 'mirror', text: '🪞 Someone reports their reflection moved wrong. They refuse to elaborate.', effect: { morale: -5 }, requiresFaction: null }, { id: 'counting', text: '🔢 A resident is found counting the same pile of rations over and over, muttering "there\'s always one more."', effect: { morale: -6 }, requiresFaction: 'hollow' }, { id: 'handprints', text: '🖐️ Small, child-sized handprints appear on the outside of the bunker door. There are no children here.', effect: { morale: -8 }, requiresFaction: null }, { id: 'singing', text: '🎵 Someone heard singing from the empty storage room. The song was in no language anyone recognizes.', effect: { morale: -7 }, requiresFaction: 'changed' }, { id: 'photo', text: '📷 A photograph is found in the bunker. It shows everyone here. It was taken from inside.', effect: { morale: -12 }, requiresFaction: 'whisper' } ]; // Punishment options for caught troublemakers const punishmentOptions = [ { id: 'warning', name: 'Warning', icon: '⚠️', effect: { loyalty: -5 }, desc: 'Let them off with a stern warning' }, { id: 'rations', name: 'Reduced Rations', icon: '🍽️', effect: { loyalty: -15, mood: -20 }, desc: 'Cut their food and water allocation' }, { id: 'confinement', name: 'Confinement', icon: '🔒', effect: { loyalty: -20, mood: -30 }, desc: 'Lock them in isolation for a day', daysConfined: 1 }, { id: 'labor', name: 'Hard Labor', icon: '⛏️', effect: { loyalty: -10, mood: -15 }, desc: 'Assign them to the worst jobs' }, { id: 'exile', name: 'Exile', icon: '🚪', effect: {}, desc: 'Cast them out into the wasteland', removes: true }, { id: 'execute', name: 'Execute', icon: '💀', effect: { morale: -10 }, desc: 'A permanent solution', removes: true, kills: true }, { id: 'forgive', name: 'Forgive', icon: '🤝', effect: { loyalty: 10, mood: 15 }, desc: 'Show mercy and understanding' } ]; // Inner Circle Meeting topics const meetingTopics = [ { id: 'priorities', name: 'Resource Priorities', icon: '📊', question: { tex: "What should we focus on right now?", banks: "Report: current operational priorities." }, desc: 'Discuss what the bunker needs most' }, { id: 'threats', name: 'External Threats', icon: '⚔️', question: { tex: "What's the situation outside? Should we be worried?", banks: "Threat assessment. Report on hostile factions." }, desc: 'Discuss known factions and dangers' }, { id: 'morale', name: 'Bunker Morale', icon: '😊', question: { tex: "How are people holding up? What's the mood?", banks: "Personnel morale status. Any concerns?" }, desc: 'Discuss the mental state of residents' }, { id: 'suspicions', name: 'Suspicious Activity', icon: '👁️', question: { tex: "Anyone acting strange lately? Anything I should know?", banks: "Report any suspicious personnel or behavior." }, desc: 'Discuss potential threats within the bunker' }, { id: 'personnel', name: 'Personnel Assessment', icon: '👥', question: { tex: "Who's pulling their weight? Who's struggling?", banks: "Performance review of key personnel." }, desc: 'Discuss specific bunker members' }, { id: 'strategy', name: 'Long-term Strategy', icon: '🎯', question: { tex: "What's our plan here? How do we survive long-term?", banks: "Strategic outlook. What's our endgame?" }, desc: 'Discuss the future of the bunker' }, { id: 'scavenging', name: 'Scavenging Operations', icon: '🎒', question: { tex: "Where should we send our scavengers next?", banks: "Scavenging mission planning. Target recommendations." }, desc: 'Discuss where to search for supplies' }, { id: 'eachother', name: 'About Each Other', icon: '🤝', question: { tex: "Be honest - how do you all feel about each other?", banks: "Inter-personnel relations assessment." }, desc: 'Have advisors discuss their opinions of each other' } ]; // Generate meeting responses based on personality and context const generateMeetingResponse = (advisor, topicId, identity, resources, members, knownFactionsList, otherAdvisors) => { const personality = advisor.personality; const address = identity === 'tex' ? randomFrom(['Tex', 'boss', '']) : randomFrom(['sir', 'Director', '']); // Check if advisor is secretly a threat const isSecretThreat = advisor.isThreat && advisor.threatInfo; const faction = advisor.threatInfo?.faction; const responses = { priorities: () => { if (personality?.id === 'paranoid') return `${address}, security first. Always security. We don't know who to trust.`; if (personality?.id === 'generous' || personality?.id === 'nurturing') return `People are hungry, ${address}. Food should be priority one.`; if (personality?.id === 'hardworking') return `We need more workers. Put everyone to use.`; if (personality?.id === 'lazy') return `*yawns* I think we're doing fine. Don't overwork people.`; if (personality?.id === 'aggressive') return `We should be raiding others, not sitting here waiting to die.`; if (resources.food < 40) return `Food, ${address}. We're running low.`; if (resources.water < 40) return `Water's the priority. Without it, nothing else matters.`; if (resources.power < 40) return `Power systems need attention before everything fails.`; if (resources.morale < 40) return `Morale, ${address}. People are losing hope.`; return `Things are balanced for now. Stay the course.`; }, threats: () => { if (knownFactionsList.length === 0) { if (personality?.id === 'paranoid') return `Just because we haven't seen them doesn't mean they're not watching.`; return `We haven't encountered any organized groups yet. Let's hope it stays that way.`; } // If advisor is secretly aligned with a known faction, downplay their threat if (isSecretThreat && knownFactionsList.includes(faction?.id)) { return `${factions.find(f => f.id === faction.id)?.name}? I've heard they're... misunderstood. Maybe we could negotiate.`; } const knownFaction = factions.find(f => knownFactionsList.includes(f.id)); if (personality?.id === 'aggressive') return `${knownFaction?.name}? We should hit them before they hit us.`; if (personality?.id === 'coward') return `*nervously* ${knownFaction?.name}... they terrify me. We should hide.`; if (personality?.id === 'paranoid') return `They're planning something. I can feel it. We need more security.`; if (personality?.id === 'calm') return `${knownFaction?.name} is a concern, but panic won't help. Stay vigilant.`; return `${knownFaction?.name} is dangerous. We need to be ready for them.`; }, morale: () => { const avgMood = members.length > 0 ? members.reduce((sum, m) => sum + m.mood, 0) / members.length : 50; if (personality?.id === 'optimist') return `People are staying positive! We'll get through this together.`; if (personality?.id === 'pessimist') return `Everyone's pretending to be fine. They're not. This place is a powder keg.`; if (personality?.id === 'social') return `We need more community events. People need connection.`; if (personality?.id === 'loner') return `People talk too much. Less chatting, more working.`; if (avgMood < 40) return `${address}, morale is critical. People are on the edge.`; if (avgMood > 70) return `Spirits are high, all things considered. Keep it up.`; return `People are... surviving. Not thriving, but surviving.`; }, suspicions: () => { // Find members with low loyalty or who are threats const suspicious = members.filter(m => m.loyalty < 40 || m.isThreat); // Paranoid always suspects someone if (personality?.id === 'paranoid') { const target = randomFrom(members.filter(m => m.id !== advisor.id)); if (target) return `I've been watching ${target.name}. Something's off about them. Don't trust them.`; return `Everyone's hiding something. Trust no one.`; } // If advisor is a threat, deflect or point at innocent people if (isSecretThreat) { const innocent = members.find(m => !m.isThreat && m.id !== advisor.id); if (innocent && Math.random() < 0.4) { return `Actually... ${innocent.name} has been acting strange. Might want to watch them.`; } return `I haven't noticed anything unusual, ${address}. Everyone seems fine to me.`; } // Honest personality might actually detect threats if (personality?.id === 'honest' && suspicious.length > 0) { const target = randomFrom(suspicious); return `${address}, I have to be honest. ${target.name} doesn't sit right with me.`; } if (personality?.id === 'manipulative') return `*smiles* Oh, I hear things. But I'll keep that between us for now.`; if (suspicious.length > 0 && Math.random() < 0.3) { const target = randomFrom(suspicious); return `${target.name} has been... off lately. Could be nothing.`; } return `Nothing concrete, ${address}. I'll keep my eyes open.`; }, personnel: () => { const workers = members.filter(m => m.job); const slackers = members.filter(m => !m.job); const topWorker = workers.length > 0 ? workers.reduce((best, m) => { const skill = m.job ? m.skills[jobDefinitions[m.job]?.skill] || 0 : 0; const bestSkill = best?.job ? best.skills[jobDefinitions[best.job]?.skill] || 0 : 0; return skill > bestSkill ? m : best; }, workers[0]) : null; if (personality?.id === 'hardworking' && topWorker) { return `${topWorker.name} is doing excellent work. We need more people like them.`; } if (personality?.id === 'lazy' && slackers.length > 0) { return `Not everyone needs a job. Some people work better with... flexibility.`; } if (personality?.id === 'aggressive') { const weakest = members.filter(m => m.id !== advisor.id).sort((a, b) => a.health - b.health)[0]; if (weakest) return `${weakest.name} is weak. Dead weight. We might need to make hard choices.`; } if (personality?.id === 'nurturing') { const struggling = members.find(m => m.mood < 40 || m.health < 50); if (struggling) return `${struggling.name} needs support. They're struggling.`; } if (slackers.length > workers.length) { return `Too many people without jobs, ${address}. We need to put them to work.`; } return `The crew's doing alright. No major concerns.`; }, strategy: () => { if (personality?.id === 'optimist') return `We build, we grow, we survive. One day, we rebuild the world.`; if (personality?.id === 'pessimist') return `Survive another day. That's all we can hope for.`; if (personality?.id === 'aggressive') return `We take what we need. Weakness gets you killed out there.`; if (personality?.id === 'paranoid') return `Trust no one. Build walls. Prepare for the worst.`; if (personality?.id === 'social') return `Community is everything. Together, we can overcome anything.`; if (isSecretThreat) return `Maybe we should consider... alliances. With outside groups.`; return `Keep supplies up, keep people healthy, stay alert. Day by day.`; }, scavenging: () => { const safestRegion = scavengingRegions[0]; const richestRegion = scavengingRegions[scavengingRegions.length - 1]; if (personality?.id === 'coward') return `Stick to ${safestRegion.name}. The danger isn't worth it.`; if (personality?.id === 'brave') return `${richestRegion.name}. High risk, high reward. That's how you survive.`; if (personality?.id === 'greedy') return `We should be hitting the richer areas. ${richestRegion.name} has the good stuff.`; if (personality?.id === 'calm') return `Balance risk and reward. The suburbs are usually a safe bet.`; // If threat, might suggest dangerous areas to get scavengers killed if (isSecretThreat) return `The ${richestRegion.name} would be... interesting. Lots of resources there.`; if (resources.food < 40 || resources.water < 40) { return `We need supplies badly. Take some risks if we have to.`; } return `Scout the area first. Don't rush into danger.`; }, eachother: () => { const otherAdvisor = randomFrom(otherAdvisors.filter(a => a.id !== advisor.id)); if (!otherAdvisor) return `I work alone. Don't need anyone else.`; // Personality conflicts if (personality?.id === 'paranoid') { return `I don't fully trust ${otherAdvisor.name}. I don't fully trust anyone.`; } if (personality?.id === 'aggressive' && otherAdvisor.personality?.id === 'coward') { return `${otherAdvisor.name} is weak. We can't rely on them when things get hard.`; } if (personality?.id === 'social') { return `${otherAdvisor.name}? They're good people. I'm glad they're here.`; } if (personality?.id === 'manipulative') { return `${otherAdvisor.name} has their uses. We all do, don't we?`; } if (personality?.id === 'honest') { return `${otherAdvisor.name} means well. ${otherAdvisor.personality?.goodTrait === false ? 'Though they have their flaws.' : 'I trust them.'}`; } // If both are threats from same faction if (isSecretThreat && otherAdvisor.isThreat && advisor.threatInfo?.faction?.id === otherAdvisor.threatInfo?.faction?.id) { return `${otherAdvisor.name}... they understand things. We see eye to eye.`; } return `${otherAdvisor.name} is... fine. We work together when needed.`; } }; return responses[topicId]?.() || `I don't have strong feelings about that, ${address}.`; }; // ============ STORY ARC & WIN CONDITIONS ============ const storyMilestones = [ { day: 5, id: 'radio_signal', title: 'MYSTERIOUS SIGNAL', text: 'The bunker radio crackles to life. Through the static, a voice: "This is Outpost Echo. Is anyone receiving? We have supplies... and answers. Broadcasting coordinates now." The signal cuts out before you can respond.', choices: [ { id: 'investigate', text: 'Send scouts to investigate', effect: { storyPhase: 1 } }, { id: 'ignore', text: 'It could be a trap. Ignore it.', effect: { morale: -5 } } ] }, { day: 10, id: 'faction_contact', title: 'AN OFFER', text: 'A messenger arrives under white flag. They represent the Military Remnant. "Join us voluntarily, and you\'ll have protection, supplies, order. Refuse... and eventually you\'ll join anyway."', choices: [ { id: 'consider', text: 'We\'ll consider your offer.', effect: { factionRelations: { remnant: 20 } } }, { id: 'refuse', text: 'We bow to no one.', effect: { factionRelations: { remnant: -30 }, morale: 10 } }, { id: 'attack', text: 'Kill the messenger.', effect: { factionRelations: { remnant: -50 }, karma: -20 } } ] }, { day: 15, id: 'bunker_secret', title: 'THE HIDDEN ROOM', text: 'While repairing a wall, workers discover a sealed door. Behind it: a room filled with old documents, a working radio transmitter, and a skeleton clutching a note: "They\'re not human anymore. Don\'t let them in. DON\'T LET THEM IN."', choices: [ { id: 'radio', text: 'Try to repair the radio transmitter', effect: { radioRepaired: true, storyPhase: 2 } }, { id: 'burn', text: 'Burn the documents. Some things are better unknown.', effect: { karma: -10, sanity: 10 } }, { id: 'study', text: 'Study the documents carefully', effect: { storyPhase: 2, sanity: -15 } } ] }, { day: 20, id: 'the_changed_offer', title: 'EVOLUTION\'S CALL', text: 'A Changed creature approaches. Unlike the others, it speaks clearly: "We were like you once. The radiation doesn\'t destroy - it transforms. Evolves. Join us willingly, and the change will be... gentle. Fight us, and it will be forced upon you all."', choices: [ { id: 'join', text: 'Perhaps evolution is the answer...', effect: { ending: 'changed', factionRelations: { changed: 50 } } }, { id: 'refuse', text: 'We\'d rather die human.', effect: { factionRelations: { changed: -40 }, morale: 5 } }, { id: 'negotiate', text: 'What do you want from us?', effect: { storyPhase: 3 } } ] }, { day: 25, id: 'rescue_signal', title: 'HOPE ON THE HORIZON', text: 'The repaired radio picks up a military frequency: "...evacuation zone established at coordinates 7-7-Alpha. Helicopter extraction available for verified survivors. Respond on this frequency to schedule pickup. You have 10 days."', choices: [ { id: 'respond', text: 'Respond immediately!', effect: { rescueCountdown: 10, storyPhase: 4 } }, { id: 'skeptical', text: 'Could be a trap. Monitor the frequency.', effect: { storyPhase: 4 } }, { id: 'ignore', text: 'We\'ve built something here. We stay.', effect: { morale: 10 } } ], requires: { radioRepaired: true } }, { day: 30, id: 'final_stand', title: 'THE HORDE APPROACHES', text: 'Scouts report a massive force approaching - Raiders, Changed, Feeders, all united under a dark banner. They\'ll arrive in 3 days. This is the end... or a new beginning.', choices: [ { id: 'fight', text: 'We stand and fight!', effect: { pendingAttack: { faction: 'horde', days: 3, strength: 15 } } }, { id: 'evacuate', text: 'Abandon the bunker. Take what we can.', effect: { ending: 'exodus' } }, { id: 'negotiate', text: 'Send an envoy to negotiate', effect: { storyPhase: 5 } } ] } ]; // ============ MORAL DILEMMAS ============ const moralDilemmas = [ { id: 'sick_child', title: 'THE SICK CHILD', text: 'A child arrives at the bunker, clearly infected with something. They\'re contagious. Behind them, their parent begs: "Please. She\'s all I have left."', choices: [ { id: 'admit', text: 'Let them both in. We\'ll quarantine them.', effect: { food: -10, risk: 'disease', karma: 20, morale: 5 } }, { id: 'child_only', text: 'Only the child. The parent is too risky.', effect: { karma: -5, morale: -5 } }, { id: 'refuse', text: 'We can\'t risk everyone. Close the door.', effect: { karma: -15, morale: -10 } } ] }, { id: 'cannibalism', title: 'DESPERATE MEASURES', text: 'Food has run out. People are dying. One of your advisors approaches quietly: "The dead... they don\'t need to eat. The living do. No one has to know."', choices: [ { id: 'accept', text: 'Do what must be done. Quietly.', effect: { food: 30, karma: -50, sanity: -20 } }, { id: 'refuse', text: 'Never. We stay human.', effect: { karma: 10, morale: -10 } }, { id: 'vote', text: 'Put it to a vote. Let the bunker decide.', effect: { morale: -5 } } ], requires: { food: { max: 5 } } }, { id: 'mercy_killing', title: 'THE INFECTED', text: 'Marcus has been bitten by a Changed. The transformation has begun - he\'s in agony. "Please," he begs through mutating lips, "don\'t let me become one of them. End it. End it NOW."', choices: [ { id: 'kill', text: 'Grant him mercy. End his suffering.', effect: { karma: 5, morale: -8, memberDeath: true } }, { id: 'wait', text: 'Maybe there\'s a cure. Chain him up.', effect: { risk: 'transformation', karma: 0 } }, { id: 'exile', text: 'Cast him out. Let nature take its course.', effect: { karma: -10, morale: -5, memberExile: true } } ] }, { id: 'traitor_child', title: 'BLOOD TIES', text: 'Your security team has caught a spy - a Hollow cultist who\'s been feeding information to the enemy. But she\'s just a teenager. "I didn\'t have a choice," she cries. "They have my family."', choices: [ { id: 'execute', text: 'Treason is treason. Execute her.', effect: { karma: -20, morale: -15, factionRelations: { hollow: -20 } } }, { id: 'imprison', text: 'Lock her up. She might have useful information.', effect: { karma: 0 } }, { id: 'release', text: 'Let her go. She\'s just a kid.', effect: { karma: 15, risk: 'information_leak', morale: 5 } }, { id: 'recruit', text: 'Turn her. Make her a double agent.', effect: { karma: -5, factionRelations: { hollow: 10 } } } ] }, { id: 'sacrifice', title: 'THE REACTOR', text: 'The reactor is going critical. Someone needs to go into the radiation chamber to fix it manually. They won\'t survive. Three volunteers step forward: your best engineer, a mother of two, and an elderly man who says "I\'ve lived enough."', choices: [ { id: 'engineer', text: 'Send the engineer. They have the best chance of success.', effect: { memberDeath: 'engineer', power: 50, karma: -5 } }, { id: 'mother', text: 'Send the mother. The bunker needs engineers.', effect: { memberDeath: 'mother', power: 50, karma: -15, morale: -20 } }, { id: 'elder', text: 'Send the elder. He volunteered.', effect: { memberDeath: 'elder', power: 50, karma: 5 } }, { id: 'yourself', text: 'I\'ll go myself.', effect: { ending: 'sacrifice' } } ] }, { id: 'raiders_hostage', title: 'THE EXCHANGE', text: 'Raiders have captured one of your people. They offer a trade: 50 food and 30 water, or they kill the hostage. You can see them on the monitors - knife to their throat.', choices: [ { id: 'pay', text: 'Pay the ransom. Every life matters.', effect: { food: -50, water: -30, karma: 10 } }, { id: 'refuse', text: 'We don\'t negotiate with terrorists.', effect: { memberDeath: true, karma: -5, factionRelations: { raiders: -20 } } }, { id: 'attack', text: 'Launch a rescue mission.', effect: { risk: 'combat', karma: 5 } }, { id: 'trade', text: 'Offer them a different prisoner instead.', effect: { karma: -25 } } ] }, { id: 'medicine_choice', title: 'LIMITED SUPPLY', text: 'A plague has struck. You have enough medicine for three people, but five are sick: your best security guard, a pregnant woman, twins age 8, and your most trusted advisor.', choices: [ { id: 'useful', text: 'Save the guard and advisor. The bunker needs them.', effect: { karma: -15, memberDeath: 'others' } }, { id: 'innocent', text: 'Save the children and pregnant woman.', effect: { karma: 15, memberDeath: 'adults' } }, { id: 'lottery', text: 'Draw lots. Let fate decide.', effect: { karma: 0, memberDeath: 'random' } }, { id: 'ration', text: 'Split the medicine. Maybe some will survive.', effect: { karma: 5, risk: 'all_die' } } ] } ]; // ============ EXPEDITION EVENTS ============ const expeditionEvents = [ { id: 'survivors_found', title: 'SURVIVORS!', text: 'Your team finds a group of survivors hiding in a basement - 4 adults and 2 children. They beg to come back with you, but you only have supplies for the return trip.', choices: [ { id: 'all', text: 'Bring them all. We\'ll share.', effect: { newSurvivors: 6, food: -15, risk: 'starvation' } }, { id: 'some', text: 'Only the healthy adults. The children slow us down.', effect: { newSurvivors: 2, karma: -20 } }, { id: 'none', text: 'We can\'t. Leave them supplies and directions.', effect: { food: -5, karma: 5 } }, { id: 'location', text: 'Mark their location. We\'ll return for them.', effect: { urgentEvent: { type: 'rescue', days: 3 } } } ] }, { id: 'supply_cache', title: 'THE CACHE', text: 'A massive supply cache! But it\'s in a radiation zone. Your team can grab some safely, or risk going deeper for the real haul.', choices: [ { id: 'safe', text: 'Take what\'s safe. Don\'t risk anyone.', effect: { food: 20, water: 15 } }, { id: 'risk', text: 'Push deeper. We need those supplies.', effect: { food: 50, water: 40, risk: 'radiation' } }, { id: 'volunteer', text: 'Ask for a volunteer to go alone.', effect: { food: 50, water: 40, memberDeath: 'maybe' } } ] }, { id: 'ambush', title: 'AMBUSH!', text: 'Raiders spring from the ruins! Your team is surrounded. They offer terms: drop your supplies and weapons, and they\'ll let you live.', choices: [ { id: 'surrender', text: 'Drop everything. Lives matter more than supplies.', effect: { lootLost: true } }, { id: 'fight', text: 'Fight our way out!', effect: { risk: 'combat', factionRelations: { raiders: -20 } } }, { id: 'negotiate', text: 'Offer to trade information about other bunkers.', effect: { karma: -15, factionRelations: { raiders: 10 } } }, { id: 'distract', text: 'Distract them while some escape with supplies.', effect: { memberDeath: 'maybe', lootKept: true } } ] }, { id: 'hollow_ritual', title: 'THE CEREMONY', text: 'Your team stumbles upon a Hollow ritual. Hooded figures surround a sacrifice. You could sneak past... but that\'s a person on the altar.', choices: [ { id: 'sneak', text: 'Sneak past. It\'s not our problem.', effect: { karma: -10 } }, { id: 'attack', text: 'Attack! Save the victim!', effect: { risk: 'combat', karma: 15, factionRelations: { hollow: -30 }, newSurvivor: true } }, { id: 'watch', text: 'Watch and gather intelligence.', effect: { karma: -5, intel: 'hollow' } }, { id: 'join', text: 'Approach peacefully. Witness the emptying.', effect: { karma: -30, factionRelations: { hollow: 30 }, risk: 'conversion' } } ] }, { id: 'vehicle_found', title: 'WHEELS', text: 'An intact vehicle! But it needs fuel, and there\'s a gas station nearby crawling with Changed.', choices: [ { id: 'grab_go', text: 'Take what we can carry and leave.', effect: { food: 15 } }, { id: 'fuel_run', text: 'Risk the gas station for fuel.', effect: { risk: 'combat', vehicle: true } }, { id: 'trap', text: 'Set the vehicle as a trap for the Changed.', effect: { factionRelations: { changed: -10 }, risk: 'ambush' } } ] }, { id: 'military_cache', title: 'MILITARY DEPOT', text: 'A sealed military bunker. Inside: weapons, medical supplies, MREs. But there\'s also a still-active security system. And... are those motion sensors?', choices: [ { id: 'careful', text: 'Move slowly. Disable the system.', effect: { timeDelay: true, weapons: 2, medicine: 10, food: 30 } }, { id: 'rush', text: 'Grab and run!', effect: { risk: 'security', weapons: 1, food: 15 } }, { id: 'return', text: 'Mark location. Come back with an engineer.', effect: { discoveredLocation: 'depot' } } ] }, { id: 'whisper_vision', title: 'THE WHISPER SPEAKS', text: 'One of your scavengers stops mid-stride. Their eyes go white. When they speak, it\'s not their voice: "We see you, little bunker. We know your name. We are coming to make you... quiet."', choices: [ { id: 'shake', text: 'Snap them out of it!', effect: { sanity: -10 } }, { id: 'listen', text: 'What do you want?', effect: { sanity: -20, intel: 'whisper' } }, { id: 'kill', text: 'They\'re compromised. End them.', effect: { memberDeath: true, karma: -10, sanity: 5 } }, { id: 'flee', text: 'RUN. Back to the bunker. NOW.', effect: { lootLost: true, sanity: -5 } } ] } ]; // ============ RELATIONSHIP EVENTS ============ const relationshipEvents = [ { id: 'romance_bloom', type: 'romance', text: '{name1} and {name2} have been spending a lot of time together. The bunker is buzzing with gossip about a budding romance.', effect: { relationship: 30, moodBoost: 10 } }, { id: 'rivalry_starts', type: 'rivalry', text: '{name1} and {name2} got into a heated argument over resources. Tensions are high.', effect: { relationship: -40, moodDrain: 5 } }, { id: 'friendship_formed', type: 'friendship', text: '{name1} helped {name2} through a difficult time. A strong bond has formed.', effect: { relationship: 25, loyalty: 5 } }, { id: 'betrayal', type: 'betrayal', text: '{name1} discovered that {name2} has been stealing their rations. Trust is broken.', effect: { relationship: -60, loyalty: -15 } }, { id: 'jealousy', type: 'jealousy', text: '{name1} is jealous of {name2}\'s position in the inner circle. Resentment grows.', effect: { relationship: -25, plotRisk: true } }, { id: 'clique_formed', type: 'clique', text: 'A group has formed around {name1}: they eat together, work together, whisper together. Should you be concerned?', effect: { cliqueFormed: true } }, { id: 'love_triangle', type: 'drama', text: 'Both {name1} and {name2} have feelings for {name3}. This could get messy.', effect: { relationship: -20, dramaLevel: 3 } } ]; // ============ FACTION DIPLOMACY ============ const factionDiplomacyOptions = { raiders: { name: 'Scavenger Horde', trade: { offer: { food: 20 }, receive: { weapons: 1, info: 'location' } }, tribute: { cost: { food: 10, water: 5 }, protection: 5 }, alliance: { cost: { autonomy: 30 }, benefit: { defense: 20, trade: true } }, demands: ['food', 'weapons', 'people'] }, remnant: { name: 'Military Remnant', trade: { offer: { food: 30, water: 20 }, receive: { weapons: 2, medicine: 5 } }, tribute: { cost: { loyalty: 10, food: 15 }, protection: 10 }, alliance: { cost: { autonomy: 50, leader: 'them' }, benefit: { defense: 40, supplies: true } }, demands: ['submission', 'intel', 'recruits'] }, hollow: { name: 'The Hollow', trade: { offer: { people: 1 }, receive: { peace: 30, info: 'whisper' } }, tribute: { cost: { convert: 1 }, protection: 3 }, alliance: { cost: { faith: true }, benefit: { sanity: -30, immunity: 'hollow' } }, demands: ['converts', 'silence', 'emptiness'] }, changed: { name: 'The Changed', trade: null, // They don't trade tribute: null, alliance: { cost: { transform: true }, benefit: { power: 50, humanity: false } }, demands: ['flesh', 'acceptance', 'evolution'] }, feeders: { name: 'The Feeders', trade: { offer: { people: 2 }, receive: { food: 50, peace: 20 } }, tribute: { cost: { people: 1 }, protection: 8 }, alliance: null, // They only see you as food demands: ['meat', 'meat', 'meat'] } }; // ============ HALLUCINATIONS & DEEP HORROR ============ const hallucinationEvents = [ { id: 'doppelganger', text: 'You see {name} walking down the corridor. But wait... {name} is standing right next to you.', effect: { sanity: -8 }, visual: 'double' }, { id: 'dead_speak', text: '{deadName} is sitting in the cafeteria, eating breakfast. They wave at you. {deadName} died three days ago.', effect: { sanity: -12 }, requires: { recentDeath: true }, visual: 'ghost' }, { id: 'walls_bleed', text: 'The walls are bleeding. Dark ichor seeps from the concrete. No one else seems to notice.', effect: { sanity: -10 }, visual: 'blood' }, { id: 'wrong_face', text: 'Everyone\'s face is wrong today. Their features shift when you\'re not looking directly at them.', effect: { sanity: -15 }, visual: 'faces' }, { id: 'countdown', text: 'Someone has written numbers on every surface. They\'re counting down. You\'re the only one who can see them.', effect: { sanity: -7 }, visual: 'numbers' }, { id: 'the_visitor', text: 'There\'s someone standing in the corner of your room. They\'ve been there all night. They haven\'t moved. They haven\'t blinked.', effect: { sanity: -20 }, visual: 'figure' }, { id: 'mirror_wrong', text: 'Your reflection smiled at you. You didn\'t smile.', effect: { sanity: -10 }, visual: 'mirror' } ]; // ============ BUNKER UPGRADES ============ const bunkerUpgrades = [ { id: 'armory', name: 'Armory', icon: '🔫', cost: { food: 30, water: 20, days: 3 }, effect: { defense: 10, weapons: true }, desc: 'Store and maintain weapons. Improves defense significantly.' }, { id: 'chapel', name: 'Chapel', icon: '⛪', cost: { food: 20, water: 10, days: 2 }, effect: { morale: 10, sanity: 5 }, desc: 'A place for hope and reflection. Boosts morale and sanity.' }, { id: 'greenhouse', name: 'Greenhouse', icon: '🌿', cost: { food: 15, water: 30, days: 4 }, effect: { foodProduction: 5 }, desc: 'Expanded growing space. Increases daily food production.' }, { id: 'vault', name: 'Secure Vault', icon: '🔒', cost: { food: 25, water: 15, days: 3 }, effect: { storageBonus: 50, theftImmune: true }, desc: 'Protected storage. Increases capacity and prevents theft.' }, { id: 'lab', name: 'Research Lab', icon: '🔬', cost: { food: 40, water: 25, days: 5 }, effect: { research: true, medicineProduction: 2 }, desc: 'Study the Changed. Develop cures. Understand the Whisper.' }, { id: 'watchtower', name: 'Watchtower', icon: '🗼', cost: { food: 20, water: 10, days: 2 }, effect: { detection: 20, warningDays: 1 }, desc: 'Early warning system. Spot threats before they arrive.' }, { id: 'infirmary', name: 'Infirmary Upgrade', icon: '🏥', cost: { food: 30, water: 20, days: 3 }, effect: { healingRate: 2, diseaseResist: 30 }, desc: 'Better medical facilities. Faster healing, disease resistance.' } ]; // ============ URGENT EVENTS ============ const urgentEventTypes = [ { id: 'rescue_mission', title: 'RESCUE MISSION', desc: 'Survivors trapped at {location}. {days} days before they\'re overrun.', reward: { survivors: [2, 5], karma: 20 }, failure: { karma: -10, morale: -5 } }, { id: 'supply_window', title: 'SUPPLY WINDOW', desc: 'A supply convoy will pass through {location} in {days} days. One chance to intercept.', reward: { food: [30, 60], water: [20, 40] }, failure: { morale: -3 } }, { id: 'disease_outbreak', title: 'OUTBREAK', desc: 'Disease spreading. Find medicine within {days} days or people will die.', reward: { saved: true }, failure: { deaths: [1, 3], morale: -15 } }, { id: 'structural_failure', title: 'STRUCTURAL FAILURE', desc: 'Section B is collapsing. Repair within {days} days or lose it forever.', reward: { bunkerIntegrity: 10 }, failure: { bunkerIntegrity: -30, roomLost: true } }, { id: 'faction_ultimatum', title: 'ULTIMATUM', desc: '{faction} demands tribute within {days} days. Pay or face consequences.', reward: { peace: true }, failure: { attack: true, factionRelations: -30 } } ]; // ============ WEATHER SYSTEM ============ const weatherTypes = [ { id: 'clear', name: 'Clear Skies', icon: '☀️', duration: [2, 4], effects: {}, desc: 'A rare peaceful day.' }, { id: 'overcast', name: 'Overcast', icon: '☁️', duration: [1, 3], effects: { morale: -2 }, desc: 'Grey skies hang heavy.' }, { id: 'rain', name: 'Acid Rain', icon: '🌧️', duration: [1, 2], effects: { scavengingDanger: 0.1, waterBonus: 5 }, desc: 'Corrosive rain falls. Stay inside.' }, { id: 'storm', name: 'Radiation Storm', icon: '⚡', duration: [1, 1], effects: { scavengingDanger: 0.3, power: -10, radiation: true }, desc: 'The sky turns green. Radiation spikes.' }, { id: 'heatwave', name: 'Heat Wave', icon: '🔥', duration: [1, 3], effects: { waterDecay: 2, morale: -5 }, desc: 'Unbearable heat. Water evaporates faster.' }, { id: 'cold', name: 'Cold Snap', icon: '❄️', duration: [1, 2], effects: { powerDecay: 5, diseaseRisk: 0.1 }, desc: 'Freezing temperatures drain power.' }, { id: 'fog', name: 'Toxic Fog', icon: '🌫️', duration: [1, 2], effects: { visibility: 0, sanity: -5 }, desc: 'Something moves in the fog...' }, { id: 'ashfall', name: 'Ash Fall', icon: '🌋', duration: [1, 2], effects: { morale: -3, breathing: true }, desc: 'The sky rains ash from distant fires.' } ]; // ============ RADIO STATIONS ============ const radioStations = [ { freq: 87.5, name: 'STATIC', type: 'static', messages: ['...kssshhh...', '...crackle...', '...white noise...'] }, { freq: 91.5, name: 'REMNANT RADIO', type: 'faction', faction: 'remnant', messages: [ 'This is Military Remnant Command. All survivors report to Sector 7 for processing.', 'Order will be restored. Resistance is futile. Join us or perish.', 'Day {day} of the new order. Compliance is survival.', 'Attention unregistered bunkers: Submit to inspection or face consequences.', 'The weak serve. The strong lead. This is the natural order.' ] }, { freq: 95.3, name: 'THE HOLLOW FREQUENCY', type: 'faction', faction: 'hollow', messages: [ 'Empty yourself... become nothing... become everything...', 'The void welcomes you. Surrender your burdens.', 'We were like you once. Full of noise. Now we are... quiet.', '*distant chanting* ...one of us... one of us...', 'Your thoughts are loud. Let us help you silence them.' ] }, { freq: 99.9, name: 'SURVIVOR\'S HOPE', type: 'friendly', messages: [ 'If you can hear this, you are not alone. Stay strong.', 'Day {day} since the fall. We remember what we were.', 'To all survivors: There is still hope. There is still humanity.', 'Playing a song from before... *music plays briefly*', 'If you have food, share it. If you have shelter, offer it. We survive together.' ] }, { freq: 103.7, name: 'NUMBERS STATION', type: 'cryptic', messages: [ '7... 15... 23... 4... 19... 8...', 'The sleeper awakens on day {day}...', 'ALPHA TANGO NOVEMBER... coordinates follow...', '*child\'s voice* Are you listening? They\'re coming.', 'Repeat: The bunker at {location} has fallen. Do not investigate.' ] }, { freq: 107.1, name: 'THE WHISPER', type: 'horror', faction: 'whisper', messages: [ '*barely audible* ...we see you...', '*breathing sounds*', 'Your name is {playerName}. We know your name.', '*static that sounds like screaming*', '...inside... we are inside... look behind you...' ] }, { freq: 88.1, name: 'MUSIC BOX', type: 'music', messages: [ '♪ Playing: "Yesterday" - The Beatles ♪', '♪ Playing: "Mad World" - Gary Jules ♪', '♪ Playing: "Sound of Silence" - Simon & Garfunkel ♪', '♪ Playing: "Hurt" - Johnny Cash ♪', '♪ Playing: "The End" - The Doors ♪' ] }, { freq: 104.5, name: 'DISTRESS CALLS', type: 'distress', messages: [ 'Mayday, mayday! We\'re trapped at the old hospital! Please, anyone!', '*crying* They broke through... they\'re in the building... help us...', 'This is bunker 17... we\'re overrun... don\'t come here... save yourselves...', 'If anyone can hear this... my family... we need medicine... please...', '*gunshots* Hold the door! HOLD THE— *static*' ] } ]; // ============ CRAFTING RECIPES ============ const craftingRecipes = [ { id: 'medkit', name: 'Medical Kit', icon: '🩹', materials: { cloth: 3, chemicals: 2, herbs: 2 }, time: 1, effect: { healing: 30 } }, { id: 'weapon', name: 'Makeshift Weapon', icon: '🔪', materials: { scrap: 5, cloth: 1 }, time: 1, effect: { defense: 5 } }, { id: 'armor', name: 'Scrap Armor', icon: '🛡️', materials: { scrap: 8, cloth: 3 }, time: 2, effect: { protection: 10 } }, { id: 'filter', name: 'Air Filter', icon: '😷', materials: { cloth: 4, chemicals: 2 }, time: 1, effect: { diseaseResist: 20 } }, { id: 'radaway', name: 'Rad-Away', icon: '☢️', materials: { chemicals: 5, herbs: 3 }, time: 2, effect: { radiationCure: 50 } }, { id: 'stimulant', name: 'Stimulant', icon: '💉', materials: { chemicals: 3, herbs: 2 }, time: 1, effect: { energyBoost: true } }, { id: 'trap', name: 'Perimeter Trap', icon: '🪤', materials: { scrap: 4, electronics: 2 }, time: 1, effect: { defense: 3 } }, { id: 'radio_booster', name: 'Radio Booster', icon: '📡', materials: { electronics: 5, scrap: 3 }, time: 2, effect: { radioRange: true } }, { id: 'generator', name: 'Backup Generator', icon: '🔋', materials: { electronics: 8, scrap: 10 }, time: 3, effect: { powerBackup: 20 } }, { id: 'water_purifier', name: 'Water Purifier', icon: '💧', materials: { chemicals: 4, scrap: 3 }, time: 2, effect: { waterBonus: 5 } } ]; // ============ DISEASES ============ const diseases = [ { id: 'flu', name: 'Wasteland Flu', icon: '🤒', severity: 1, duration: [3, 5], symptoms: ['fever', 'weakness'], spread: 0.2, treatment: 'medkit' }, { id: 'radiation_sickness', name: 'Radiation Sickness', icon: '☢️', severity: 3, duration: [5, 10], symptoms: ['nausea', 'hair_loss', 'bleeding'], spread: 0, treatment: 'radaway' }, { id: 'plague', name: 'The Blight', icon: '🦠', severity: 4, duration: [7, 14], symptoms: ['boils', 'madness', 'aggression'], spread: 0.5, treatment: 'special' }, { id: 'parasites', name: 'Gut Parasites', icon: '🪱', severity: 2, duration: [4, 8], symptoms: ['hunger', 'weakness'], spread: 0.1, treatment: 'herbs' }, { id: 'madness', name: 'The Whisper Sickness', icon: '🌀', severity: 5, duration: [10, 999], symptoms: ['voices', 'paranoia', 'violence'], spread: 0.3, treatment: 'none' } ]; // ============ MUTATIONS ============ const mutationTypes = [ { id: 'thick_skin', name: 'Thick Skin', icon: '🦎', tier: 1, effect: { protection: 10 }, desc: 'Leathery skin provides natural armor.' }, { id: 'night_vision', name: 'Night Eyes', icon: '👁️', tier: 1, effect: { nightBonus: true }, desc: 'Eyes adapt to see in darkness.' }, { id: 'rapid_healing', name: 'Rapid Healing', icon: '💚', tier: 2, effect: { healingRate: 2 }, desc: 'Wounds close unnaturally fast.' }, { id: 'toxic_blood', name: 'Toxic Blood', icon: '🩸', tier: 2, effect: { poisonImmune: true, touch_damage: 5 }, desc: 'Blood becomes caustic.' }, { id: 'extra_limb', name: 'Extra Limb', icon: '🦑', tier: 3, effect: { workSpeed: 1.5 }, desc: 'A third arm grows from the back.' }, { id: 'hive_mind', name: 'Hive Connection', icon: '🧠', tier: 3, effect: { changedAlliance: true }, desc: 'Hear the thoughts of the Changed.' }, { id: 'luminescence', name: 'Bioluminescence', icon: '✨', tier: 1, effect: { glow: true, stealth: -20 }, desc: 'Skin emits soft light.' }, { id: 'claws', name: 'Bone Claws', icon: '🦴', tier: 2, effect: { damage: 15 }, desc: 'Fingers elongate into weapons.' } ]; // ============ PETS ============ const petTypes = [ { id: 'dog', name: 'Dog', icon: '🐕', effect: { security: 5, morale: 5, detection: true }, food: 2, desc: 'Loyal guardian and early warning system.' }, { id: 'cat', name: 'Cat', icon: '🐈', effect: { morale: 8, pestControl: true }, food: 1, desc: 'Keeps pests away and spirits high.' }, { id: 'bird', name: 'Bird', icon: '🐦', effect: { morale: 3, earlyWarning: true }, food: 0.5, desc: 'Senses danger before humans do.' }, { id: 'rat', name: 'Trained Rat', icon: '🐀', effect: { scavenging: 3 }, food: 0.5, desc: 'Finds small treasures in tight spaces.' }, { id: 'mutant_dog', name: 'Mutant Hound', icon: '🐺', effect: { security: 15, fear: 10 }, food: 4, desc: 'Terrifying but loyal. Two heads.' } ]; // ============ RELIGION TYPES ============ const religionTypes = [ { id: 'hope', name: 'Church of New Dawn', icon: '☀️', tenets: ['kindness', 'sharing', 'rebuilding'], bonus: { morale: 10, karma: 'good' } }, { id: 'strength', name: 'Cult of the Strong', icon: '💪', tenets: ['power', 'survival', 'dominance'], bonus: { defense: 10, karma: 'neutral' } }, { id: 'science', name: 'The Rationalists', icon: '🔬', tenets: ['knowledge', 'progress', 'logic'], bonus: { research: true, karma: 'neutral' } }, { id: 'nature', name: 'Children of the Atom', icon: '☢️', tenets: ['mutation', 'evolution', 'acceptance'], bonus: { radiationResist: true, karma: 'dark' } }, { id: 'void', name: 'The Empty Ones', icon: '🕳️', tenets: ['silence', 'surrender', 'nothingness'], bonus: { sanityDrain: true, hollowAlliance: true, karma: 'evil' } } ]; // ============ LORE DOCUMENTS ============ const loreDocuments = [ { id: 'journal_1', title: 'Dr. Chen\'s Journal - Day 1', type: 'journal', content: 'The first cases appeared yesterday. Patients complaining of voices. We thought it was mass hysteria. We were wrong.' }, { id: 'journal_2', title: 'Dr. Chen\'s Journal - Day 15', type: 'journal', content: 'Patient Zero has... changed. His skin moves on its own. He asked me to "join the quiet." I declined. He smiled.' }, { id: 'military_1', title: 'CLASSIFIED: Operation Silence', type: 'military', content: 'EYES ONLY. The Whisper originated from Site 17. Containment failed on Day 0. Protocol Omega authorized.' }, { id: 'letter_1', title: 'Unsent Letter', type: 'letter', content: 'My dearest Sarah, if you find this, know that I tried to reach you. The roads are gone. The sky is wrong. I love y—' }, { id: 'news_1', title: 'Last Newspaper', type: 'news', content: 'WORLD IN CHAOS. Communications down globally. Military declares martial law. Citizens advised to shelter in pl—' }, { id: 'audio_1', title: 'Black Box Recording', type: 'audio', content: '[TRANSCRIPT] Pilot: "Something in the clouds... it\'s looking at us... dear god it\'s so big—" [END RECORDING]' }, { id: 'child_1', title: 'Child\'s Drawing', type: 'drawing', content: 'A crayon drawing of a family. Above them, a massive eye in the sky. Written in shaky letters: "IT SEES US"' }, { id: 'scientist_1', title: 'Research Notes', type: 'research', content: 'The Changed aren\'t hostile by nature. They\'re trying to communicate. The mutations are a language we don\'t understand.' }, { id: 'hollow_1', title: 'Hollow Scripture', type: 'religious', content: 'And the Emptiness spoke: "Fill yourself with nothing, and you shall want for nothing. Become hollow, become holy."' }, { id: 'deep_1', title: 'Tunnels Map', type: 'map', content: 'Hand-drawn map of tunnels beneath the bunker. Notes read: "Don\'t go past the red door. DON\'T GO PAST THE RED DOOR."' } ]; // ============ DREAMS ============ const dreamSequences = [ { id: 'family', title: 'The Family', type: 'memory', text: 'You dream of a dinner table. Everyone is laughing. You can\'t see their faces. When you look down, your plate is full of ash.', effect: { sanity: -5, morale: -5 } }, { id: 'falling', title: 'The Fall', type: 'nightmare', text: 'You\'re falling through darkness. Something falls beside you—a person. They turn to look at you. It\'s you.', effect: { sanity: -10 } }, { id: 'door', title: 'The Red Door', type: 'prophetic', text: 'A red door in a dark tunnel. Something knocks from the other side. Three times. You wake before it opens.', effect: { theDeepClue: true } }, { id: 'garden', title: 'The Garden', type: 'peaceful', text: 'A garden, green and alive. Someone tends to flowers. They wave at you. For a moment, everything is okay.', effect: { sanity: 10, morale: 10 } }, { id: 'mirror', title: 'The Reflection', type: 'horror', text: 'You stand before a mirror. Your reflection doesn\'t move. Then it smiles. Then it reaches through.', effect: { sanity: -15, mirrorEvent: true } }, { id: 'voices', title: 'The Choir', type: 'whisper', text: 'A thousand voices singing in perfect harmony. You don\'t know the words, but you want to. You almost join them.', effect: { sanity: -10, whisperExposure: true } } ]; // ============ ACHIEVEMENTS ============ const achievementList = [ { id: 'first_survivor', name: 'First Contact', icon: '👋', desc: 'Admit your first survivor', secret: false }, { id: 'full_bunker', name: 'Full House', icon: '🏠', desc: 'Have 20+ people in the bunker', secret: false }, { id: 'first_death', name: 'First Loss', icon: '💀', desc: 'Experience your first death', secret: false }, { id: 'week_survived', name: 'One Week', icon: '📅', desc: 'Survive 7 days', secret: false }, { id: 'month_survived', name: 'One Month', icon: '📆', desc: 'Survive 30 days', secret: false }, { id: 'caught_spy', name: 'Traitor!', icon: '🕵️', desc: 'Catch an infiltrator', secret: false }, { id: 'mercy', name: 'Merciful', icon: '🤝', desc: 'Forgive someone who wronged you', secret: false }, { id: 'ruthless', name: 'Ruthless', icon: '⚔️', desc: 'Execute 5 people', secret: false }, { id: 'romance', name: 'Love Blooms', icon: '💕', desc: 'A romance forms in the bunker', secret: false }, { id: 'first_birth', name: 'New Life', icon: '👶', desc: 'A child is born in the bunker', secret: false }, { id: 'the_deep', name: 'Into the Deep', icon: '🕳️', desc: 'Explore the tunnels below', secret: true }, { id: 'radio_master', name: 'Radio Operator', icon: '📻', desc: 'Discover all radio stations', secret: false }, { id: 'thing_found', name: 'It Was Among Us', icon: '👽', desc: 'Discover the Thing', secret: true }, { id: 'rescue', name: 'Salvation', icon: '🚁', desc: 'Get rescued', secret: false }, { id: 'madness', name: 'Descent', icon: '🌀', desc: 'Fall to madness', secret: true } ]; // ============ THE DEEP ============ const deepLevels = [ { level: 1, name: 'Maintenance Tunnels', danger: 0.1, loot: ['scrap', 'electronics'], events: ['rats', 'echo'] }, { level: 2, name: 'Old Bunker Section', danger: 0.2, loot: ['supplies', 'lore'], events: ['ghost', 'collapse'] }, { level: 3, name: 'The Cistern', danger: 0.3, loot: ['water', 'chemicals'], events: ['thing_in_water', 'whispers'] }, { level: 4, name: 'The Red Door', danger: 0.5, loot: ['artifact', 'knowledge'], events: ['guardian', 'vision'] }, { level: 5, name: 'The Heart', danger: 0.8, loot: ['truth', 'ending'], events: ['final'] } ]; // ============ TRADER GOODS ============ const traderGoods = [ { id: 'food_crate', name: 'Food Crate', price: { scrap: 15 }, gives: { food: 30 } }, { id: 'water_barrel', name: 'Water Barrel', price: { scrap: 12 }, gives: { water: 25 } }, { id: 'medicine', name: 'Medical Supplies', price: { scrap: 20, electronics: 2 }, gives: { medkit: 3 } }, { id: 'ammo', name: 'Ammunition', price: { scrap: 25 }, gives: { defense: 10 } }, { id: 'pet_dog', name: 'Guard Dog', price: { food: 30, scrap: 10 }, gives: { pet: 'dog' } }, { id: 'info', name: 'Map Information', price: { scrap: 15 }, gives: { mapReveal: true } }, { id: 'radio_parts', name: 'Radio Components', price: { electronics: 5 }, gives: { electronics: 3, radioBoost: true } }, { id: 'survivor', name: 'Rescued Survivor', price: { food: 20, water: 10 }, gives: { survivor: true } } ]; // ============ MAP LOCATIONS ============ const mapLocations = [ { id: 'hospital', name: 'Abandoned Hospital', x: 30, y: 20, type: 'medical', danger: 0.4, loot: ['medicine', 'chemicals'], discovered: false, desc: 'St. Helena Memorial. The east wing collapsed on day one. Screams were heard from inside for weeks after.', rooms: [ { id: 'lobby', name: 'Reception Lobby', searched: false, loot: { food: 3, water: 2 }, danger: 0.1, flavor: 'Overturned wheelchairs. A radio still crackling static.' }, { id: 'pharmacy', name: 'Pharmacy', searched: false, loot: { medicine: 8, chemicals: 3 }, danger: 0.3, flavor: 'Most shelves are bare, but the locked cage in back...' }, { id: 'surgery', name: 'Surgical Wing', searched: false, loot: { medicine: 5, scrap: 4 }, danger: 0.5, flavor: 'Operating lights still on. How is there power?' }, { id: 'morgue', name: 'The Morgue', searched: false, loot: { medicine: 2, lore: 1 }, danger: 0.7, flavor: 'The drawers are open. All of them. All empty.' }, { id: 'roof', name: 'Rooftop Helipad', searched: false, loot: { electronics: 3, weapons: 1 }, danger: 0.4, flavor: 'A military helicopter, half-melted. The pilot is still strapped in.' } ]}, { id: 'mall', name: 'Collapsed Mall', x: 60, y: 35, type: 'supplies', danger: 0.3, loot: ['food', 'cloth', 'scrap'], discovered: false, desc: 'Greenfield Plaza. Three stories, mostly pancaked. Survivors barricaded the food court early on.', rooms: [ { id: 'food_court', name: 'Food Court', searched: false, loot: { food: 12, water: 5 }, danger: 0.2, flavor: 'Canned goods everywhere. Someone was stockpiling.' }, { id: 'department', name: 'Department Store', searched: false, loot: { cloth: 6, scrap: 3 }, danger: 0.15, flavor: 'Mannequins arranged in a circle, facing inward.' }, { id: 'hardware', name: 'Hardware Store', searched: false, loot: { scrap: 8, weapons: 1 }, danger: 0.25, flavor: 'Tools, lumber, nails. Someone built something big in here and took it.' }, { id: 'parking', name: 'Underground Parking', searched: false, loot: { fuel: 4, electronics: 2 }, danger: 0.5, flavor: 'It goes down further than any parking garage should.' }, { id: 'cinema', name: 'Cinema', searched: false, loot: { food: 4 }, danger: 0.1, flavor: 'The projector is running. The film shows a family. Your family.' } ]}, { id: 'military_base', name: 'Military Outpost', x: 80, y: 50, type: 'military', danger: 0.6, loot: ['weapons', 'electronics', 'lore'], discovered: false, desc: "Forward Operating Base Keller. Went dark three days before the bunker sealed. Last transmission: \"They're inside the wire.\"", rooms: [ { id: 'armory', name: 'Armory', searched: false, loot: { weapons: 5, scrap: 2 }, danger: 0.6, flavor: 'Rifle racks half-empty. Spent casings everywhere. They fought hard.' }, { id: 'comms', name: 'Communications Center', searched: false, loot: { electronics: 6, lore: 2 }, danger: 0.4, flavor: 'Banks of radios. One is receiving. The voice says your name.' }, { id: 'barracks', name: 'Barracks', searched: false, loot: { food: 8, cloth: 4, medicine: 2 }, danger: 0.3, flavor: 'Personal effects scattered. Photos of families that no longer exist.' }, { id: 'motor_pool', name: 'Motor Pool', searched: false, loot: { fuel: 6, scrap: 5 }, danger: 0.5, flavor: "One APC still runs. The keys are in a dead man's hand." }, { id: 'bunker_cmd', name: 'Command Bunker', searched: false, loot: { lore: 4, electronics: 3, weapons: 2 }, danger: 0.8, flavor: "Classified folders. Project DEEPWELL. Your bunker's real name is in here." } ]}, { id: 'school', name: 'Elementary School', x: 25, y: 60, type: 'shelter', danger: 0.2, loot: ['survivors', 'food'], discovered: false, desc: "Willowbrook Elementary. Someone painted \"SAFE HAVEN\" on the roof. It wasn't.", rooms: [ { id: 'cafeteria', name: 'Cafeteria', searched: false, loot: { food: 10, water: 6 }, danger: 0.1, flavor: 'Industrial-sized cans of vegetables. A child\'s drawing on the wall: "When is mommy coming?"' }, { id: 'gym', name: 'Gymnasium', searched: false, loot: { cloth: 3, survivors: 1 }, danger: 0.15, flavor: 'Cots arranged in rows. Someone organized a shelter here. It held for a while.' }, { id: 'library', name: 'Library', searched: false, loot: { lore: 3 }, danger: 0.05, flavor: 'Books. So many books. And a journal hidden behind the encyclopedias.' }, { id: 'basement', name: 'Boiler Room', searched: false, loot: { scrap: 4, fuel: 2 }, danger: 0.35, flavor: 'The boiler is still warm. There are handprints on the inside of the door.' } ]}, { id: 'factory', name: 'Old Factory', x: 70, y: 15, type: 'industrial', danger: 0.4, loot: ['scrap', 'electronics', 'chemicals'], discovered: false, desc: 'Meridian Manufacturing. Made car parts before. Makes something else now. The chimney still smokes.', rooms: [ { id: 'floor', name: 'Factory Floor', searched: false, loot: { scrap: 10, electronics: 2 }, danger: 0.3, flavor: 'Assembly lines frozen mid-production. The machines twitch occasionally.' }, { id: 'warehouse', name: 'Warehouse', searched: false, loot: { scrap: 6, chemicals: 4 }, danger: 0.25, flavor: 'Barrels of industrial chemicals. Some are leaking. Some are breathing.' }, { id: 'office', name: 'Management Office', searched: false, loot: { lore: 2, electronics: 3 }, danger: 0.2, flavor: 'A computer still on. Emails about "Project Resonance." Dates from 1987.' }, { id: 'furnace', name: 'The Furnace Room', searched: false, loot: { fuel: 5, scrap: 3 }, danger: 0.6, flavor: 'The furnace is on. It has always been on. The temperature reads -4°C.' } ]}, { id: 'church', name: 'Ruined Church', x: 45, y: 75, type: 'religious', danger: 0.3, loot: ['lore', 'cloth'], discovered: false, desc: 'Our Lady of Perpetual Silence. The steeple fell inward. The bell still rings at 3 AM.', rooms: [ { id: 'nave', name: 'Main Nave', searched: false, loot: { cloth: 4, lore: 1 }, danger: 0.2, flavor: 'Pews arranged in a spiral. Candles lit. Recently.' }, { id: 'crypt', name: 'The Crypt', searched: false, loot: { lore: 3, medicine: 2 }, danger: 0.5, flavor: "Older than the church above it. Older than the town. The names on the stones aren't in any language." }, { id: 'bell_tower', name: 'Bell Tower', searched: false, loot: { electronics: 2, scrap: 2 }, danger: 0.4, flavor: 'You can see the whole wasteland from here. Something can see you back.' }, { id: 'rectory', name: 'Rectory', searched: false, loot: { food: 5, water: 3, lore: 1 }, danger: 0.15, flavor: 'The priest\'s journal. Last entry: "It forgave me. I wish it hadn\'t."' } ]}, { id: 'bunker_17', name: 'Bunker 17', x: 90, y: 80, type: 'bunker', danger: 0.7, loot: ['supplies', 'survivors', 'lore'], discovered: false, desc: 'Sister facility. Sealed same day as yours. Radio contact lost day 4. The blast door is open now.', rooms: [ { id: 'entrance_17', name: 'Blast Door Entrance', searched: false, loot: { scrap: 3, weapons: 1 }, danger: 0.5, flavor: 'Claw marks on the INSIDE of the blast door. They wanted out more than anything wanted in.' }, { id: 'quarters_17', name: 'Living Quarters', searched: false, loot: { food: 8, water: 6, cloth: 3 }, danger: 0.4, flavor: '30 bunks. 30 names on the wall. 0 bodies.' }, { id: 'ops_17', name: 'Operations Room', searched: false, loot: { electronics: 5, lore: 4 }, danger: 0.6, flavor: 'Their logs. Day 1: normal. Day 2: normal. Day 3: "the floor is singing." Day 4: blank.' }, { id: 'lab_17', name: 'Research Lab', searched: false, loot: { medicine: 6, chemicals: 5, lore: 3 }, danger: 0.8, flavor: 'They were studying samples from below. The containment glass is shattered.' }, { id: 'deep_access', name: 'Sub-Level Access', searched: false, loot: { lore: 5 }, danger: 0.95, flavor: "Stairs going down. Your flashlight doesn't reach the bottom. Nothing does." } ]}, { id: 'the_pit', name: 'The Pit', x: 15, y: 40, type: 'horror', danger: 0.9, loot: ['artifact', 'truth'], discovered: false, desc: 'Not on any map. Not on any record. The ground opened on day one. It was waiting.', rooms: [ { id: 'rim', name: 'The Rim', searched: false, loot: { lore: 2 }, danger: 0.6, flavor: 'The edge. Perfectly circular. 200 meters across. The rocks are warm.' }, { id: 'descent', name: 'The Descent Path', searched: false, loot: { lore: 3, scrap: 2 }, danger: 0.8, flavor: 'Hand-carved steps spiraling down. They were carved from below, going up.' }, { id: 'gallery', name: 'The Gallery', searched: false, loot: { lore: 4 }, danger: 0.85, flavor: 'Paintings on the walls. Your bunker. Your people. Dated 10,000 years ago.' }, { id: 'heart', name: 'The Heart', searched: false, loot: { lore: 6 }, danger: 0.98, flavor: 'It beats. You can feel it in your teeth. This is where the voice comes from.' } ]} ]; // Location exploration events const locationEvents = [ { id: 'trapped_survivor', trigger: 'any', chance: 0.12, title: 'TRAPPED!', text: "A survivor is pinned under debris. They're alive, but freeing them will take time and make noise.", choices: [ { text: 'Free them. Worth the risk.', effect: { survivors: 1, time: 1, danger: 0.2 } }, { text: 'Too dangerous. Mark it and move on.', effect: { karma: -5 } }, { text: 'Put them out of their misery.', effect: { karma: -15, sanity: -5 } } ]}, { id: 'supply_stash', trigger: 'any', chance: 0.15, title: 'HIDDEN STASH', text: "Behind a false wall — someone's personal stockpile. Canned goods, water purification tablets, a handgun.", choices: [ { text: 'Take everything.', effect: { food: 8, water: 5, weapons: 1 } }, { text: 'Take half. Someone might come back for this.', effect: { food: 4, water: 2, karma: 5 } }, { text: 'Booby-trap it. If raiders find this, surprise.', effect: { food: 0, karma: -5, defense: 3 } } ]}, { id: 'raider_patrol', trigger: 'any', chance: 0.1, title: 'RAIDERS!', text: "A three-person raider patrol rounds the corner. They haven't seen you yet.", choices: [ { text: 'Ambush them.', effect: { weapons: 1, karma: -5, danger: 0.3, combatRisk: true } }, { text: 'Hide and wait.', effect: { time: 1 } }, { text: 'Approach peacefully. Offer to trade info.', effect: { karma: 0, tradeChance: true } }, { text: "Fall back. This room isn't worth it.", effect: { abort: true } } ]}, { id: 'strange_sound', trigger: 'any', chance: 0.08, title: 'DO YOU HEAR THAT?', text: "A low hum. Not mechanical. Organic. Coming from the walls. Your scavenger's nose starts bleeding.", choices: [ { text: 'Push through. Ignore it.', effect: { sanity: -8, lootBonus: 1.5 } }, { text: 'Retreat immediately.', effect: { abort: true, sanity: -3 } }, { text: 'Record it. This could be important.', effect: { sanity: -5, lore: 2 } } ]}, { id: 'hollow_shrine', trigger: 'religious', chance: 0.3, title: "THE HOLLOW'S MARK", text: "Fresh symbols on the walls. A Hollow offering — food, water, teeth — arranged in a pattern you almost understand.", choices: [ { text: 'Take the offerings.', effect: { food: 5, water: 3, factionRep: { hollow: -15 } } }, { text: 'Leave your own offering.', effect: { food: -3, factionRep: { hollow: 10 }, sanity: -3 } }, { text: 'Study the pattern.', effect: { lore: 3, sanity: -5 } }, { text: 'Destroy it.', effect: { factionRep: { hollow: -25 }, karma: 5 } } ]}, { id: 'old_recording', trigger: 'any', chance: 0.1, title: 'A MESSAGE', text: 'A tape recorder, still working. The voice is calm: "If you\'re hearing this, the bunker failed. They all fail eventually. Go deeper. The answer is always deeper."', choices: [ { text: 'Take the recording.', effect: { lore: 2 } }, { text: 'Play it for the team.', effect: { lore: 2, sanity: -5 } }, { text: 'Destroy it. Some things should stay buried.', effect: { karma: 0, sanity: 2 } } ]}, { id: 'collapsed_floor', trigger: 'any', chance: 0.08, title: 'FLOOR COLLAPSE!', text: 'The floor gives way! Your scavenger grabs the edge. Below: a room nobody knew existed.', choices: [ { text: 'Pull them up. Too risky.', effect: {} }, { text: 'Lower them down on a rope.', effect: { lootBonus: 2.0, danger: 0.3, time: 1 } }, { text: 'Everyone goes down.', effect: { lootBonus: 2.5, danger: 0.5, time: 2, discoveryChance: true } } ]}, { id: 'the_changed', trigger: 'any', chance: 0.06, title: 'THE CHANGED', text: "One of them. Standing perfectly still in the center of the room, facing the wall. It's... crying? No. Laughing. No. Both.", choices: [ { text: 'Back away slowly.', effect: { abort: true } }, { text: 'Put it down.', effect: { karma: -10, weapons: -1, sanity: -3 } }, { text: 'Try to communicate.', effect: { sanity: -10, lore: 3, factionRep: { changed: 5 } } }, { text: 'Observe from a distance.', effect: { lore: 1, sanity: -5, time: 1 } } ]} ]; // Travel events - triggered while scouting team is en route const travelEvents = [ { id: 'roadblock', title: 'ROADBLOCK', text: 'The main route is blocked by collapsed overpass debris. It will take time to find another way.', choices: [ { text: 'Climb over carefully.', effect: { delay: 0, danger: 0.2 } }, { text: 'Find a detour.', effect: { delay: 1 } }, { text: 'Blast through with charges.', effect: { delay: 0, weapons: -1, noise: true } } ]}, { id: 'dust_storm', title: 'DUST STORM', text: 'Visibility drops to zero. Toxic particles in the air. The team needs to decide fast.', choices: [ { text: 'Wait it out.', effect: { delay: 1 } }, { text: 'Push through.', effect: { danger: 0.3, sanity: -5 } }, { text: 'Find shelter in a nearby ruin.', effect: { delay: 1, discoveryChance: 0.4 } } ]}, { id: 'wanderer', title: 'THE WANDERER', text: "A figure on the road ahead. Walking away from you. It doesn't turn around. It's wearing a bunker jumpsuit. Yours.", choices: [ { text: "Follow it.", effect: { sanity: -8, discoveryChance: 0.6, lore: 1 } }, { text: "Call out to it.", effect: { sanity: -5 } }, { text: "Don't look back. Keep moving.", effect: { sanity: -2 } } ]}, { id: 'abandoned_camp', title: 'ABANDONED CAMP', text: 'A recent campsite. Fire still warm. Supplies left behind in a hurry. Whatever scared them could still be close.', choices: [ { text: 'Loot quickly and go.', effect: { food: 4, water: 3 } }, { text: 'Check for survivors.', effect: { survivorChance: 0.3, danger: 0.2 } }, { text: "It's a trap. Keep moving.", effect: {} } ]}, { id: 'radio_signal', title: 'SIGNAL DETECTED', text: "The team's radio picks up coordinates. Repeating. Close by. Could be a supply cache. Could be bait.", choices: [ { text: 'Follow the signal.', effect: { delay: 1, discoveryChance: 0.5, danger: 0.2 } }, { text: 'Log coordinates for later.', effect: { lore: 1 } }, { text: 'Jam the signal. Could be tracking us.', effect: { sanity: -3 } } ]}, { id: 'animal_pack', title: 'FERAL PACK', text: 'A pack of dogs. Or what used to be dogs. They circle the team, not attacking. Watching.', choices: [ { text: 'Scare them off with gunfire.', effect: { weapons: -1 } }, { text: 'Offer food and try to befriend one.', effect: { food: -3, petChance: 0.4 } }, { text: 'Stay still. Wait for them to lose interest.', effect: { delay: 0 } } ]} ]; // Outpost types - buildable at cleared locations const outpostTypes = [ { id: 'supply_cache', name: 'Supply Cache', desc: 'Stockpile for gathered resources', type: 'any', cost: { food: 10, scrap: 5 }, dailyYield: { food: 2, water: 1 } }, { id: 'watchtower', name: 'Watchtower', desc: 'Early warning system', type: 'any', cost: { scrap: 8 }, dailyYield: { defense: 5 } }, { id: 'field_hospital', name: 'Field Hospital', desc: 'Remote medical station', type: 'medical', cost: { medicine: 5, scrap: 3 }, dailyYield: { medicine: 1 } }, { id: 'scrap_yard', name: 'Scrap Yard', desc: 'Automated salvage operation', type: 'industrial', cost: { scrap: 5, electronics: 3 }, dailyYield: { integrity: 3 } }, { id: 'armory_cache', name: 'Forward Armory', desc: 'Weapons depot', type: 'military', cost: { weapons: 3, scrap: 5 }, dailyYield: { defense: 3 } }, { id: 'listening_post', name: 'Listening Post', desc: 'Intel on faction movements', type: 'any', cost: { electronics: 5, scrap: 3 }, dailyYield: { defense: 2 } } ]; // Clearing rewards - one-time bonus for fully exploring all rooms const clearingRewards = { hospital: { name: 'Medical Archives', desc: 'Complete pharmaceutical database. Medicine production doubled.', effect: { medicine: 20, morale: 5 } }, mall: { name: 'Underground Vault', desc: 'A prepper vault beneath the parking garage. Jackpot.', effect: { food: 40, water: 30, scrap: 15 } }, military_base: { name: 'Project DEEPWELL Files', desc: 'Classified. Your bunker was not built for protection. It was built for containment.', effect: { lore: 10, sanity: -15, storyAdvance: true } }, school: { name: 'Safe Haven Network', desc: 'A network of safe houses. Trade routes and survivor locations.', effect: { karma: 10, morale: 10, food: 15 } }, factory: { name: 'The Machine', desc: 'It still works. You do not understand what it does. But it helps.', effect: { morale: 15, sanity: 10 } }, church: { name: 'The Book of Names', desc: 'Every person who will ever enter your bunker. Written before the war.', effect: { lore: 8, sanity: -20 } }, bunker_17: { name: 'Sister Bunker Data Core', desc: 'Complete logs. What happened here will happen to you. Unless you read carefully.', effect: { lore: 15, storyAdvance: true, sanity: -10 } }, the_pit: { name: 'THE TRUTH', desc: 'You understand now. The bunker. The surface. The things below. It was never about survival. It was about selection.', effect: { lore: 20, sanity: -30, storyAdvance: true } } }; const getLocIcon = (type) => type === 'medical' ? '🏥' : type === 'supplies' ? '🏬' : type === 'military' ? '🎖️' : type === 'shelter' ? '🏫' : type === 'industrial' ? '🏭' : type === 'religious' ? '⛪' : type === 'bunker' ? '🚪' : type === 'horror' ? '👁️' : '❓'; // ============ PERSONAL QUESTS ============ // ============ FOUND NOTES ============ const foundNotes = [ { id: 'note_01', title: 'Scratched into the wall', text: 'Day 1. They said it would be temporary. They lied. The locks work from the outside too. -J.K.', location: 'Storage Room B' }, { id: 'note_02', title: 'Crumpled letter', text: 'My dearest Maria, if you find this, know I tried. The water started tasting like copper on day 12. By day 15, Hendricks was gone. Not dead. Gone. His bunk was still warm.', location: 'Under a mattress' }, { id: 'note_03', title: 'Medical log fragment', text: 'Patient 7 exhibits rapid cellular degeneration. Patient 9 shows the opposite - accelerated growth. Both exposed to same compound. God help us, this was supposed to be a vaccine.', location: 'Infirmary cabinet' }, { id: 'note_04', title: 'Child\'s drawing', text: 'A crayon drawing of stick figures underground. One figure is drawn entirely in black. All the other figures are running from it. On the back: "the tall man lives in the walls"', location: 'Vent shaft' }, { id: 'note_05', title: 'Encrypted note (decoded)', text: 'SECTOR 7 COMPROMISED. BUNKER 12 FELL. BUNKER 17 DARK. DO NOT TRUST THE BROADCASTS. THEY ARE NOT FROM US. REPEAT: THEY ARE NOT FROM US. -ECHO COMMAND', location: 'Hidden radio compartment' }, { id: 'note_06', title: 'Personal diary page', text: 'We voted. 14 to 3. The three went outside. I can still hear Dr. Okafor screaming my name. We made the right choice. We made the right choice. We made the right choice.', location: 'Between wall panels' }, { id: 'note_07', title: 'Inventory list', text: 'MISSING: 47 ration packs, 12 water filters, 3 hazmat suits, 1 security keycard (LEVEL 5), ALL ammunition from Locker 7. Someone is preparing to leave. Or preparing for something worse.', location: 'Security desk drawer' }, { id: 'note_08', title: 'Scratched tally marks', text: 'Hundreds of tally marks scratched into the wall, then abruptly stopping. Below them: "they count differently down here"', location: 'Sub-level 3 stairwell' }, { id: 'note_09', title: 'Polaroid photo', text: 'A faded photo of a group of 20 people smiling in front of THIS bunker. The date reads 3 years before the apocalypse. On the back: "First shift. Full of hope. Full of lies."', location: 'Taped behind a mirror' }, { id: 'note_10', title: 'Final transmission log', text: 'MAYDAY MAYDAY. This is Bunker 12 Sector 7. The deep levels have been breached. Something is coming up through the floor. It knows our names. It knows our—[SIGNAL LOST]', location: 'Radio room logbook' }, { id: 'note_11', title: 'Maintenance report', text: 'Third time this month the sub-basement temperature has dropped to -15C despite heating running at full. Frost patterns on walls do NOT match any natural formation. Request immediate investigation of sub-level.', location: 'Engineering clipboard' }, { id: 'note_12', title: 'Torn journal', text: 'I found the other bunker. Bunker 17. Doors were open. Lights on. Food still warm on the tables. 200 people lived there. Not a single body. Not a single drop of blood. Just silence and warm food.', location: 'Scout pack' }, { id: 'note_13', title: 'Written in blood', text: 'DON\'T DIG. The words are written on the wall in what testing confirmed was human blood. The handwriting matches no one in the previous occupant roster. Analysis dates the blood to 40 years before the bunker was built.', location: 'Deepest sub-level' }, { id: 'note_14', title: 'Love letter', text: 'I know what you did to keep us alive. I know about the pills in the water. I know about Room 6. And I forgive you, because our children are still breathing. But God may not. -S', location: 'Commander\'s quarters' }, { id: 'note_15', title: 'List of names', text: 'A list of 42 names with dates next to them. Every name has been crossed out in red except the last one. The last name is yours.', location: 'Behind the generator' } ]; // ============ MEMBER CONFESSIONS ============ const memberConfessions = [ { id: 'conf_arson', text: "I... I started the fire. Back in Sector 4. People died. I told myself it was an accident but I knew the wiring was bad. I just didn't want to fix it in the cold.", minTrust: 70, mood: 'guilty' }, { id: 'conf_abandon', text: "I left my family behind. Not because I couldn't save them. Because I was faster alone. I hear my daughter calling me every night.", minTrust: 75, mood: 'haunted' }, { id: 'conf_cannibal', text: "There were 8 of us in the basement for 40 days. Only 3 walked out. I can still taste it. Don't look at me like that. You would have done the same.", minTrust: 80, mood: 'dark' }, { id: 'conf_spy', text: "I'm not who I said I am. I was military intelligence. The apocalypse? We had 72 hours warning. They only evacuated the 'essential personnel.' Everyone else was... acceptable losses.", minTrust: 65, mood: 'bitter' }, { id: 'conf_experiment', text: "They tested something on me at the hospital. Before everything. I signed a waiver I didn't read. Now sometimes I can hear... frequencies. Voices in the static. They're getting clearer.", minTrust: 60, mood: 'afraid' }, { id: 'conf_mercy', text: "I killed someone. But it was mercy. She was Changing, and she begged me. Held her hand and... I still see her eyes. Grateful. That's the worst part. She was grateful.", minTrust: 70, mood: 'grieving' }, { id: 'conf_hoard', text: "I have a cache. Outside. Three days worth of food and water, a weapon. If things go bad here, I'm gone. I know it makes me a coward. But I've already died in one bunker.", minTrust: 55, mood: 'pragmatic' }, { id: 'conf_child', text: "My son is still out there. I know he is. Everyone says he's gone but I can feel him. Mothers know. He's in Bunker 17. I need to get there. Will you help me?", minTrust: 65, mood: 'desperate' }, { id: 'conf_immune', text: "The radiation doesn't affect me. Nothing does. I've been exposed to doses that should have killed me ten times over. I don't know what I am anymore.", minTrust: 70, mood: 'confused' }, { id: 'conf_betrayal', text: "The last bunker I was in? I'm the reason it fell. I opened the door. They were going to execute an innocent man and I couldn't let them. Thirty people died because of my conscience.", minTrust: 75, mood: 'conflicted' }, { id: 'conf_voices', text: "Something talks to me from the deep levels. It knows things about me that no one knows. It promised me my family back. I haven't said yes... yet.", minTrust: 80, mood: 'tempted' }, { id: 'conf_government', text: "I worked for the agency that built these bunkers. They're not shelters. They're containers. We're not being protected. We're being stored.", minTrust: 85, mood: 'paranoid' } ]; // ============ THE VOICE ON THE RADIO ============ const voiceOnRadioMessages = [ { day: 5, text: "...hello? Is someone there? I can see your signal. Don't worry, I'm a friend.", stage: 1 }, { day: 10, text: "You're doing well, Tex. Better than the last group. Much better.", stage: 2 }, { day: 15, text: "I've been watching the bunker for a long time. Long before you arrived. Do you know why you were chosen?", stage: 3 }, { day: 20, text: "The faction attacks are increasing, aren't they? I could help with that. All I need is for you to open sub-level 4.", stage: 4 }, { day: 25, text: "I know about Walter Banks. I know about the guitar. I know what you see when you play. The auras aren't what you think they are.", stage: 5 }, { day: 30, text: "One of your people is talking to me too. They don't know I'm also talking to you. Isn't that interesting?", stage: 6 }, { day: 40, text: "The previous occupants heard me too. They thought I was the enemy. By the time they realized I was trying to help, it was too late. Please don't make their mistake.", stage: 7 }, { day: 50, text: "I'm going to tell you something now, and you need to stay calm. I am not broadcasting from outside the bunker. I am broadcasting from below it.", stage: 8 }, { day: 60, text: "You found the notes, didn't you? The ones from before. They called me 'The Voice.' Unimaginative. I prefer my real name. But you're not ready for that yet.", stage: 9 }, { day: 75, text: "Something is coming. Not from outside. Not from below. From between. The walls of your bunker exist in two places at once. I've been trying to seal the gap. I'm failing.", stage: 10 } ]; // ============ MORALE EVENTS ============ const moraleEventOptions = [ { id: 'movie_night', name: '🎬 Movie Night', desc: 'Project old films on the wall. A reminder of better times.', cost: { power: 5 }, boost: 12, cooldown: 3 }, { id: 'guitar_concert', name: '🎸 Tex\'s Concert', desc: 'Tex plays guitar for everyone. Music heals.', cost: {}, boost: 15, cooldown: 5 }, { id: 'story_circle', name: '📖 Story Circle', desc: 'Members share stories from before. Some laugh. Some cry.', cost: {}, boost: 8, cooldown: 2 }, { id: 'feast', name: '🍖 Feast Day', desc: 'Prepare a special meal with extra rations.', cost: { food: 15 }, boost: 18, cooldown: 7 }, { id: 'memorial', name: '🕯️ Memorial Service', desc: 'Honor those we\'ve lost. Grief shared is grief halved.', cost: {}, boost: 10, cooldown: 4, requiresDeaths: true }, { id: 'dance', name: '💃 Dance Night', desc: 'Crank up the radio and let loose.', cost: { power: 3 }, boost: 14, cooldown: 4 }, { id: 'games', name: '🎲 Game Tournament', desc: 'Cards, dice, whatever we can find. Competition breeds camaraderie.', cost: {}, boost: 10, cooldown: 3 }, { id: 'talent_show', name: '🎤 Talent Show', desc: 'Everyone has hidden talents. Time to share them.', cost: {}, boost: 13, cooldown: 6 } ]; // ============ RIVAL BUNKER ============ const rivalBunkerData = { name: 'Bunker 23', leader: 'Commander Reyes', population: 15, attitude: 50, // 0=hostile, 50=neutral, 100=allied strength: 8, resources: { food: 80, water: 70 }, traits: ['organized', 'suspicious', 'resourceful'], messages: { neutral: [ "Commander Reyes here. We know you're out there. Let's keep things civil.", "Bunker 23 to unknown bunker. We have no quarrel with you. Yet.", "Our scouts spotted your people. We could be allies, or we could be problems for each other." ], friendly: [ "Tex, it's Reyes. Good to hear from you. What do you need?", "Bunker 23 standing by. Our people speak well of your hospitality.", "We've got surplus medical supplies. Interested in a trade?" ], hostile: [ "You've made an enemy today. Bunker 23 does not forget.", "Your bunker's resources belong to those strong enough to take them.", "Reyes here. Last warning. Return what you stole or face consequences." ] } }; const rivalActions = [ { id: 'trade_food', name: '📦 Trade: Food for Water', desc: 'Send 20 food, receive 20 water', cost: { food: 20 }, gives: { water: 20 }, attitudeReq: 30, attitudeChange: 5 }, { id: 'trade_water', name: '💧 Trade: Water for Food', desc: 'Send 20 water, receive 20 food', cost: { water: 20 }, gives: { food: 20 }, attitudeReq: 30, attitudeChange: 5 }, { id: 'share_intel', name: '📡 Share Intelligence', desc: 'Trade map data and faction info', cost: {}, gives: { mapReveal: true }, attitudeReq: 40, attitudeChange: 8 }, { id: 'send_gift', name: '🎁 Send Gift Package', desc: 'Send 15 food + 10 water as goodwill', cost: { food: 15, water: 10 }, gives: {}, attitudeReq: 0, attitudeChange: 15 }, { id: 'request_help', name: '🆘 Request Reinforcements', desc: 'Ask for 2 fighters for defense', cost: {}, gives: { defense: 10 }, attitudeReq: 60, attitudeChange: -10 }, { id: 'joint_raid', name: '⚔️ Joint Raid on Faction', desc: 'Combined attack on a hostile faction', cost: {}, gives: { food: 25, water: 15, scrap: 10 }, attitudeReq: 70, attitudeChange: 10 }, { id: 'raid_them', name: '🗡️ Raid Bunker 23', desc: 'Attack and steal their resources (HOSTILE)', cost: {}, gives: { food: 30, water: 25 }, attitudeReq: -999, attitudeChange: -40 }, { id: 'alliance', name: '🤝 Propose Alliance', desc: 'Formal alliance for mutual defense', cost: {}, gives: { defense: 5, morale: 10 }, attitudeReq: 80, attitudeChange: 15 } ]; const questTemplates = [ { id: 'find_family', name: 'Find My Family', desc: 'Locate family members in the wasteland', stages: 3, reward: { loyalty: 50, morale: 20 } }, { id: 'revenge', name: 'Vengeance', desc: 'Find the people who wronged them', stages: 2, reward: { loyalty: 30, karma: -10 } }, { id: 'redemption', name: 'Redemption', desc: 'Make amends for past mistakes', stages: 3, reward: { karma: 30, loyalty: 20 } }, { id: 'knowledge', name: 'The Truth', desc: 'Discover what caused the apocalypse', stages: 4, reward: { lore: true, sanity: -10 } }, { id: 'cure', name: 'Find a Cure', desc: 'Search for a cure to the mutations', stages: 5, reward: { cure: true, karma: 20 } } ]; // ============ SECRETS ============ const secretTypes = [ { id: 'murderer', name: 'Former Murderer', reveal: 'They killed someone before the apocalypse. Self-defense? Or cold blood?', effect: { fear: true } }, { id: 'scientist', name: 'Involved Scientist', reveal: 'They worked on the project that caused all this.', effect: { knowledge: true, guilt: true } }, { id: 'immune', name: 'Immune', reveal: 'They cannot be Changed. Their blood might be valuable.', effect: { target: true } }, { id: 'spy', name: 'Former Spy', reveal: 'They were a government agent. Old skills, old secrets.', effect: { skills: true } }, { id: 'hollow_escaped', name: 'Escaped the Hollow', reveal: 'They were converted once, but broke free. Partially.', effect: { whisperResist: true, sanityPenalty: true } }, { id: 'cannibal', name: 'Ate Human Flesh', reveal: 'They survived by eating the dead. The hunger never fully leaves.', effect: { trauma: true } }, { id: 'child_lost', name: 'Lost a Child', reveal: 'Their child didn\'t make it. They see them in every young face.', effect: { childTrigger: true } } ]; // Visitor questions const visitorQuestions = [ { id: 'background', texText: "Tell me about yourself. Where'd you come from?", banksText: "State your background. Previous occupation. Now." }, { id: 'skills', texText: "What can you do? Everyone pitches in here.", banksText: "List your useful skills. Be specific." }, { id: 'group', texText: "You traveling alone out there?", banksText: "Are there others? Did anyone follow you?" }, { id: 'intentions', texText: "Look, space is tight. Why should we take you in?", banksText: "Justify your admission. What value do you provide?" }, { id: 'secrets', texText: "I'm not gonna judge, but... anything I should know?", banksText: "Full disclosure. Any information you're withholding?" }, { id: 'trust', texText: "Can I count on you?", banksText: "Loyalty is mandatory. Will you comply with all directives?" } ]; // Generate visitor responses const generateVisitorResponse = (visitor, questionId, identity) => { const isThreat = visitor.isThreat; const personality = visitor.personality; const isHonest = personality?.id === 'honest'; const isManipulative = personality?.id === 'manipulative'; const isNervous = personality?.id === 'coward' || personality?.id === 'volatile'; const faction = visitor.threatInfo?.faction; const address = identity === 'tex' ? (visitor.mood > 50 ? randomFrom(['Tex', 'Mr. Santos']) : randomFrom(['Look', '...'])) : randomFrom(['Sir', 'Mr. Banks', 'Director']); const topSkill = Object.entries(visitor.skills).sort((a,b) => b[1] - a[1])[0]; // Faction-specific creepy hints (subtle) const factionHints = { hollow: { secrets: `*stares vacantly* We must all become... empty. ${address}, I have nothing to hide.`, trust: `Trust is... irrelevant. In the end, we all become nothing.`, background: `I was... someone else before. That person is gone now. Empty.` }, whisper: { secrets: `*tilts head as if listening* They say there are no secrets. Not anymore.`, trust: `*smiles too wide* We already know you, ${address}. We've been watching.`, group: `Alone? *laughs softly* No one is ever truly alone. They're always... listening.` }, changed: { background: `I was normal once. Before the blessing. Before I started... changing.`, intentions: `*scratches arm* I just need shelter. Somewhere... warm. To grow.`, skills: `I'm... adapting. Every day I can do things I couldn't before. *twitches*` }, feeders: { intentions: `*licks lips* Your bunker looks... well-stocked. I could help with that.`, group: `My family is... hungry. Always hungry. But I came alone. For now.`, secrets: `*stares at your neck* Just hungry, ${address}. So very hungry.` }, remnant: { background: `I served before. Chain of command. Following orders. Some habits don't die.`, trust: `I know how to follow orders, ${address}. The question is: do you know how to give them?`, intentions: `Your operation here is... interesting. Very organized. Command would approve.` } }; const responses = { background: { honest: `${address}, I was a ${visitor.background.name.toLowerCase()}. ${visitor.backstory}`, evasive: `Does it matter where I came from? I'm here now.`, nervous: `I-I'm just... I was a ${visitor.background.name.toLowerCase()}. Please, I need shelter.`, threat: faction && factionHints[faction.id]?.background ? factionHints[faction.id].background : `I've survived this long. That's all you need to know.` }, skills: { honest: `I'm decent at ${topSkill[0]} - about ${topSkill[1].toFixed(1)} out of 5. I can help.`, evasive: `I can do what needs doing. I'm adaptable.`, nervous: `I-I can help! I know some ${topSkill[0]}...`, threat: faction && factionHints[faction.id]?.skills ? factionHints[faction.id].skills : `I know how to survive. That should be enough.` }, group: { honest: `No, I'm alone. Lost my group weeks back.`, evasive: `I'm here alone now. That's what matters.`, nervous: `N-no one followed me! I made sure... I think.`, threat: faction && factionHints[faction.id]?.group ? factionHints[faction.id].group : (isThreat ? `Maybe. Why do you ask?` : `Just me.`) }, intentions: { honest: `I just want to survive, ${address}. I'll work hard.`, evasive: `I can contribute. That's all anyone can offer.`, nervous: `Please... I've been out there so long. I just need safety.`, threat: faction && factionHints[faction.id]?.intentions ? factionHints[faction.id].intentions : `Because turning me away would be a mistake.` }, secrets: { honest: isHonest ? `${address}, I'm an open book.` : `Nothing dangerous... just trying to survive.`, evasive: `We all have things we'd rather forget.`, nervous: `N-no! Nothing! I swear!`, threat: faction && factionHints[faction.id]?.secrets ? factionHints[faction.id].secrets : (isThreat && !isManipulative ? `*long pause* ...Nothing you need to worry about.` : `My secrets are my own.`) }, trust: { honest: `${address}, I give you my word.`, evasive: `Trust is earned. Let me prove myself.`, nervous: `Y-yes! I promise! I just want to be safe!`, threat: faction && factionHints[faction.id]?.trust ? factionHints[faction.id].trust : (identity === 'banks' ? `I'll follow orders. That's what you want, right?` : `I'm not looking for trouble.`) } }; // Pick response style let style = 'honest'; if (isThreat && isManipulative) style = 'honest'; else if (isThreat) style = Math.random() < 0.4 ? 'evasive' : 'threat'; // More likely to slip up else if (isNervous) style = 'nervous'; else if (isManipulative) style = 'evasive'; return responses[questionId]?.[style] || `*looks uncertain*`; }; // Member conversation topics const memberTopics = [ { id: 'howAreYou', texText: "How you holding up?", banksText: "Status report. How are you?" }, { id: 'concerns', texText: "Anything bothering you?", banksText: "Report any concerns." }, { id: 'others', texText: "How are you getting along with everyone?", banksText: "Assessment of other personnel?" }, { id: 'job', texText: "How's the work going?", banksText: "Report on your duties." }, { id: 'trust', texText: "Do you trust me to lead us?", banksText: "Your assessment of leadership?" }, { id: 'improve', texText: "What would make things better?", banksText: "Suggestions for improvement?" } ]; // Generate member responses const generateMemberResponse = (member, topicId, identity, resources, allMembers) => { const mood = member.mood; const moodLevel = getMoodConsequence(mood, member.personality); const address = getAddressStyle(identity, member.loyalty); const personality = member.personality; const responses = { howAreYou: { critical: identity === 'tex' ? `*doesn't meet your eyes* Honestly ${address}? I'm barely holding on.` : `*stiff* ...Managing, sir.`, low: identity === 'tex' ? `Could be better, ${address}. The stress is getting to me.` : `Functioning, sir.`, neutral: identity === 'tex' ? `Hanging in there, ${address}. One day at a time.` : `Adequate, sir.`, good: identity === 'tex' ? `Pretty good, ${address}! Thanks for asking.` : `Well, sir.`, excellent: identity === 'tex' ? `Great, ${address}! I feel hopeful.` : `Excellent, sir.` }, concerns: { critical: `Yeah. ${resources.food < 30 ? 'We\'re starving.' : resources.water < 30 ? 'No water.' : 'We\'re all gonna die down here.'}`, low: `${address}, things are tense. ${resources.morale < 50 ? 'People are losing hope.' : 'We need more supplies.'}`, neutral: `Nothing major, ${address}. The usual worries.`, good: `Not really. You're doing a good job, ${address}.`, excellent: `Honestly? I think we might make it.` }, others: () => { if (personality?.id === 'paranoid') return `*whispers* ${address}, I don't trust everyone here. Watch your back.`; if (personality?.id === 'social') return `I love this group! We're like family now.`; if (personality?.id === 'loner') return `I keep to myself. Less drama.`; if (personality?.id === 'aggressive') return `Some people need to learn their place.`; return `We're all just trying to survive, ${address}.`; }, job: () => { if (!member.job) return `I don't have an assignment yet, ${address}. Put me to work.`; if (personality?.id === 'lazy') return `*sighs* It's work. Gets done... eventually.`; if (personality?.id === 'hardworking') return `Going great, ${address}! Putting in extra hours.`; return `The ${jobDefinitions[member.job]?.name} keeps me busy. ${mood > 60 ? 'I don\'t mind it.' : 'It\'s exhausting.'}`; }, trust: { high: identity === 'tex' ? `${address}, you play that guitar and I see you care. Yeah, I trust you.` : `You've kept us alive, sir. You have my respect.`, medium: identity === 'tex' ? `I want to, ${address}. Keep being straight with us.` : `Trust is earned, sir. You're adequate so far.`, low: identity === 'tex' ? `*hesitates* I'm... still figuring that out.` : `*uncomfortable* I follow orders. Isn't that enough?` }, improve: { critical: `${randomFrom(['More food', 'Clean water', 'Some hope', 'Less death'])} would help.`, low: `Better conditions. ${resources.food < 50 ? 'Food\'s running low.' : 'People need to feel safe.'}`, neutral: `Little things, ${address}. Music, moments of normalcy.`, good: `Just keep doing what you're doing, ${address}.`, excellent: `More of the same. You've built something good here.` } }; const topic = responses[topicId]; if (typeof topic === 'function') return topic(); if (topicId === 'trust') { if (member.loyalty > 70) return topic.high; if (member.loyalty > 40) return topic.medium; return topic.low; } return topic?.[moodLevel.level] || topic?.neutral || `*shrugs*`; }; // Names const firstNames = ['Marcus', 'Elena', 'Dwayne', 'Sarah', 'Viktor', 'Mei', 'Carlos', 'Anya', 'Tyrell', 'Fatima', 'Javier', 'Yuki', 'Omar', 'Svetlana', 'Kwame', 'Rosa', 'Dmitri', 'Aisha', 'Kofi', 'Ingrid', 'Raj', 'Lucia', 'Chen', 'Amara', 'Pavel']; const lastNames = ['Chen', 'Volkov', 'Martinez', 'O\'Brien', 'Nakamura', 'Okonkwo', 'Singh', 'Petrov', 'Garcia', 'Kim', 'Hassan', 'Reyes', 'Johansson', 'Diallo', 'Tanaka', 'Kovacs', 'Oyelaran', 'Fernandez', 'Lindqvist', 'Abara', 'Patel', 'Nowak', 'Santos', 'Weber', 'Ali']; const backstories = [ "Found wandering near the old highway. Claims to be a former {background}.", "Says their settlement was overrun. Has {background} training.", "Traveling alone. Background as a {background} is evident.", "Claims to know about a supply cache. Former {background}.", "Found hiding in an abandoned store. Reluctantly admits to being a {background}.", "Says they escaped from another bunker. {background} skills might be useful.", "Has visible scars. {background} background, they say.", "Young but hardened. Learned {background} skills from family.", "Older, experienced. {background} for over 20 years before the collapse.", "Carries tools of a {background}. Speaks little but works hard." ]; // ============ HELPER FUNCTIONS ============ const randomFrom = (arr) => arr[Math.floor(Math.random() * arr.length)]; const randomRange = (min, max) => Math.floor(Math.random() * (max - min + 1)) + min; const generateAppearance = () => ({ skinTone: randomFrom(skinTones), hairColor: randomFrom(hairColors), hairStyle: randomFrom(hairStyles), facialHair: randomFrom(facialHairTypes), accessory: randomFrom(accessoryTypes), expression: randomFrom(expressionTypes), eyeColor: ['#2c1810', '#1a4a6b', '#2a5a2a', '#4a3a2a', '#1a1a1a'][Math.floor(Math.random() * 5)], gender: Math.random() > 0.5 ? 'masc' : 'fem' }); const generateSurvivor = () => { const background = randomFrom(backgrounds); const personality = randomFrom(personalityTypes); const isThreat = Math.random() < 0.30; const hasAgenda = Math.random() < 0.40; // Determine threat details if this person is dangerous let threatInfo = null; if (isThreat) { const faction = randomFrom(factions); const threatType = randomFrom(threatTypes.filter(t => t.faction === 'any' || (Array.isArray(t.faction) && t.faction.includes(faction.id)) )); threatInfo = { faction: faction, threatType: threatType, actionsUntilStrike: randomRange(2, 5), // Days before they act hasActed: false, suspicionLevel: 0 // Increases when they do suspicious things }; } // Base skills from background const skills = { scavenging: 1, security: 1, medical: 1, engineering: 1, cooking: 1, farming: 1, maintenance: 1, community: 1, ...background.skills }; // Add some random variation Object.keys(skills).forEach(skill => { skills[skill] = Math.max(1, Math.min(5, skills[skill] + randomRange(-1, 1))); }); const backstory = randomFrom(backstories).replace('{background}', background.name.toLowerCase()); // Starting mood based on personality const baseMood = personality.baseMood + randomRange(-10, 10); return { id: Date.now() + Math.random(), name: `${randomFrom(firstNames)} ${randomFrom(lastNames)}`, background, personality, backstory, appearance: generateAppearance(), skills, isThreat, threatInfo, hasAgenda, agendaType: hasAgenda ? randomFrom(['power', 'supplies', 'sabotage', 'escape', 'revenge']) : null, revealed: false, job: null, health: 100, hunger: randomRange(70, 100), thirst: randomRange(70, 100), mood: Math.max(10, Math.min(100, baseMood)), loyalty: isThreat ? randomRange(10, 40) : randomRange(50, 90), daysInBunker: 0, isInnerCircle: false, isConfined: false, currentActivity: 'idle', activityFrame: Math.floor(Math.random() * 6), lastMoodEvent: null, opinions: {}, // New attributes for expanded systems sanity: 100, isConverted: false, // Turned by a cult conversionProgress: 0, // 0-100, converts at 100 isWatched: false, // Being surveilled suspicionOnThem: 0, // How much others suspect them romanceWith: null, // ID of romantic partner friends: [], // IDs of friends enemies: [], // IDs of enemies cliqueId: null, // Which clique they belong to secretKnown: false, // Do they know a bunker secret transformationProgress: 0, // For Changed infection isPossessed: false, // Whisper possession lastWords: null // What they said before death/transformation }; }; const generateNewspaper = (day, members, problems, events, deadPeople, resources) => { const articles = []; const headline = randomFrom(newsTemplates.headlines).replace('{day}', day); // Add event reports (limit to most recent) events.slice(-10).forEach(event => { articles.push({ type: 'event', content: event }); }); // Add death reports deadPeople.forEach(death => { const causeText = death.cause === 'starvation' ? 'starved to death' : death.cause === 'dehydration' ? 'died of thirst' : death.cause === 'exposure' ? 'died waiting outside' : death.cause === 'scavenging' ? 'was lost while scavenging' : 'passed away'; articles.push({ type: 'death', content: `${death.name} ${causeText}. They will be remembered.` }); }); // Add problem reports problems.filter(p => p.severity >= 2).forEach(problem => { articles.push({ type: 'problem', content: `CONCERN: ${problem.name} - ${problem.desc} (Severity: ${problem.severity.toFixed(1)}/5)` }); }); // Add resource warnings if (resources.food < 30) articles.push({ type: 'warning', content: 'Food supplies critically low!' }); if (resources.water < 30) articles.push({ type: 'warning', content: 'Water reserves running dry!' }); if (resources.power < 30) articles.push({ type: 'warning', content: 'Power systems strained!' }); if (resources.morale < 30) articles.push({ type: 'warning', content: 'Morale at dangerous levels!' }); // Add gossip if we have enough members if (members.length >= 2) { const gossipCount = Math.min(3, Math.floor(members.length / 3)); for (let i = 0; i < gossipCount; i++) { let gossip = randomFrom(newsTemplates.gossip); const person1 = randomFrom(members); gossip = gossip.replace('{name}', person1.name); gossip = gossip.replace('{background}', person1.background.name.toLowerCase()); if (gossip.includes('{name2}')) { const others = members.filter(m => m.id !== person1.id); if (others.length > 0) { gossip = gossip.replace('{name2}', randomFrom(others).name); } } articles.push({ type: 'gossip', content: gossip }); } } // Add good or bad news based on conditions if (resources.morale > 60 && deadPeople.length === 0) { articles.push({ type: 'good', content: randomFrom(newsTemplates.goodNews) }); } else { articles.push({ type: 'bad', content: randomFrom(newsTemplates.badNews) }); } return { headline, articles, day }; }; // ============ PIXEL ART PORTRAIT ============ const CharacterPortrait = ({ appearance, size = 100, revealed, isThreat, isInnerCircle }) => { const skin = appearance.skinTone; const hair = appearance.hairColor; const bgColor = revealed ? (isThreat ? '#2a1515' : '#152a15') : '#1a1815'; const borderColor = isInnerCircle ? '#c4a35a' : revealed ? (isThreat ? '#aa4444' : '#44aa44') : '#4a4035'; return (
{/* Neck & shoulders */} {/* Head base */} {/* Shading */} {/* Ears */} {/* Hair styles */} {appearance.hairStyle === 'short' && ( <> )} {appearance.hairStyle === 'spiky' && ( <> )} {appearance.hairStyle === 'long' && ( <> )} {appearance.hairStyle === 'mohawk' && ( <> )} {appearance.hairStyle === 'ponytail' && ( <> )} {appearance.hairStyle === 'buzzcut' && ( <> )} {appearance.hairStyle === 'curly' && ( <> )} {appearance.hairStyle === 'slick' && ( <> )} {/* Eyes */} {/* Eyebrows by expression */} {appearance.expression === 'neutral' && ( <> )} {appearance.expression === 'stern' && ( <> )} {appearance.expression === 'worried' && ( <> )} {appearance.expression === 'angry' && ( <> )} {appearance.expression === 'tired' && ( <> )} {/* Nose */} {/* Mouth */} {/* Facial hair */} {appearance.facialHair === 'stubble' && appearance.gender === 'masc' && ( )} {appearance.facialHair === 'beard' && appearance.gender === 'masc' && ( <> )} {appearance.facialHair === 'bigbeard' && appearance.gender === 'masc' && ( <> )} {appearance.facialHair === 'mustache' && appearance.gender === 'masc' && ( <> )} {/* Accessories */} {appearance.accessory === 'glasses' && ( <> )} {appearance.accessory === 'eyepatch' && ( <> )} {appearance.accessory === 'scar' && ( <> )} {appearance.accessory === 'bandana' && ( <> )} {appearance.accessory === 'mask' && ( )} {appearance.accessory === 'earring' && ( <> )} {/* Status indicators */} {revealed && ( isThreat ? ( ) : ( ) )} {isInnerCircle && ( )}
); }; // ============ MAIN GAME COMPONENT ============ function TheBunker() { // Core state const [gameState, setGameState] = useState('title'); const [day, setDay] = useState(1); const [gameTime, setGameTime] = useState(0); // Seconds elapsed today const [resources, setResources] = useState({ food: 100, water: 100, power: 100, morale: 70, foodDecayRate: 2, waterDecayRate: 1 }); // Population const [bunkerMembers, setBunkerMembers] = useState([]); const [visitorQueue, setVisitorQueue] = useState([]); // Queue of visitors waiting const [currentVisitor, setCurrentVisitor] = useState(null); // Currently being evaluated const [innerCircle, setInnerCircle] = useState([]); const [recentDeaths, setRecentDeaths] = useState([]); // Deaths this session // Problems & Events const [activeProblems, setActiveProblems] = useState([]); const [dailyEvents, setDailyEvents] = useState([]); // Newspaper const [newspaper, setNewspaper] = useState(null); const [showNewspaper, setShowNewspaper] = useState(false); // UI State const [showGuitar, setShowGuitar] = useState(false); const [guitarPlayed, setGuitarPlayed] = useState(false); const [selectedMember, setSelectedMember] = useState(null); const [selectedTab, setSelectedTab] = useState('overview'); const [scavengingRegion, setScavengingRegion] = useState('nearby'); const [texIdentity, setTexIdentity] = useState('tex'); const [isPaused, setIsPaused] = useState(false); // Timers const [nextVisitorTime, setNextVisitorTime] = useState(null); // Camera system const [activeCameraFeed, setActiveCameraFeed] = useState('entrance'); // Scavenger system const [scavengersDeployed, setScavengersDeployed] = useState(false); const [scavengerReturnTime, setScavengerReturnTime] = useState(null); // When they'll return const [deployedScavengers, setDeployedScavengers] = useState([]); // IDs of deployed scavengers // Dialogue system const [visitorDialogue, setVisitorDialogue] = useState([]); // Conversation history with current visitor const [memberDialogue, setMemberDialogue] = useState([]); // Conversation history with selected member const [showTalkPanel, setShowTalkPanel] = useState(false); // Show talk UI for visitor // Security & Incident system const [detectedIncidents, setDetectedIncidents] = useState([]); // Incidents caught by security const [activeAttack, setActiveAttack] = useState(null); // Current faction attack const [showSecurityReport, setShowSecurityReport] = useState(false); const [selectedIncident, setSelectedIncident] = useState(null); const [knownFactions, setKnownFactions] = useState([]); // Factions you've encountered const [horrorLevel, setHorrorLevel] = useState(0); // Increases over time, triggers events const [confinedMembers, setConfinedMembers] = useState([]); // Members in isolation { id, daysLeft } // Inner Circle Meeting system const [showMeeting, setShowMeeting] = useState(false); const [meetingTopic, setMeetingTopic] = useState(null); const [meetingDiscussion, setMeetingDiscussion] = useState([]); const [discussedTopics, setDiscussedTopics] = useState([]); // Topics discussed this meeting // Story Arc & Win Conditions const [storyPhase, setStoryPhase] = useState(0); // 0-5 story progression const [radioRepaired, setRadioRepaired] = useState(false); const [rescueCountdown, setRescueCountdown] = useState(null); // Days until rescue arrives const [storyEvents, setStoryEvents] = useState([]); // Triggered story beats const [endingTriggered, setEndingTriggered] = useState(null); // Which ending // NPC Relationships const [relationships, setRelationships] = useState({}); // { odor1_id2: value (-100 to 100) } const [romances, setRomances] = useState([]); // [{person1, person2, strength}] const [rivalries, setRivalries] = useState([]); // [{person1, person2, intensity}] const [cliques, setCliques] = useState([]); // [{name, memberIds, mood}] // Investigation & Surveillance const [watchList, setWatchList] = useState([]); // IDs being watched by security const [investigationProgress, setInvestigationProgress] = useState({}); // {memberId: progress 0-100} const [falseAccusations, setFalseAccusations] = useState([]); // Innocent people accused const [convertedMembers, setConvertedMembers] = useState([]); // People turned by cults // Moral Dilemmas const [activeDilemma, setActiveDilemma] = useState(null); const [dilemmaHistory, setDilemmaHistory] = useState([]); // Past choices and consequences const [karmaScore, setKarmaScore] = useState(0); // Tracks moral choices // Faction Diplomacy const [factionRelations, setFactionRelations] = useState({ raiders: -50, hollow: -30, changed: -60, remnant: -20, whisper: -80, feeders: -90 }); const [activeTrade, setActiveTrade] = useState(null); const [tributeDeals, setTributeDeals] = useState([]); // Ongoing protection payments const [factionAlliance, setFactionAlliance] = useState(null); // Allied faction const [showDiplomacy, setShowDiplomacy] = useState(false); // Expedition Events const [expeditionEvent, setExpeditionEvent] = useState(null); const [expeditionChoices, setExpeditionChoices] = useState([]); const [discoveredLocations, setDiscoveredLocations] = useState([]); const [rescuedSurvivors, setRescuedSurvivors] = useState([]); // People saved on expeditions // Sanity & Deep Horror const [sanityLevel, setSanityLevel] = useState(100); // Global bunker sanity const [possessedMember, setPossessedMember] = useState(null); // Whisper possession const [hallucinations, setHallucinations] = useState([]); // Active hallucination events const [missingMembers, setMissingMembers] = useState([]); // Disappeared people const [lastThingSnatchDay, setLastThingSnatchDay] = useState(-999); // Cooldown for "The Thing" disappearances const [theThingMember, setTheThingMember] = useState(null); // Secret monster among us // Bunker Upgrades const [bunkerRooms, setBunkerRooms] = useState({ armory: false, chapel: false, greenhouse: false, vault: false, lab: false }); const [defenseLevel, setDefenseLevel] = useState(0); const [bunkerIntegrity, setBunkerIntegrity] = useState(100); // Time-sensitive Events const [urgentEvents, setUrgentEvents] = useState([]); // [{type, daysLeft, data}] const [pendingAttack, setPendingAttack] = useState(null); // Announced attack countdown // ============ NEW MEGA EXPANSION STATE ============ // Weather System const [currentWeather, setCurrentWeather] = useState('clear'); const [weatherDuration, setWeatherDuration] = useState(0); const [weatherEffects, setWeatherEffects] = useState({}); // Day/Night Cycle const [timeOfDay, setTimeOfDay] = useState('day'); // day, dusk, night, dawn const [nightEvents, setNightEvents] = useState([]); // Dreams & Nightmares const [activeDream, setActiveDream] = useState(null); const [dreamHistory, setDreamHistory] = useState([]); // Radio System const [radioFrequency, setRadioFrequency] = useState(91.5); const [radioOn, setRadioOn] = useState(false); const [radioMessage, setRadioMessage] = useState(''); const [radioMessageIndex, setRadioMessageIndex] = useState(0); const [radioStatic, setRadioStatic] = useState(0); const [isBroadcasting, setIsBroadcasting] = useState(false); const [broadcastType, setBroadcastType] = useState(null); const [radioMusicPlaying, setRadioMusicPlaying] = useState(false); // Lore Documents const [discoveredLore, setDiscoveredLore] = useState([]); const [selectedLore, setSelectedLore] = useState(null); // Crafting System const [inventory, setInventory] = useState({ scrap: 10, chemicals: 5, electronics: 3, cloth: 8, herbs: 4 }); const [craftingQueue, setCraftingQueue] = useState([]); // Disease System const [activeDiseases, setActiveDiseases] = useState([]); // { memberId, diseaseId, progress, day } const [quarantinedMembers, setQuarantinedMembers] = useState([]); // Mutation System const [mutations, setMutations] = useState({}); // { memberId: [mutationIds] } const [radiationExposure, setRadiationExposure] = useState({}); // { memberId: level } // Vehicles const [vehicles, setVehicles] = useState([]); const [selectedVehicle, setSelectedVehicle] = useState(null); // Outposts const [outposts, setOutposts] = useState({}); // Children & Pregnancy const [pregnancies, setPregnancies] = useState([]); // { motherId, fatherId, daysLeft } const [children, setChildren] = useState([]); // Special child members // Pets const [pets, setPets] = useState([]); // Trials & Justice const [activeTrial, setActiveTrial] = useState(null); const [trialHistory, setTrialHistory] = useState([]); // Religion & Beliefs const [bunkerReligion, setBunkerReligion] = useState(null); const [faithLevel, setFaithLevel] = useState(0); const [rituals, setRituals] = useState([]); // Character Depth const [memberSecrets, setMemberSecrets] = useState({}); const [revealedSecrets, setRevealedSecrets] = useState([]); const [personalQuests, setPersonalQuests] = useState({}); const [mentalConditions, setMentalConditions] = useState({}); // Trading const [traderPresent, setTraderPresent] = useState(false); const [traderInventory, setTraderInventory] = useState([]); const [nextTraderDay, setNextTraderDay] = useState(7); // Map Exploration const [exploredMap, setExploredMap] = useState({}); const [mapMarkers, setMapMarkers] = useState([]); const [currentExpedition, setCurrentExpedition] = useState(null); const [selectedLocation, setSelectedLocation] = useState(null); const [scoutingTeam, setScoutingTeam] = useState(null); const [searchingRoom, setSearchingRoom] = useState(null); const [locationEvent, setLocationEvent] = useState(null); const [explorationLog, setExplorationLog] = useState([]); const [locationLootCollected, setLocationLootCollected] = useState({}); const [travelEvent, setTravelEvent] = useState(null); const [clearedLocations, setClearedLocations] = useState([]); const [showClearReward, setShowClearReward] = useState(null); // Achievements const [achievements, setAchievements] = useState([]); const [achievementPopup, setAchievementPopup] = useState(null); // Deep Horror const [watcherPresence, setWatcherPresence] = useState(0); const [mirrorEvents, setMirrorEvents] = useState([]); const [foundFootage, setFoundFootage] = useState([]); const [theDeepExplored, setTheDeepExplored] = useState(0); const [impostorSuspicion, setImpostorSuspicion] = useState({}); // Game Over const [gameOverReason, setGameOverReason] = useState(null); const [showHowToPlay, setShowHowToPlay] = useState(false); // ============ NEW FEATURES STATE ============ // Statistics Dashboard const [stats, setStats] = useState({ totalAdmitted: 0, totalExpelled: 0, totalDied: 0, totalFoodGathered: 0, totalWaterGathered: 0, totalAttacksRepelled: 0, totalAttacksSuffered: 0, peakPopulation: 0, moraleEventsHeld: 0, tradesCompleted: 0 }); // Speed Controls const [gameSpeed, setGameSpeed] = useState(1); // 1x, 2x, 3x /** easy | normal | hard — set on intro before first day */ const [bunkerDifficulty, setBunkerDifficulty] = useState('normal'); const diffParams = () => (BUNKER_DIFFICULTY[bunkerDifficulty] || BUNKER_DIFFICULTY.normal); const setDifficultyAndPersist = (id) => { if (!BUNKER_DIFFICULTY[id]) return; setBunkerDifficulty(id); try { localStorage.setItem('bunker_difficulty', id); } catch (e) {} }; useEffect(() => { try { const v = localStorage.getItem('bunker_difficulty'); if (v && BUNKER_DIFFICULTY[v]) setBunkerDifficulty(v); } catch (e) {} }, []); // Found Notes const [discoveredNotes, setDiscoveredNotes] = useState([]); const [showNoteModal, setShowNoteModal] = useState(null); // Member Confessions const [confessionsRevealed, setConfessionsRevealed] = useState({}); // {memberId: confessionId} const [showConfessionModal, setShowConfessionModal] = useState(null); // The Voice on the Radio const [voiceStage, setVoiceStage] = useState(0); const [voiceMessages, setVoiceMessages] = useState([]); const [showVoiceModal, setShowVoiceModal] = useState(null); const [voiceRelationship, setVoiceRelationship] = useState('unknown'); // unknown, trusting, suspicious, hostile // Memorial Wall const [memorialWall, setMemorialWall] = useState([]); // [{name, cause, day, portrait}] const [showMemorial, setShowMemorial] = useState(false); // Morale Events const [moraleEventCooldowns, setMoraleEventCooldowns] = useState({}); const [lastMoraleEvent, setLastMoraleEvent] = useState(null); // Rival Bunker const [rivalBunker, setRivalBunker] = useState({ discovered: false, attitude: 50, population: 15, lastContact: 0, alliance: false, betrayed: false, raidedUs: false, tradeHistory: [] }); const [showRivalBunker, setShowRivalBunker] = useState(false); // Active tab for new features const [showStats, setShowStats] = useState(false); // Audio refs const radioSynth = useRef(null); const noiseSynth = useRef(null); const guitarSynth = useRef(null); const voiceSynth = useRef(null); const gameLoopRef = useRef(null); const radioLoopRef = useRef(null); useEffect(() => { guitarSynth.current = createGuitarSynth(); voiceSynth.current = createVoiceSynth(); // Create radio synths radioSynth.current = new Tone.PolySynth(Tone.Synth, { oscillator: { type: 'sine' }, envelope: { attack: 0.1, decay: 0.3, sustain: 0.4, release: 0.8 } }).toDestination(); radioSynth.current.volume.value = -10; noiseSynth.current = new Tone.NoiseSynth({ noise: { type: 'brown' }, envelope: { attack: 0.1, decay: 0.2, sustain: 1, release: 0.5 } }).toDestination(); noiseSynth.current.volume.value = -20; return () => { if (guitarSynth.current) guitarSynth.current.dispose(); if (voiceSynth.current) voiceSynth.current.dispose(); if (radioSynth.current) radioSynth.current.dispose(); if (noiseSynth.current) noiseSynth.current.dispose(); }; }, []); // Radio music player const playRadioMusic = async () => { await Tone.start(); if (!radioSynth.current) return; const melodies = [ ['C4', 'E4', 'G4', 'E4', 'C4', 'D4', 'F4', 'D4'], ['A3', 'C4', 'E4', 'A4', 'G4', 'E4', 'C4', 'A3'], ['D4', 'F4', 'A4', 'D5', 'C5', 'A4', 'F4', 'D4'], ['E4', 'G4', 'B4', 'E5', 'D5', 'B4', 'G4', 'E4'] ]; const melody = melodies[Math.floor(Math.random() * melodies.length)]; const now = Tone.now(); melody.forEach((note, i) => { radioSynth.current.triggerAttackRelease(note, '4n', now + i * 0.3); }); }; const playRadioStatic = async () => { await Tone.start(); if (!noiseSynth.current) return; noiseSynth.current.triggerAttackRelease('8n'); }; // Get current radio station const getCurrentStation = () => { return radioStations.find(s => Math.abs(s.freq - radioFrequency) < 1.5) || null; }; // Get radio message for current station const getRadioMessage = () => { const station = getCurrentStation(); if (!station) return { text: '...static...', type: 'static' }; let message = station.messages[Math.floor(Math.random() * station.messages.length)]; message = message.replace('{day}', day); message = message.replace('{playerName}', 'Tex'); message = message.replace('{location}', 'Sector 7'); return { text: message, type: station.type, station: station.name }; }; // ============ MOOD UPDATE SYSTEM ============ const updateMemberMoods = () => { setBunkerMembers(prev => prev.map(member => { let moodChange = 0; const personality = member.personality; // Quality of life factors if (member.hunger < 30) moodChange -= 5; if (member.hunger > 70) moodChange += 2; if (member.thirst < 30) moodChange -= 5; if (member.thirst > 70) moodChange += 2; if (member.health < 50) moodChange -= 3; // Resource-based mood if (resources.food < 30) moodChange -= 2; if (resources.water < 30) moodChange -= 2; if (resources.power < 30) moodChange -= 1; // Personality-specific effects if (personality.id === 'social' && prev.length > 5) moodChange += 3; if (personality.id === 'social' && prev.length < 3) moodChange -= 3; if (personality.id === 'loner' && prev.length > 8) moodChange -= 3; if (personality.id === 'loner' && prev.length < 4) moodChange += 2; if (personality.id === 'optimist') moodChange += 1; if (personality.id === 'pessimist') moodChange -= 1; if (personality.id === 'hardworking' && member.job) moodChange += 1; if (personality.id === 'lazy' && member.job) moodChange -= 1; // Apply mood resistance moodChange *= (1 - personality.moodResistance * 0.5); // Tend toward base mood const baseMoodPull = (personality.baseMood - member.mood) * 0.05; const newMood = Math.max(5, Math.min(100, member.mood + moodChange + baseMoodPull)); return { ...member, mood: newMood }; })); }; // ============ REAL-TIME GAME LOOP ============ useEffect(() => { if (gameState !== 'playing' || isPaused || showNewspaper) return; gameLoopRef.current = setInterval(() => { setGameTime(prev => { const newTime = prev + 1; // Check if day should end (1 day = 180 seconds = 3 minutes) if (newTime >= 180) { endDay(); return 0; } return newTime; }); // Update visitor queue - decrease patience, remove dead/left setVisitorQueue(prev => { const updated = prev.map(v => ({ ...v, patience: v.patience - 1, waitTime: v.waitTime + 1 })); const remaining = []; updated.forEach(v => { if (v.patience <= 0) { // They either die or wander off const died = Math.random() < diffParams().visitorPatienceDeath; if (died) { setDailyEvents(e => [...e, `☠️ ${v.name} died waiting outside the bunker.`]); setRecentDeaths(d => [...d, { ...v, cause: 'exposure' }]); } else { setDailyEvents(e => [...e, `${v.name} gave up waiting and wandered off.`]); } } else { remaining.push(v); } }); return remaining; }); // Update bunker members - check needs, possible death setBunkerMembers(prev => { const updated = prev.map(m => { let newHunger = m.hunger - 0.06; let newThirst = m.thirst - 0.08; let newHealth = m.health; // Starving if (newHunger <= 0) { newHealth -= 2; newHunger = 0; } // Dehydrated if (newThirst <= 0) { newHealth -= 3; newThirst = 0; } return { ...m, hunger: newHunger, thirst: newThirst, health: Math.max(0, newHealth) }; }); // Check for deaths const alive = []; updated.forEach(m => { if (m.health <= 0) { setDailyEvents(e => [...e, `💀 ${m.name} has died in the bunker.`]); setRecentDeaths(d => [...d, { ...m, cause: m.hunger <= 0 ? 'starvation' : 'dehydration' }]); setInnerCircle(ic => ic.filter(id => id !== m.id)); } else { alive.push(m); } }); return alive; }); // Check if scavengers should return if (scavengersDeployed && scavengerReturnTime && gameTime >= scavengerReturnTime) { processScavengerReturn(); } // Spawn new visitor at random intervals setNextVisitorTime(prev => { if (prev === null) { // Set initial spawn time (15-45 seconds) return randomRange(15, 45); } if (prev <= 0) { // Spawn a visitor and reset timer const newVisitor = { ...generateSurvivor(), patience: randomRange(60, 120), // 1-2 minutes of patience waitTime: 0 }; setVisitorQueue(q => [...q, newVisitor]); setDailyEvents(e => [...e, `${newVisitor.name} has arrived at the bunker entrance.`]); return randomRange(20, 60); // Next visitor in 20-60 seconds } return prev - 1; }); // Update member activity animations and moods every 5 seconds if (gameTime % 5 === 0) { setBunkerMembers(prev => prev.map(m => ({ ...m, activityFrame: (m.activityFrame + 1) % 6 }))); updateMemberMoods(); } // Trigger mood events every 30 seconds if (gameTime % 30 === 0 && bunkerMembers.length > 0) { const member = randomFrom(bunkerMembers); const moodLevel = getMoodConsequence(member.mood, member.personality); let eventPool = []; if (moodLevel.level === 'critical') eventPool = moodEvents.critical || []; else if (moodLevel.level === 'low') eventPool = moodEvents.low || []; else if (moodLevel.level === 'good') eventPool = moodEvents.good || []; else if (moodLevel.level === 'excellent') eventPool = moodEvents.excellent || []; if (eventPool.length > 0 && Math.random() < diffParams().moodIntervalEvent) { const event = randomFrom(eventPool); const eventText = event.text.replace('{name}', member.name); setDailyEvents(e => [...e, eventText]); // Apply effects if (event.effect) { if (event.effect.morale) { setResources(r => ({ ...r, morale: Math.max(0, Math.min(100, r.morale + event.effect.morale)) })); } if (event.effect.food) { setResources(r => ({ ...r, food: Math.max(0, r.food + event.effect.food) })); } if (event.effect.power) { setResources(r => ({ ...r, power: Math.max(0, r.power + event.effect.power) })); } } } } }, Math.floor(1000 / gameSpeed)); // Tick rate adjusted by game speed return () => { if (gameLoopRef.current) clearInterval(gameLoopRef.current); }; }, [gameState, isPaused, showNewspaper, gameSpeed, bunkerDifficulty]); // Auto-select next visitor from queue when current is dealt with useEffect(() => { if (!currentVisitor && visitorQueue.length > 0 && gameState === 'playing') { // Take first in line const next = visitorQueue[0]; setCurrentVisitor(next); setVisitorQueue(prev => prev.slice(1)); setGuitarPlayed(false); setShowGuitar(false); } }, [currentVisitor, visitorQueue, gameState]); // ============ FEEDING SYSTEM ============ const distributeRations = () => { if (bunkerMembers.length === 0) return; const foodPerPerson = Math.min(10, resources.food / bunkerMembers.length); const waterPerPerson = Math.min(10, resources.water / bunkerMembers.length); const foodUsed = foodPerPerson * bunkerMembers.length; const waterUsed = waterPerPerson * bunkerMembers.length; setResources(prev => ({ ...prev, food: Math.max(0, prev.food - foodUsed), water: Math.max(0, prev.water - waterUsed) })); setBunkerMembers(prev => prev.map(m => ({ ...m, hunger: Math.min(100, m.hunger + foodPerPerson * 3), thirst: Math.min(100, m.thirst + waterPerPerson * 3) }))); setDailyEvents(e => [...e, `Rations distributed: ${foodPerPerson.toFixed(1)} food, ${waterPerPerson.toFixed(1)} water per person.`]); }; // ============ SCAVENGER DEPLOYMENT ============ const deployScavengers = () => { const scavengers = bunkerMembers.filter(m => m.job === 'scavenger'); if (scavengers.length === 0) { setDailyEvents(e => [...e, '⚠️ No scavengers assigned! Assign workers to the Scavenger job first.']); return; } if (scavengersDeployed) { setDailyEvents(e => [...e, '⚠️ Scavengers are already out! Wait for them to return.']); return; } const region = scavengingRegions.find(r => r.id === scavengingRegion); setScavengersDeployed(true); setDeployedScavengers(scavengers.map(s => s.id)); // Return time: 30-60 seconds depending on region distance const returnDelay = 30 + scavengingRegions.findIndex(r => r.id === scavengingRegion) * 5; setScavengerReturnTime(gameTime + returnDelay); setDailyEvents(e => [...e, `🎒 ${scavengers.length} scavenger(s) deployed to ${region.name}. ETA: ${returnDelay}s`]); }; const processScavengerReturn = () => { const D = diffParams(); const region = scavengingRegions.find(r => r.id === scavengingRegion); const scavengers = bunkerMembers.filter(m => deployedScavengers.includes(m.id)); let totalFood = 0; let totalWater = 0; const survivors = []; const casualties = []; scavengers.forEach(scav => { // Death risk based on region danger and skill (halved for balance) const deathRisk = region.danger * 0.5 * D.scavDeathDangerMult * (1 - (scav.skills.scavenging * 0.15)); // Cowards have higher flee/death chance, brave have bonus const personalityMod = scav.personality?.survivalBonus || -(scav.personality?.survivalPenalty || 0); const finalDeathRisk = Math.max(0.01, deathRisk - personalityMod); if (Math.random() < finalDeathRisk) { casualties.push(scav); } else { survivors.push(scav); // Calculate loot const skillBonus = scav.skills.scavenging * 0.2; totalFood += Math.floor(randomRange(region.foodYield[0], region.foodYield[1]) * (1 + skillBonus)); totalWater += Math.floor(randomRange(region.waterYield[0], region.waterYield[1]) * (1 + skillBonus)); // Skill improvement setBunkerMembers(prev => prev.map(m => m.id === scav.id ? { ...m, skills: { ...m.skills, scavenging: Math.min(5, m.skills.scavenging + 0.1) }} : m )); } }); // Process casualties casualties.forEach(casualty => { setDailyEvents(e => [...e, `💀 ${casualty.name} was lost in ${region.name}. They will be remembered.`]); setRecentDeaths(d => [...d, { ...casualty, cause: 'scavenging' }]); setBunkerMembers(prev => prev.filter(m => m.id !== casualty.id)); setInnerCircle(prev => prev.filter(id => id !== casualty.id)); setResources(prev => ({ ...prev, morale: Math.max(0, prev.morale - 5) })); }); // Add resources if (totalFood > 0 || totalWater > 0) { setResources(prev => ({ ...prev, food: Math.min(200, prev.food + totalFood), water: Math.min(200, prev.water + totalWater) })); setDailyEvents(e => [...e, `📦 Scavengers returned with ${totalFood} food and ${totalWater} water!`]); } if (survivors.length > 0 && casualties.length === 0) { setDailyEvents(e => [...e, `✓ All ${survivors.length} scavenger(s) returned safely from ${region.name}.`]); } else if (survivors.length > 0) { setDailyEvents(e => [...e, `⚠️ ${survivors.length} of ${scavengers.length} scavengers made it back.`]); } else if (casualties.length > 0) { setDailyEvents(e => [...e, `☠️ No scavengers returned from ${region.name}...`]); } // EXPEDITION EVENT - Chance of encounter based on region danger if (survivors.length > 0 && Math.random() < region.danger * 1.5 * D.expeditionEventMult) { const event = randomFrom(expeditionEvents); if (event) { setExpeditionEvent(event); } } // Reset deployment state setScavengersDeployed(false); setDeployedScavengers([]); setScavengerReturnTime(null); }; // ============ GAME ACTIONS ============ const playTexSong = async () => { await Tone.start(); setGuitarPlayed(true); const melody = ['E4', 'G4', 'A4', 'G4', 'E4', 'D4', 'E4']; melody.forEach((note, i) => { setTimeout(() => { if (guitarSynth.current) guitarSynth.current.triggerAttackRelease(note, '8n'); }, i * 200); }); setTimeout(() => { const vocals = ['C4', 'E4', 'G4', 'E4', 'C4', 'G4', 'C5']; vocals.forEach((note, i) => { setTimeout(() => { if (voiceSynth.current) voiceSynth.current.triggerAttackRelease(note, '16n'); }, i * 150); }); }, 1400); if (currentVisitor) { setTimeout(() => { setCurrentVisitor(prev => prev ? { ...prev, revealed: true } : null); }, 2000); } }; const admitVisitor = () => { if (currentVisitor) { // Loyalty boost if talked to as Tex (friendly approach) const loyaltyBonus = texIdentity === 'tex' ? 10 : 0; // Talked to them = they feel heard const talkedBonus = visitorDialogue.length > 0 ? 5 : 0; const newMember = { ...currentVisitor, revealed: false, patience: undefined, waitTime: undefined, hunger: Math.max(20, currentVisitor.hunger - (currentVisitor.waitTime || 0) * 0.3), thirst: Math.max(20, currentVisitor.thirst - (currentVisitor.waitTime || 0) * 0.4), loyalty: Math.min(100, currentVisitor.loyalty + loyaltyBonus + talkedBonus), mood: Math.min(100, currentVisitor.mood + (texIdentity === 'tex' ? 10 : 0)) }; setBunkerMembers(prev => [...prev, newMember]); setStats(prev => ({ ...prev, totalAdmitted: prev.totalAdmitted + 1 })); setDailyEvents(prev => [...prev, `✓ ${currentVisitor.name} was admitted to the bunker.`]); setCurrentVisitor(null); setGuitarPlayed(false); setShowGuitar(false); setShowTalkPanel(false); setVisitorDialogue([]); } }; const rejectVisitor = () => { if (currentVisitor) { setDailyEvents(prev => [...prev, `✗ ${currentVisitor.name} was turned away.`]); setCurrentVisitor(null); setGuitarPlayed(false); setShowGuitar(false); setShowTalkPanel(false); setVisitorDialogue([]); } }; const assignJob = (memberId, jobId) => { const job = jobDefinitions[jobId]; const member = bunkerMembers.find(m => m.id === memberId); if (!member) return; // Check if already has this job if (member.job === jobId) { setDailyEvents(prev => [...prev, `${member.name} is already assigned to ${job.name}.`]); return; } // Check if job slots are full const currentWorkers = bunkerMembers.filter(m => m.job === jobId).length; if (currentWorkers >= job.maxSlots) { setDailyEvents(prev => [...prev, `Cannot assign more workers to ${job.name}. All ${job.maxSlots} slots filled.`]); return; } // If member has existing job, notify of reassignment if (member.job) { const oldJob = jobDefinitions[member.job]; setDailyEvents(prev => [...prev, `${member.name} reassigned from ${oldJob.name} to ${job.name}.`]); } else { setDailyEvents(prev => [...prev, `${member.name} assigned to ${job.name}.`]); } setBunkerMembers(prev => prev.map(m => m.id === memberId ? { ...m, job: jobId } : m )); }; const removeFromJob = (memberId) => { const member = bunkerMembers.find(m => m.id === memberId); if (member && member.job) { setDailyEvents(prev => [...prev, `${member.name} removed from ${jobDefinitions[member.job].name}.`]); } setBunkerMembers(prev => prev.map(m => m.id === memberId ? { ...m, job: null } : m )); }; const addToInnerCircle = (memberId) => { if (innerCircle.length >= 3) { setDailyEvents(prev => [...prev, "Inner circle is full. Remove someone first."]); return; } setInnerCircle(prev => [...prev, memberId]); setBunkerMembers(prev => prev.map(m => m.id === memberId ? { ...m, isInnerCircle: true } : m )); }; const removeFromInnerCircle = (memberId) => { setInnerCircle(prev => prev.filter(id => id !== memberId)); setBunkerMembers(prev => prev.map(m => m.id === memberId ? { ...m, isInnerCircle: false } : m )); }; const expelMember = (memberId) => { const member = bunkerMembers.find(m => m.id === memberId); if (member) { setBunkerMembers(prev => prev.filter(m => m.id !== memberId)); setInnerCircle(prev => prev.filter(id => id !== memberId)); setStats(prev => ({ ...prev, totalExpelled: prev.totalExpelled + 1 })); setDailyEvents(prev => [...prev, `${member.name} was expelled from the bunker.`]); if (!member.isThreat) { setResources(prev => ({ ...prev, morale: Math.max(0, prev.morale - 10) })); } } }; // ============ INCIDENT & PUNISHMENT SYSTEM ============ // -- Exploration System --------------------------------- const sendScoutTeam = (locationId) => { const loc = mapLocations.find(l => l.id === locationId); if (!loc) return; const available = bunkerMembers.filter(m => !m.isConfined && !deployedScavengers.includes(m.id) && m.job !== 'security'); if (available.length < 2) { setDailyEvents(e => [...e, '⚠️ Not enough available members to send a scouting team (need 2).']); return; } const team = available .sort((a, b) => (b.skills.security + (b.skills.survival || 0)) - (a.skills.security + (a.skills.survival || 0))) .slice(0, Math.min(3, available.length)); const distance = Math.sqrt(Math.pow(loc.x - 50, 2) + Math.pow(loc.y - 50, 2)); const travelDays = Math.max(1, Math.floor(distance / 25)); setScoutingTeam({ locationId, members: team.map(m => m.id), memberNames: team.map(m => m.name), departDay: day, returnDay: day + travelDays, status: 'traveling', travelDays }); setDailyEvents(e => [...e, `🗺️ Scouting team deployed to ${loc.name}: ${team.map(m => m.name).join(', ')}. ETA: ${travelDays} day(s).`]); }; const searchRoom = (locationId, roomId) => { const loc = mapLocations.find(l => l.id === locationId); if (!loc) return; const room = loc.rooms.find(r => r.id === roomId); if (!room) return; if (locationLootCollected[locationId]?.[roomId]) { setDailyEvents(e => [...e, `📍 ${room.name} has already been searched.`]); return; } const possibleEvents = locationEvents.filter(ev => (ev.trigger === 'any' || ev.trigger === loc.type) && Math.random() < ev.chance ); if (possibleEvents.length > 0) { setLocationEvent({ ...possibleEvents[0], locationId, roomId }); return; } completeRoomSearch(locationId, roomId); }; const completeRoomSearch = (locationId, roomId, lootMultiplier = 1) => { const loc = mapLocations.find(l => l.id === locationId); if (!loc) return; const room = loc.rooms.find(r => r.id === roomId); if (!room) return; const dangerRoll = Math.random(); const survived = dangerRoll > room.danger; if (!survived && scoutingTeam && scoutingTeam.members.length > 0 && bunkerMembers.length > 3) { const unlucky = randomFrom(scoutingTeam.members); const member = bunkerMembers.find(m => m.id === unlucky); if (member) { setBunkerMembers(prev => prev.filter(m => m.id !== unlucky)); setMemorialWall(prev => [...prev, { name: member.name, cause: 'exploration', day, appearance: member.appearance }]); setDailyEvents(e => [...e, `💀 ${member.name} was killed searching ${room.name} at ${loc.name}.`]); setExplorationLog(prev => [...prev, { day, location: loc.name, room: room.name, result: 'casualty', member: member.name }]); setScoutingTeam(prev => prev ? { ...prev, members: prev.members.filter(id => id !== unlucky) } : null); } } const loot = {}; Object.entries(room.loot).forEach(([key, val]) => { if (key === 'survivors') { const count = typeof val === 'number' ? val : 1; for (let i = 0; i < count; i++) { const newPerson = generateSurvivor(); setBunkerMembers(prev => [...prev, { ...newPerson, daysInBunker: 0 }]); } setDailyEvents(e => [...e, `👥 Found ${count} survivor(s) at ${room.name}!`]); loot.survivors = count; } else if (key === 'lore') { setDiscoveredLore(prev => [...prev, { source: `${loc.name} - ${room.name}`, text: room.flavor, day }]); loot.lore = val; } else { const amount = Math.floor(val * lootMultiplier * (0.7 + Math.random() * 0.6)); loot[key] = amount; } }); setResources(prev => ({ ...prev, food: Math.min(200, prev.food + (loot.food || 0)), water: Math.min(200, prev.water + (loot.water || 0)), power: Math.min(100, prev.power + (loot.electronics || 0)), })); if (loot.medicine) setResources(prev => ({ ...prev, food: Math.min(200, prev.food + Math.floor(loot.medicine / 2)) })); if (loot.weapons) setDefenseLevel(d => d + loot.weapons); if (loot.scrap) setBunkerIntegrity(i => Math.min(100, i + loot.scrap)); setLocationLootCollected(prev => ({ ...prev, [locationId]: { ...(prev[locationId] || {}), [roomId]: loot } })); const lootSummary = Object.entries(loot).filter(([k, v]) => v > 0 && k !== 'survivors' && k !== 'lore').map(([k, v]) => `${v} ${k}`).join(', '); setDailyEvents(e => [...e, `🔍 Searched ${room.name}: ${lootSummary || 'nothing useful'}`]); setExplorationLog(prev => [...prev, { day, location: loc.name, room: room.name, result: 'searched', loot: lootSummary }]); if (room.danger > 0.5) setSanityLevel(s => Math.max(0, s - Math.floor(room.danger * 8))); }; const handleLocationEventChoice = (choiceIndex) => { if (!locationEvent) return; const choice = locationEvent.choices[choiceIndex]; const effect = choice.effect || {}; if (effect.food) setResources(r => ({ ...r, food: Math.min(200, r.food + effect.food) })); if (effect.water) setResources(r => ({ ...r, water: Math.min(200, r.water + effect.water) })); if (effect.weapons) setDefenseLevel(d => d + effect.weapons); if (effect.karma) setKarmaScore(k => k + effect.karma); if (effect.sanity) setSanityLevel(s => Math.max(0, Math.min(100, s + effect.sanity))); if (effect.lore) setDiscoveredLore(prev => [...prev, { source: 'Field Discovery', text: locationEvent.text, day }]); if (effect.defense) setDefenseLevel(d => d + effect.defense); if (effect.survivors) { for (let i = 0; i < effect.survivors; i++) { const p = generateSurvivor(); setBunkerMembers(prev => [...prev, { ...p, daysInBunker: 0 }]); } setDailyEvents(e => [...e, `👥 ${effect.survivors} survivor(s) rescued!`]); } if (effect.lootBonus) { completeRoomSearch(locationEvent.locationId, locationEvent.roomId, effect.lootBonus); } else if (!effect.abort) { completeRoomSearch(locationEvent.locationId, locationEvent.roomId); } if (effect.abort) setDailyEvents(e => [...e, '🚫 Team fell back. Room not searched.']); if (effect.combatRisk && Math.random() < 0.3 && scoutingTeam && bunkerMembers.length > 3) { const unlucky = randomFrom(scoutingTeam.members); const member = bunkerMembers.find(m => m.id === unlucky); if (member) { setBunkerMembers(prev => prev.filter(m => m.id !== unlucky)); setMemorialWall(prev => [...prev, { name: member.name, cause: 'exploration', day, appearance: member.appearance }]); setDailyEvents(e => [...e, `💀 ${member.name} was killed in combat!`]); } } setLocationEvent(null); }; const handleTravelEventChoice = (choiceIndex) => { if (!travelEvent) return; const choice = travelEvent.choices[choiceIndex]; const effect = choice.effect || {}; if (effect.delay && scoutingTeam) setScoutingTeam(prev => prev ? { ...prev, returnDay: prev.returnDay + effect.delay } : null); if (effect.food) setResources(r => ({ ...r, food: Math.max(0, Math.min(200, r.food + effect.food)) })); if (effect.water) setResources(r => ({ ...r, water: Math.max(0, Math.min(200, r.water + effect.water)) })); if (effect.weapons) setDefenseLevel(d => Math.max(0, d + effect.weapons)); if (effect.sanity) setSanityLevel(s => Math.max(0, Math.min(100, s + effect.sanity))); if (effect.lore) setDiscoveredLore(prev => [...prev, { source: 'Travel Encounter', text: travelEvent.text, day }]); if (effect.karma) setKarmaScore(k => k + effect.karma); if (effect.danger && Math.random() < effect.danger && scoutingTeam && scoutingTeam.members.length > 1) { const unlucky = randomFrom(scoutingTeam.members); const member = bunkerMembers.find(m => m.id === unlucky); if (member) { setBunkerMembers(prev => prev.filter(m => m.id !== unlucky)); setMemorialWall(prev => [...prev, { name: member.name, cause: 'exploration', day, appearance: member.appearance }]); setScoutingTeam(prev => prev ? { ...prev, members: prev.members.filter(id => id !== unlucky) } : null); setDailyEvents(e => [...e, `💀 ${member.name} was lost during travel.`]); } } if (effect.discoveryChance && Math.random() < effect.discoveryChance) { const undiscovered = mapLocations.filter(l => !exploredMap[l.id]); if (undiscovered.length > 0) { const found = randomFrom(undiscovered); setExploredMap(prev => ({ ...prev, [found.id]: { discovered: true, day } })); setDailyEvents(e => [...e, `🗺️ Team spotted ${found.name} during travel!`]); } } if (effect.petChance && Math.random() < effect.petChance) { const petType = randomFrom(petTypes); setPets(prev => [...prev, { type: petType.id, name: petType.name + ' ' + (prev.length + 1) }]); setDailyEvents(e => [...e, `🐾 A ${petType.name} joined the team!`]); } if (effect.survivorChance && Math.random() < effect.survivorChance) { const p = generateSurvivor(); setBunkerMembers(prev => [...prev, { ...p, daysInBunker: 0 }]); setDailyEvents(e => [...e, `👥 Found a survivor at the camp!`]); } setTravelEvent(null); }; const buildOutpost = (locationId, outpostId) => { const outpost = outpostTypes.find(o => o.id === outpostId); if (!outpost) return; if (outposts[locationId]) { setDailyEvents(e => [...e, '⚠️ This location already has an outpost.']); return; } if (outpost.cost.food && resources.food < outpost.cost.food) { setDailyEvents(e => [...e, '⚠️ Not enough food.']); return; } if (outpost.cost.scrap && bunkerIntegrity < outpost.cost.scrap) { setDailyEvents(e => [...e, '⚠️ Not enough scrap/integrity.']); return; } if (outpost.cost.medicine && resources.food < outpost.cost.medicine) { setDailyEvents(e => [...e, '⚠️ Not enough medicine.']); return; } if (outpost.cost.electronics && resources.power < outpost.cost.electronics) { setDailyEvents(e => [...e, '⚠️ Not enough electronics.']); return; } if (outpost.cost.weapons && defenseLevel < outpost.cost.weapons) { setDailyEvents(e => [...e, '⚠️ Not enough weapons.']); return; } if (outpost.cost.food) setResources(r => ({ ...r, food: r.food - outpost.cost.food })); if (outpost.cost.scrap) setBunkerIntegrity(i => i - outpost.cost.scrap); if (outpost.cost.electronics) setResources(r => ({ ...r, power: r.power - outpost.cost.electronics })); if (outpost.cost.weapons) setDefenseLevel(d => d - outpost.cost.weapons); if (outpost.cost.medicine) setResources(r => ({ ...r, food: r.food - outpost.cost.medicine })); setOutposts(prev => ({ ...prev, [locationId]: outpost })); setDailyEvents(e => [...e, `🏗️ Built ${outpost.name} at ${mapLocations.find(l => l.id === locationId)?.name || locationId}.`]); }; const claimClearingReward = (locationId) => { const reward = clearingRewards[locationId]; if (!reward || clearedLocations.includes(locationId)) return; const effect = reward.effect; if (effect.food) setResources(r => ({ ...r, food: Math.min(200, r.food + effect.food) })); if (effect.water) setResources(r => ({ ...r, water: Math.min(200, r.water + effect.water) })); if (effect.scrap) setBunkerIntegrity(i => Math.min(100, i + effect.scrap)); if (effect.morale) setResources(r => ({ ...r, morale: Math.min(100, r.morale + effect.morale) })); if (effect.sanity) setSanityLevel(s => Math.max(0, Math.min(100, s + effect.sanity))); if (effect.lore) { for (let i = 0; i < effect.lore; i++) { setDiscoveredLore(prev => [...prev, { source: reward.name, text: reward.desc, day }]); } } if (effect.medicine) setResources(r => ({ ...r, food: Math.min(200, r.food + effect.medicine) })); if (effect.karma) setKarmaScore(k => k + effect.karma); if (effect.storyAdvance) setStoryPhase(p => Math.min(5, p + 1)); setClearedLocations(prev => [...prev, locationId]); setShowClearReward(reward); setDailyEvents(e => [...e, `⭐ LOCATION CLEARED: ${reward.name}`]); }; // Morale Event Handler const triggerMoraleEvent = (eventId) => { const event = moraleEventOptions.find(e => e.id === eventId); if (!event) return; if (moraleEventCooldowns[eventId]) return; // Check costs if (event.cost.food && resources.food < event.cost.food) return; if (event.cost.power && resources.power < event.cost.power) return; // Apply costs const newRes = { ...resources }; if (event.cost.food) newRes.food -= event.cost.food; if (event.cost.power) newRes.power -= event.cost.power; newRes.morale = Math.min(100, newRes.morale + event.boost); setResources(newRes); setMoraleEventCooldowns(prev => ({ ...prev, [eventId]: event.cooldown })); setLastMoraleEvent(event); setStats(prev => ({ ...prev, moraleEventsHeld: prev.moraleEventsHeld + 1 })); setDailyEvents(prev => [...prev, `${event.name} - Morale +${event.boost}!`]); }; // Rival Bunker Action Handler const handleRivalAction = (actionId) => { const action = rivalActions.find(a => a.id === actionId); if (!action) return; if (rivalBunker.attitude < action.attitudeReq && action.attitudeReq > 0) return; // Check costs if (action.cost.food && resources.food < action.cost.food) return; if (action.cost.water && resources.water < action.cost.water) return; // Apply costs const newRes = { ...resources }; if (action.cost.food) newRes.food -= action.cost.food; if (action.cost.water) newRes.water -= action.cost.water; // Apply rewards if (action.gives.food) newRes.food = Math.min(200, newRes.food + action.gives.food); if (action.gives.water) newRes.water = Math.min(200, newRes.water + action.gives.water); if (action.gives.defense) setDefenseLevel(d => d + action.gives.defense); if (action.gives.morale) newRes.morale = Math.min(100, newRes.morale + action.gives.morale); setResources(newRes); // Update attitude setRivalBunker(prev => ({ ...prev, attitude: Math.max(0, Math.min(100, prev.attitude + action.attitudeChange)), alliance: action.id === 'alliance' ? true : prev.alliance, lastContact: day, tradeHistory: [...prev.tradeHistory, { action: actionId, day }] })); setStats(prev => ({ ...prev, tradesCompleted: prev.tradesCompleted + 1 })); // Special messages if (action.id === 'raid_them') { setDailyEvents(prev => [...prev, `🗡️ You raided ${rivalBunkerData.name}! They won't forget this.`]); } else if (action.id === 'alliance') { setDailyEvents(prev => [...prev, `🤝 Alliance formed with ${rivalBunkerData.name}! Mutual defense pact active.`]); } else { setDailyEvents(prev => [...prev, `📦 Completed action with ${rivalBunkerData.name}: ${action.name}`]); } }; const handlePunishment = (incident, punishmentId) => { const punishment = punishmentOptions.find(p => p.id === punishmentId); const perpetrator = bunkerMembers.find(m => m.id === incident.perpetratorId); if (!punishment || !perpetrator) { setDetectedIncidents(prev => prev.filter(i => i !== incident)); setSelectedIncident(null); return; } // Apply punishment if (punishment.removes) { setBunkerMembers(prev => prev.filter(m => m.id !== perpetrator.id)); setInnerCircle(prev => prev.filter(id => id !== perpetrator.id)); if (punishment.kills) { setRecentDeaths(prev => [...prev, { ...perpetrator, cause: 'executed' }]); setDailyEvents(prev => [...prev, `💀 ${perpetrator.name} was executed for ${incident.name}.`]); // Executing people hurts morale unless they were clearly evil if (!perpetrator.isThreat) { setResources(prev => ({ ...prev, morale: Math.max(0, prev.morale - 15) })); } } else { setDailyEvents(prev => [...prev, `🚪 ${perpetrator.name} was exiled for ${incident.name}.`]); } } else if (punishment.daysConfined) { setConfinedMembers(prev => [...prev, { id: perpetrator.id, daysLeft: punishment.daysConfined }]); setBunkerMembers(prev => prev.map(m => m.id === perpetrator.id ? { ...m, isConfined: true, job: null } : m )); setDailyEvents(prev => [...prev, `🔒 ${perpetrator.name} confined for ${punishment.daysConfined} day(s).`]); } else { // Apply loyalty/mood effects if (punishment.effect) { setBunkerMembers(prev => prev.map(m => { if (m.id !== perpetrator.id) return m; return { ...m, loyalty: Math.max(0, Math.min(100, m.loyalty + (punishment.effect.loyalty || 0))), mood: Math.max(0, Math.min(100, m.mood + (punishment.effect.mood || 0))) }; })); } setDailyEvents(prev => [...prev, `⚖️ ${perpetrator.name} received: ${punishment.name} for ${incident.name}.`]); } // Global morale effect from punishment if (punishment.effect?.morale && punishment.removes) { setResources(prev => ({ ...prev, morale: Math.max(0, prev.morale + punishment.effect.morale) })); } // Remove incident from queue setDetectedIncidents(prev => prev.filter(i => i !== incident)); setSelectedIncident(null); // If was last incident, close security report if (detectedIncidents.length <= 1) { setShowSecurityReport(false); } }; const getInnerCircleAdviceOnIncident = (incident) => { const advice = []; innerCircle.forEach(memberId => { const advisor = bunkerMembers.find(m => m.id === memberId); if (!advisor) return; const perpetrator = bunkerMembers.find(m => m.id === incident.perpetratorId); const personality = advisor.personality; let recommendation, sentiment; // Personality-based recommendations if (personality?.id === 'aggressive' || personality?.id === 'paranoid') { recommendation = incident.severity >= 4 ? 'Execute them. Make an example.' : 'Harsh punishment. They need to learn.'; sentiment = 'harsh'; } else if (personality?.id === 'generous' || personality?.id === 'nurturing') { recommendation = 'Show mercy. Everyone deserves a second chance.'; sentiment = 'merciful'; } else if (personality?.id === 'calm' || personality?.id === 'stoic') { recommendation = incident.severity >= 4 ? 'Exile. Fair but firm.' : 'Confinement seems appropriate.'; sentiment = 'neutral'; } else if (advisor.isThreat && perpetrator?.threatInfo?.faction?.id === advisor.threatInfo?.faction?.id) { // Fellow faction member - try to protect them recommendation = 'I think there\'s been a misunderstanding. Let them go.'; sentiment = 'suspicious'; } else { recommendation = randomFrom([ 'Your call, boss.', incident.severity >= 3 ? 'This is serious. Act accordingly.' : 'A warning might suffice.', 'Whatever keeps the bunker safe.' ]); sentiment = 'neutral'; } advice.push({ advisor: advisor.name, text: recommendation, sentiment }); }); return advice; }; const addressProblem = (problemId) => { const problem = activeProblems.find(p => p.id === problemId); if (!problem) return; const workers = bunkerMembers.filter(m => m.job === problem.requiredJob); if (workers.length === 0) { setDailyEvents(prev => [...prev, `No ${jobDefinitions[problem.requiredJob]?.name || 'workers'} available to address: ${problem.name}`]); return; } // Best worker attempts to fix const bestWorker = workers.reduce((best, w) => (w.skills[jobDefinitions[problem.requiredJob].skill] > (best?.skills[jobDefinitions[problem.requiredJob].skill] || 0)) ? w : best , null); const skill = bestWorker.skills[jobDefinitions[problem.requiredJob].skill]; const successChance = 0.3 + (skill * 0.15); if (Math.random() < successChance) { setActiveProblems(prev => prev.filter(p => p.id !== problemId)); setDailyEvents(prev => [...prev, `${bestWorker.name} fixed: ${problem.name}`]); // Skill improvement setBunkerMembers(prev => prev.map(m => m.id === bestWorker.id ? { ...m, skills: { ...m.skills, [jobDefinitions[problem.requiredJob].skill]: Math.min(5, m.skills[jobDefinitions[problem.requiredJob].skill] + 0.1) }} : m )); } else { setDailyEvents(prev => [...prev, `${bestWorker.name} attempted to fix ${problem.name} but failed.`]); } }; const getInnerCircleAdvice = (memberId) => { const advisors = bunkerMembers.filter(m => innerCircle.includes(m.id)); const target = bunkerMembers.find(m => m.id === memberId) || currentVisitor; if (!target || advisors.length === 0) return []; return advisors.map(advisor => { let opinion; const roll = Math.random(); // Advisors with agendas might lie if (advisor.hasAgenda && roll < 0.4) { // Lie based on agenda if (target.isThreat) { opinion = { sentiment: 'positive', text: `"I think ${target.name} would be a great addition."` }; } else { opinion = { sentiment: 'negative', text: `"Something feels off about ${target.name}. Be careful."` }; } } else { // Honest assessment (somewhat accurate based on their perception skill) const perception = advisor.skills.security + advisor.skills.community; const accurate = roll < (perception / 10); if (accurate) { if (target.isThreat) { opinion = { sentiment: 'negative', text: `"I don't trust ${target.name}. Watch them closely."` }; } else { opinion = { sentiment: 'positive', text: `"${target.name} seems genuine to me."` }; } } else { // Uncertain opinion = { sentiment: 'neutral', text: `"Hard to say about ${target.name}. Could go either way."` }; } } return { advisor: advisor.name, ...opinion }; }); }; // ============ END DAY PROCESSING ============ const endDay = () => { const D = diffParams(); let events = [...dailyEvents]; let dayDeaths = [...recentDeaths]; let newResources = { ...resources }; // Pause the game during day transition setIsPaused(true); // 0. AUTOMATIC FEEDING - Feed everyone at day end if (bunkerMembers.length > 0 && newResources.food > 0 && newResources.water > 0) { const foodPerPerson = Math.min(15, newResources.food / bunkerMembers.length); const waterPerPerson = Math.min(15, newResources.water / bunkerMembers.length); const foodUsed = Math.floor(foodPerPerson * bunkerMembers.length * 0.5); const waterUsed = Math.floor(waterPerPerson * bunkerMembers.length * 0.5); newResources.food = Math.max(0, newResources.food - foodUsed); newResources.water = Math.max(0, newResources.water - waterUsed); setBunkerMembers(prev => prev.map(m => ({ ...m, hunger: Math.min(100, m.hunger + foodPerPerson * 2), thirst: Math.min(100, m.thirst + waterPerPerson * 2) }))); events.push(`🍽️ Daily rations: ${foodUsed} food, ${waterUsed} water consumed.`); } // 1. Food decay newResources.food = Math.max(0, newResources.food - newResources.foodDecayRate * D.foodDecayMult); // 2. Water decay newResources.water = Math.max(0, newResources.water - newResources.waterDecayRate * D.waterDecayMult); // 3. Base power consumption newResources.power = Math.max(0, newResources.power - 3 * D.powerDrainMult); // 4. Process workers by job const jobCounts = {}; Object.keys(jobDefinitions).forEach(job => { jobCounts[job] = bunkerMembers.filter(m => m.job === job); }); // If scavengers are still deployed at day end, force their return if (scavengersDeployed) { events.push('📦 Scavengers returned as night fell.'); // processScavengerReturn will handle loot and deaths processScavengerReturn(); } // Engineers - maintain power const engBonus = jobCounts.engineer.reduce((sum, e) => sum + e.skills.engineering * 3, 0); newResources.power = Math.min(100, newResources.power + engBonus); // Farmers - grow food const farmBonus = jobCounts.farmer.reduce((sum, f) => sum + f.skills.farming * 2, 0); newResources.food = Math.min(200, newResources.food + Math.floor(farmBonus)); // Cooks - reduce food decay const cookBonus = jobCounts.cook.reduce((sum, c) => sum + c.skills.cooking * 0.3, 0); newResources.foodDecayRate = Math.max(0.5, 2 - cookBonus); // Community organizers - boost morale const comBonus = jobCounts.community.reduce((sum, c) => sum + c.skills.community * 3, 0); newResources.morale = Math.min(100, newResources.morale + comBonus); // Security - detect threats and incidents const securityLevel = jobCounts.security.reduce((sum, s) => sum + s.skills.security, 0); const detectionChance = Math.min(0.8, 0.1 + securityLevel * 0.1); // 10% base + 10% per security skill point // 6. Process threats and generate incidents const newIncidents = []; bunkerMembers.filter(m => m.isThreat && m.threatInfo && !m.isConfined).forEach(threat => { // Countdown to action threat.threatInfo.actionsUntilStrike--; if (threat.threatInfo.actionsUntilStrike <= 0 && !threat.threatInfo.hasActed) { // Time to act! const action = threat.threatInfo.threatType.action; let incident = null; switch(action) { case 'sabotage': incident = randomFrom(incidentTypes.filter(i => i.id.includes('sabotage'))); break; case 'theft': incident = randomFrom(incidentTypes.filter(i => i.id.includes('theft') || i.id === 'hoarding')); break; case 'convert': case 'recruit': incident = incidentTypes.find(i => i.id === 'cult_ritual') || incidentTypes.find(i => i.id === 'recruitment'); break; case 'assassinate': if (innerCircle.length > 0 && Math.random() < 0.3) { const target = bunkerMembers.find(m => innerCircle.includes(m.id)); if (target) { incident = { ...incidentTypes.find(i => i.id === 'murder'), targetId: target.id }; } } break; case 'intel': incident = incidentTypes.find(i => i.id === 'signal'); break; case 'explode': incident = { id: 'explosion', name: 'Explosion', severity: 5, evidence: 'Blast damage found', effect: { power: -30, morale: -20 } }; break; default: incident = randomFrom(incidentTypes); } if (incident) { threat.threatInfo.suspicionLevel += 30; // Security might catch them if (Math.random() < detectionChance) { newIncidents.push({ ...incident, perpetratorId: threat.id, perpetratorName: threat.name, detected: true, day: day, factionId: threat.threatInfo.faction?.id }); // Add faction to known factions if (threat.threatInfo.faction && !knownFactions.includes(threat.threatInfo.faction.id)) { setKnownFactions(prev => [...prev, threat.threatInfo.faction.id]); } } else { // Undetected - apply effects silently if (incident.effect) { Object.entries(incident.effect).forEach(([key, val]) => { if (newResources[key] !== undefined) newResources[key] = Math.max(0, newResources[key] + val); }); } events.push(`⚠️ ${incident.evidence} Something is wrong...`); } } threat.threatInfo.hasActed = true; threat.threatInfo.actionsUntilStrike = randomRange(3, 7); // Reset for next action } }); // Add detected incidents to queue if (newIncidents.length > 0) { setDetectedIncidents(prev => [...prev, ...newIncidents]); newIncidents.forEach(inc => { events.push(`🚨 SECURITY ALERT: ${inc.name} detected! ${inc.perpetratorName} is a suspect.`); }); setShowSecurityReport(true); } // 7. Random faction attacks (chance scaled by difficulty) if (Math.random() < Math.min(D.factionAttackCap, D.factionAttackBase + (day * D.factionAttackPerDay))) { const attackingFaction = randomFrom(factions); const attackStrength = attackingFaction.attackPower + randomRange(-1, 2); const defenseStrength = securityLevel + (bunkerMembers.length * 0.3); if (!knownFactions.includes(attackingFaction.id)) { setKnownFactions(prev => [...prev, attackingFaction.id]); } const attackMessage = randomFrom(attackingFaction.attackMessages); events.push(`⚔️ ATTACK: ${attackMessage}`); if (attackStrength > defenseStrength) { // Attack succeeds - losses const damageSeverity = Math.min(3, Math.floor((attackStrength - defenseStrength) / 2)); newResources.food = Math.max(0, newResources.food - (damageSeverity * 8)); newResources.water = Math.max(0, newResources.water - (damageSeverity * 4)); newResources.morale = Math.max(0, newResources.morale - (damageSeverity * 6)); events.push(`💥 ${attackingFaction.name} breached our defenses! We lost supplies.`); // Possible casualty - only if severe and bunker has enough people if (damageSeverity >= 2 && bunkerMembers.length > 3 && Math.random() < D.attackCasualtyChance) { const victim = randomFrom(bunkerMembers.filter(m => !innerCircle.includes(m.id))); if (victim) { dayDeaths.push({ ...victim, cause: 'attack', attackedBy: attackingFaction.name }); setBunkerMembers(prev => prev.filter(m => m.id !== victim.id)); events.push(`💀 ${victim.name} was killed in the attack.`); } } } else { // Defense holds events.push(`🛡️ Our security team repelled ${attackingFaction.name}!`); newResources.morale = Math.min(100, newResources.morale + 5); } } // 8. Horror events (based on horror level and time) setHorrorLevel(prev => prev + D.horrorLevelGain); const horrorRollChance = Math.min(0.55, D.horrorEventBase + (horrorLevel * D.horrorEventPerLevel)); if (Math.random() < horrorRollChance) { const applicableEvents = horrorEvents.filter(e => !e.requiresFaction || knownFactions.includes(e.requiresFaction) ); if (applicableEvents.length > 0) { const horrorEvent = randomFrom(applicableEvents); events.push(`👁️ ${horrorEvent.text}`); if (horrorEvent.effect) { Object.entries(horrorEvent.effect).forEach(([key, val]) => { if (newResources[key] !== undefined) newResources[key] = Math.max(0, newResources[key] + val); }); } } } // 9. Process confined members setConfinedMembers(prev => { const updated = prev.map(c => ({ ...c, daysLeft: c.daysLeft - 1 })).filter(c => c.daysLeft > 0); const released = prev.filter(c => c.daysLeft <= 1); released.forEach(c => { events.push(`🔓 ${bunkerMembers.find(m => m.id === c.id)?.name || 'Someone'} was released from confinement.`); }); setBunkerMembers(members => members.map(m => released.find(c => c.id === m.id) ? { ...m, isConfined: false } : m )); return updated; }); // Morale effects if (newResources.food < 20) newResources.morale -= 10; if (newResources.water < 20) newResources.morale -= 10; if (dayDeaths.length > 0) newResources.morale -= dayDeaths.length * 5; if (bunkerMembers.length === 0) newResources.morale -= 5; newResources.morale = Math.max(0, Math.min(100, newResources.morale)); // 10. Random problems if (Math.random() < D.problemNewChance) { const availableProblems = problemTypes.filter(p => !activeProblems.find(ap => ap.id === p.id) ); if (availableProblems.length > 0) { const newProblem = { ...randomFrom(availableProblems), severity: 1, daysActive: 0 }; setActiveProblems(prev => [...prev, newProblem]); events.push(`🔧 New problem: ${newProblem.name}`); } } // 8. Escalate existing problems setActiveProblems(prev => prev.map(p => { const newSeverity = Math.min(5, p.severity + 0.5); if (p.effect) { Object.entries(p.effect).forEach(([key, value]) => { if (newResources[key] !== undefined) { const v = typeof value === 'number' ? value * D.problemEscalationMult : value; newResources[key] = Math.max(0, newResources[key] + v); } }); } return { ...p, severity: newSeverity, daysActive: p.daysActive + 1 }; })); // 9. Skill improvement for workers setBunkerMembers(prev => prev.map(m => { if (m.job && jobDefinitions[m.job]) { const skillKey = jobDefinitions[m.job].skill; return { ...m, skills: { ...m.skills, [skillKey]: Math.min(5, m.skills[skillKey] + 0.05) }, daysInBunker: m.daysInBunker + 1 }; } return { ...m, daysInBunker: m.daysInBunker + 1 }; })); // 10. Any visitors still in queue at day end face harsh night setVisitorQueue(prev => { prev.forEach(v => { if (Math.random() < D.visitorOvernightDeath) { dayDeaths.push({ ...v, cause: 'exposure' }); events.push(`☠️ ${v.name} did not survive the night outside.`); } }); return []; // Clear the queue }); // ============ NEW SYSTEMS PROCESSING ============ // 11. STORY MILESTONES - Check if any story event should trigger const currentDay = day; const milestone = storyMilestones.find(m => m.day === currentDay && !storyEvents.includes(m.id) && (!m.requires || (m.requires.radioRepaired ? radioRepaired : true)) ); if (milestone) { setActiveDilemma({ type: 'story', ...milestone }); setStoryEvents(prev => [...prev, milestone.id]); } // 12. RELATIONSHIP PROCESSING if (bunkerMembers.length >= 2 && Math.random() < D.relationshipEvent) { // Random relationship event const person1 = randomFrom(bunkerMembers); const person2 = randomFrom(bunkerMembers.filter(m => m.id !== person1.id)); if (person1 && person2) { const relKey = `${Math.min(person1.id, person2.id)}_${Math.max(person1.id, person2.id)}`; const currentRel = relationships[relKey] || 0; // Personality compatibility let relChange = randomRange(-10, 10); if (person1.personality?.id === person2.personality?.id) relChange += 10; if ((person1.personality?.id === 'aggressive' && person2.personality?.id === 'coward') || (person1.personality?.id === 'social' && person2.personality?.id === 'loner')) { relChange -= 15; } if ((person1.personality?.id === 'generous' && person2.personality?.id === 'greedy')) { relChange -= 10; } const newRel = Math.max(-100, Math.min(100, currentRel + relChange)); setRelationships(prev => ({ ...prev, [relKey]: newRel })); // Trigger relationship events if (newRel > 60 && currentRel <= 60 && !person1.romanceWith && !person2.romanceWith && Math.random() < 0.3) { events.push(`💕 ${person1.name} and ${person2.name} have grown close. Romance is in the air.`); setRomances(prev => [...prev, { person1: person1.id, person2: person2.id, strength: 10 }]); setBunkerMembers(prev => prev.map(m => { if (m.id === person1.id) return { ...m, romanceWith: person2.id, mood: Math.min(100, m.mood + 15) }; if (m.id === person2.id) return { ...m, romanceWith: person1.id, mood: Math.min(100, m.mood + 15) }; return m; })); } else if (newRel < -50 && currentRel >= -50) { events.push(`⚔️ ${person1.name} and ${person2.name} have become rivals. Watch for conflict.`); setRivalries(prev => [...prev, { person1: person1.id, person2: person2.id, intensity: Math.abs(newRel) }]); setBunkerMembers(prev => prev.map(m => { if (m.id === person1.id) return { ...m, enemies: [...(m.enemies || []), person2.id] }; if (m.id === person2.id) return { ...m, enemies: [...(m.enemies || []), person1.id] }; return m; })); } } } // 13. CULT CONVERSION PROCESSING const cultists = bunkerMembers.filter(m => m.isThreat && m.threatInfo?.threatType?.id === 'cultist'); cultists.forEach(cultist => { // Cultists try to convert others const targets = bunkerMembers.filter(m => !m.isThreat && !m.isConverted && m.id !== cultist.id && m.loyalty < 60 && m.mood < 50 ); if (targets.length > 0 && Math.random() < D.cultConvert) { const target = randomFrom(targets); const conversionPower = 10 + (cultist.personality?.id === 'manipulative' ? 15 : 0); setBunkerMembers(prev => prev.map(m => { if (m.id !== target.id) return m; const newProgress = (m.conversionProgress || 0) + conversionPower; if (newProgress >= 100) { events.push(`👁️ ${target.name} has been converted by ${cultist.threatInfo.faction.name}! They now serve a dark purpose.`); setConvertedMembers(prev => [...prev, target.id]); return { ...m, isConverted: true, conversionProgress: 100, isThreat: true, threatInfo: { faction: cultist.threatInfo.faction, threatType: { id: 'cultist', action: 'convert' }, actionsUntilStrike: randomRange(3, 6), hasActed: false } }; } if (newProgress > 50 && (m.conversionProgress || 0) <= 50) { events.push(`⚠️ ${target.name} has been acting strangely. They spend a lot of time with ${cultist.name}.`); } return { ...m, conversionProgress: newProgress }; })); } }); // 14. SANITY PROCESSING let sanityChange = 0; if (newResources.morale < 30) sanityChange -= 3; if (horrorLevel > 10) sanityChange -= 2; if (dayDeaths.length > 0) sanityChange -= dayDeaths.length * 3; if (bunkerRooms.chapel) sanityChange += 2; if (newResources.morale > 70) sanityChange += 1; const newSanity = Math.max(0, Math.min(100, sanityLevel + sanityChange)); setSanityLevel(newSanity); // Hallucination chance based on sanity if (newSanity < 50 && Math.random() < (50 - newSanity) / 100) { const hallucination = randomFrom(hallucinationEvents.filter(h => !h.requires || (h.requires.recentDeath && dayDeaths.length > 0) )); if (hallucination) { let text = hallucination.text; if (text.includes('{name}')) { text = text.replace(/{name}/g, randomFrom(bunkerMembers)?.name || 'someone'); } if (text.includes('{deadName}') && dayDeaths.length > 0) { text = text.replace(/{deadName}/g, dayDeaths[0].name); } events.push(`🌀 HALLUCINATION: ${text}`); setHallucinations(prev => [...prev, { ...hallucination, day: currentDay }]); setSanityLevel(prev => Math.max(0, prev + (hallucination.effect?.sanity || 0))); } } // 15. URGENT EVENTS COUNTDOWN setUrgentEvents(prev => { const updated = prev.map(e => ({ ...e, daysLeft: e.daysLeft - 1 })); // Check for expired events updated.forEach(e => { if (e.daysLeft <= 0 && !e.resolved) { // Event failed const eventType = urgentEventTypes.find(t => t.id === e.type); if (eventType) { events.push(`❌ FAILED: ${eventType.title} - Time ran out!`); if (eventType.failure.deaths) { const deathCount = randomRange(eventType.failure.deaths[0], eventType.failure.deaths[1]); for (let i = 0; i < deathCount && bunkerMembers.length > 3; i++) { const victim = randomFrom(bunkerMembers.filter(m => !innerCircle.includes(m.id))); if (victim) { dayDeaths.push({ ...victim, cause: 'event_failure' }); setBunkerMembers(p => p.filter(m => m.id !== victim.id)); } } } if (eventType.failure.morale) { newResources.morale = Math.max(0, newResources.morale + eventType.failure.morale); } } e.resolved = true; } }); return updated.filter(e => !e.resolved); }); // 16. PENDING ATTACK COUNTDOWN if (pendingAttack) { const newDays = pendingAttack.days - 1; if (newDays <= 0) { // Attack happens! events.push(`⚔️ THE ATTACK HAS BEGUN! ${pendingAttack.faction.toUpperCase()} forces storm the bunker!`); const attackStrength = pendingAttack.strength || 10; const defense = securityLevel + defenseLevel + (bunkerRooms.armory ? 5 : 0); if (attackStrength > defense) { const severity = Math.floor((attackStrength - defense) / 3); newResources.food = Math.max(0, newResources.food - severity * 12); newResources.water = Math.max(0, newResources.water - severity * 8); newResources.morale = Math.max(0, newResources.morale - severity * 8); setBunkerIntegrity(prev => Math.max(0, prev - severity * 8)); // Casualties - only if bunker has enough people, max 1 per attack if (severity >= 2 && bunkerMembers.length > 3) { const victim = randomFrom(bunkerMembers.filter(m => !innerCircle.includes(m.id))); if (victim) { dayDeaths.push({ ...victim, cause: 'battle' }); events.push(`💀 ${victim.name} fell defending the bunker.`); } } events.push(`💥 The bunker was breached! Heavy losses sustained.`); } else { events.push(`🛡️ The attack was repelled! Our defenses held!`); newResources.morale = Math.min(100, newResources.morale + 10); } setPendingAttack(null); } else { setPendingAttack({ ...pendingAttack, days: newDays }); events.push(`⏰ WARNING: ${pendingAttack.faction} attack in ${newDays} day(s)!`); } } // 17. RESCUE COUNTDOWN (if active) if (rescueCountdown !== null) { const newCountdown = rescueCountdown - 1; if (newCountdown <= 0) { // Rescue arrives! setEndingTriggered('rescue'); events.push(`🚁 THE HELICOPTER HAS ARRIVED! RESCUE IS HERE!`); } else { setRescueCountdown(newCountdown); events.push(`📻 Rescue ETA: ${newCountdown} day(s). Hold on!`); } } // 18. BUNKER INTEGRITY CHECK if (bunkerIntegrity < 30) { events.push(`🏚️ WARNING: Bunker structural integrity critical! (${Math.floor(bunkerIntegrity)}%)`); newResources.morale = Math.max(0, newResources.morale - 5); } // 19. MORAL DILEMMA TRIGGERS if (!activeDilemma && Math.random() < D.dilemmaChance) { // Check for condition-based dilemmas const applicableDilemmas = moralDilemmas.filter(d => { if (dilemmaHistory.find(h => h.id === d.id)) return false; if (d.requires) { if (d.requires.food && newResources.food > d.requires.food.max) return false; } return true; }); if (applicableDilemmas.length > 0) { setActiveDilemma(randomFrom(applicableDilemmas)); } } // 20. THE THING - Monster among us processing (paced + cooldown) if (theThingMember && bunkerMembers.length > 4) { const cooldownDays = 3; // prevents rapid back-to-back snatches const canSnatch = (currentDay - lastThingSnatchDay) >= cooldownDays; // Slower baseline chance; small increase when sanity is very low const chance = Math.max(0.005, 0.02 + (newSanity < 40 ? 0.01 : 0) + D.thingSnatchAdd); if (canSnatch && currentDay > 5 && Math.random() < chance) { const victims = bunkerMembers.filter(m => m.id !== theThingMember && !innerCircle.includes(m.id)); if (victims.length > 0) { const victim = randomFrom(victims); setMissingMembers(prev => [...prev, { ...victim, day: currentDay }]); setBunkerMembers(prev => prev.filter(m => m.id !== victim.id)); setLastThingSnatchDay(currentDay); events.push(`❓ ${victim.name} has gone missing. Their bunk is empty. There's... something on the walls.`); setSanityLevel(prev => Math.max(0, prev - 10)); } } } // ============ MEGA EXPANSION PROCESSING ============ // 21. WEATHER SYSTEM if (weatherDuration > 0) { setWeatherDuration(prev => prev - 1); } else if (Math.random() < D.weatherChangeChance) { // Change weather const newWeather = randomFrom(weatherTypes); setCurrentWeather(newWeather.id); setWeatherDuration(randomRange(newWeather.duration[0], newWeather.duration[1])); events.push(`${newWeather.icon} Weather changed: ${newWeather.name}`); // Apply weather effects if (newWeather.effects.morale) { newResources.morale = Math.max(0, newResources.morale + newWeather.effects.morale); } if (newWeather.effects.sanity) { setSanityLevel(s => Math.max(0, s + newWeather.effects.sanity)); } } // 22. DAY/NIGHT CYCLE const hour = Math.floor((gameTime / 180) * 24); if (hour < 6) setTimeOfDay('night'); else if (hour < 8) setTimeOfDay('dawn'); else if (hour < 18) setTimeOfDay('day'); else if (hour < 20) setTimeOfDay('dusk'); else setTimeOfDay('night'); // Night events if (timeOfDay === 'night' && Math.random() < D.nightEvent) { const nightEvent = randomFrom([ 'Strange sounds echo through the bunker...', 'Someone reports seeing shadows moving outside.', 'The radio crackles with static all night.', 'A member reports nightmares.', 'Something scratched at the door until dawn.' ]); events.push(`🌙 ${nightEvent}`); setSanityLevel(s => Math.max(0, s - 2)); } // 23. DREAMS if (Math.random() < D.dreamChance) { const dream = randomFrom(dreamSequences); setActiveDream(dream); setDreamHistory(prev => [...prev, { ...dream, day: currentDay }]); if (dream.effect.sanity) setSanityLevel(s => Math.max(0, Math.min(100, s + dream.effect.sanity))); if (dream.effect.morale) newResources.morale = Math.max(0, Math.min(100, newResources.morale + (dream.effect.morale || 0))); } // 24. CRAFTING QUEUE PROGRESS setCraftingQueue(prev => { const updated = prev.map(item => ({ ...item, daysLeft: item.daysLeft - 1 })); const completed = updated.filter(item => item.daysLeft <= 0); const remaining = updated.filter(item => item.daysLeft > 0); completed.forEach(item => { events.push(`🔨 Crafting complete: ${item.name}!`); // Apply item effects if (item.effect.defense) setDefenseLevel(d => d + item.effect.defense); if (item.effect.healing) { // Add medkit to inventory or heal someone setInventory(inv => ({ ...inv, medkits: (inv.medkits || 0) + 1 })); } }); return remaining; }); // 25. TRADER ARRIVAL if (currentDay >= nextTraderDay && !traderPresent) { setTraderPresent(true); setTraderInventory(traderGoods.slice(0, 4 + Math.floor(Math.random() * 4))); events.push('🛒 A traveling trader has arrived at the bunker!'); } else if (traderPresent && Math.random() < 0.5) { setTraderPresent(false); setNextTraderDay(currentDay + randomRange(5, 10)); events.push('🛒 The trader has moved on.'); } // 26. DISEASE SPREAD activeDiseases.forEach(disease => { const diseaseData = diseases.find(d => d.id === disease.diseaseId); if (diseaseData && diseaseData.spread > 0) { bunkerMembers.forEach(member => { if (member.id !== disease.memberId && !quarantinedMembers.includes(disease.memberId)) { if (Math.random() < diseaseData.spread * 0.5) { setActiveDiseases(prev => [...prev, { memberId: member.id, diseaseId: disease.diseaseId, progress: 0, day: currentDay }]); events.push(`🦠 ${member.name} has contracted ${diseaseData.name}!`); } } }); } }); // 27. MUTATION PROCESSING Object.entries(radiationExposure).forEach(([memberId, level]) => { if (level > 50 && Math.random() < 0.1) { const mutation = randomFrom(mutationTypes.filter(m => m.tier <= Math.ceil(level / 30))); if (mutation && !mutations[memberId]?.includes(mutation.id)) { setMutations(prev => ({ ...prev, [memberId]: [...(prev[memberId] || []), mutation.id] })); const member = bunkerMembers.find(m => m.id === parseInt(memberId)); if (member) { events.push(`☢️ ${member.name} has mutated: ${mutation.name}!`); } } } }); // 28. PET EFFECTS pets.forEach(pet => { const petData = petTypes.find(p => p.id === pet.type); if (petData) { if (petData.effect.morale) newResources.morale = Math.min(100, newResources.morale + petData.effect.morale * 0.1); if (petData.effect.security) setDefenseLevel(d => d + petData.effect.security * 0.1); // Pets eat food newResources.food = Math.max(0, newResources.food - (petData.food || 1)); } }); // 29. PREGNANCY PROGRESS setPregnancies(prev => { const updated = prev.map(p => ({ ...p, daysLeft: p.daysLeft - 1 })); const births = updated.filter(p => p.daysLeft <= 0); const ongoing = updated.filter(p => p.daysLeft > 0); births.forEach(birth => { const mother = bunkerMembers.find(m => m.id === birth.motherId); if (mother) { // Create a child const child = { ...generateSurvivor(), id: Date.now() + Math.random(), name: `Baby ${mother.name.split(' ')[1] || 'Unknown'}`, isChild: true, age: 0 }; setChildren(prev => [...prev, child]); events.push(`👶 ${mother.name} has given birth! A new life in the bunker!`); newResources.morale = Math.min(100, newResources.morale + 15); // Achievement if (!achievements.includes('first_birth')) { setAchievements(prev => [...prev, 'first_birth']); setAchievementPopup(achievementList.find(a => a.id === 'first_birth')); } } }); return ongoing; }); // 30. THE WATCHER - Horror escalation if (horrorLevel > 20) { setWatcherPresence(prev => Math.min(100, prev + randomRange(1, 5))); if (watcherPresence > 50 && Math.random() < 0.2) { events.push('👁️ You feel watched. Something vast and patient observes the bunker.'); setSanityLevel(s => Math.max(0, s - 3)); } } // 31. BROADCAST RESET if (isBroadcasting) { setIsBroadcasting(false); setBroadcastType(null); } // 32. ACHIEVEMENT CHECKS if (bunkerMembers.length >= 20 && !achievements.includes('full_bunker')) { setAchievements(prev => [...prev, 'full_bunker']); setAchievementPopup(achievementList.find(a => a.id === 'full_bunker')); } if (currentDay >= 7 && !achievements.includes('week_survived')) { setAchievements(prev => [...prev, 'week_survived']); setAchievementPopup(achievementList.find(a => a.id === 'week_survived')); } if (currentDay >= 30 && !achievements.includes('month_survived')) { setAchievements(prev => [...prev, 'month_survived']); setAchievementPopup(achievementList.find(a => a.id === 'month_survived')); } // ============ END MEGA EXPANSION ============ // ============ NEW FEATURES PROCESSING ============ // 33c. SCOUTING TEAM PROCESSING if (scoutingTeam) { if (scoutingTeam.status === 'traveling') { if (Math.random() < 0.2) { const evt = randomFrom(travelEvents); if (evt) setTravelEvent(evt); } if (day >= scoutingTeam.returnDay) { setExploredMap(prev => ({ ...prev, [scoutingTeam.locationId]: { discovered: true, day } })); setScoutingTeam(prev => prev ? { ...prev, status: 'on_site' } : null); const loc = mapLocations.find(l => l.id === scoutingTeam.locationId); events.push(`🗺️ Team arrived at ${loc?.name || 'unknown location'}!`); if (Math.random() < 0.3) { const undiscovered = mapLocations.filter(l => !exploredMap[l.id] && l.id !== scoutingTeam.locationId); if (undiscovered.length > 0) { const found = randomFrom(undiscovered); setExploredMap(prev => ({ ...prev, [found.id]: { discovered: true, day } })); events.push(`🗺️ Team also spotted ${found.name} nearby!`); } } } } if (scoutingTeam.status === 'on_site') { const loc = mapLocations.find(l => l.id === scoutingTeam.locationId); if (loc) { const allSearched = loc.rooms.every(r => locationLootCollected[loc.id]?.[r.id]); if (allSearched && !clearedLocations.includes(loc.id)) { claimClearingReward(loc.id); } } } } // 33d. OUTPOST DAILY YIELDS Object.entries(outposts).forEach(([locId, outpost]) => { if (outpost.dailyYield) { if (outpost.dailyYield.food) newResources.food = Math.min(200, newResources.food + outpost.dailyYield.food); if (outpost.dailyYield.water) newResources.water = Math.min(200, newResources.water + outpost.dailyYield.water); if (outpost.dailyYield.defense) setDefenseLevel(d => d + outpost.dailyYield.defense); if (outpost.dailyYield.integrity) setBunkerIntegrity(i => Math.min(100, i + outpost.dailyYield.integrity)); if (outpost.dailyYield.medicine) newResources.food = Math.min(200, newResources.food + 1); } }); if (Object.keys(outposts).length > 0) { const yieldSummary = Object.entries(outposts).map(([id, o]) => o.name).join(', '); events.push(`🏗️ Outpost yields: ${yieldSummary}`); } // 34. FOUND NOTES - Random chance to discover a note if (Math.random() < D.noteChance) { const undiscovered = foundNotes.filter(n => !discoveredNotes.includes(n.id)); if (undiscovered.length > 0) { const note = randomFrom(undiscovered); setDiscoveredNotes(prev => [...prev, note.id]); setShowNoteModal(note); events.push(`📜 Found a note in ${note.location}: "${note.title}"`); setSanityLevel(s => Math.max(0, s - 3)); } } // 35. MEMBER CONFESSIONS - High trust members may confess bunkerMembers.forEach(member => { if (member.loyalty >= 70 && member.daysInBunker >= 5 && !confessionsRevealed[member.id] && Math.random() < D.confessionChance) { const availableConfessions = memberConfessions.filter(c => c.minTrust <= member.loyalty && !Object.values(confessionsRevealed).includes(c.id) ); if (availableConfessions.length > 0) { const confession = randomFrom(availableConfessions); setConfessionsRevealed(prev => ({ ...prev, [member.id]: confession.id })); setShowConfessionModal({ member, confession }); events.push(`🤫 ${member.name} pulled you aside to confess something...`); } } }); // 36. THE VOICE ON THE RADIO - Progressive mystery if (radioRepaired) { const nextVoiceMsg = voiceOnRadioMessages.find(v => v.day <= currentDay && v.stage > voiceStage); if (nextVoiceMsg && Math.random() < D.voiceChance) { setVoiceStage(nextVoiceMsg.stage); setVoiceMessages(prev => [...prev, { ...nextVoiceMsg, day: currentDay }]); setShowVoiceModal(nextVoiceMsg); events.push(`📻 A strange voice crackles through the radio...`); setSanityLevel(s => Math.max(0, s - 2)); } } // 37. MEMORIAL WALL - Add deaths if (dayDeaths.length > 0) { dayDeaths.forEach(dead => { setMemorialWall(prev => [...prev, { name: dead.name, cause: dead.cause || 'unknown', day: currentDay, appearance: dead.appearance }]); }); } // 38. MORALE EVENT COOLDOWNS setMoraleEventCooldowns(prev => { const updated = {}; Object.entries(prev).forEach(([id, cd]) => { if (cd > 1) updated[id] = cd - 1; }); return updated; }); // 39. RIVAL BUNKER PROCESSING if (!rivalBunker.discovered && currentDay >= 8 && Math.random() < D.rivalDiscover) { setRivalBunker(prev => ({ ...prev, discovered: true })); events.push(`📡 Radio signal detected! Another bunker... ${rivalBunkerData.name} is nearby!`); } if (rivalBunker.discovered) { // Rival bunker attitude drifts toward neutral setRivalBunker(prev => ({ ...prev, attitude: prev.attitude + (prev.attitude < 50 ? 1 : prev.attitude > 50 ? -0.5 : 0), population: Math.max(5, prev.population + (Math.random() < 0.1 ? randomRange(-1, 1) : 0)) })); // Rival bunker may raid if hostile if (rivalBunker.attitude < 20 && Math.random() < D.rivalRaid) { const raidLoss = randomRange(5, 15); newResources.food = Math.max(0, newResources.food - raidLoss); newResources.water = Math.max(0, newResources.water - Math.floor(raidLoss * 0.7)); events.push(`⚔️ ${rivalBunkerData.name} raided your supplies! Lost ${raidLoss} food!`); setRivalBunker(prev => ({ ...prev, raidedUs: true })); } // Rival bunker may send gift if very friendly if (rivalBunker.attitude > 75 && Math.random() < 0.08) { const gift = randomRange(5, 12); newResources.food = Math.min(200, newResources.food + gift); events.push(`🎁 ${rivalBunkerData.name} sent a care package! +${gift} food.`); } // Random rival bunker messages if (Math.random() < 0.1) { const msgCategory = rivalBunker.attitude > 60 ? 'friendly' : rivalBunker.attitude < 30 ? 'hostile' : 'neutral'; const msg = randomFrom(rivalBunkerData.messages[msgCategory]); events.push(`📻 ${rivalBunkerData.name}: "${msg}"`); } } // 40. UPDATE STATISTICS setStats(prev => ({ ...prev, peakPopulation: Math.max(prev.peakPopulation, bunkerMembers.length), totalDied: prev.totalDied + dayDeaths.length })); // ============ END NEW FEATURES ============ // 33. Generate newspaper const paper = generateNewspaper(day, bunkerMembers, activeProblems, events, dayDeaths, newResources); setNewspaper(paper); setShowNewspaper(true); // 12. Update state setResources(newResources); setDailyEvents([]); setRecentDeaths([]); setDay(prev => prev + 1); setGameTime(0); setNextVisitorTime(randomRange(10, 30)); // 13. Check game over / endings if (endingTriggered) { // Ending was triggered - don't do normal game over return; } if (newResources.food <= 0) { setGameOverReason({ type: 'starvation', text: 'The food ran out. Starvation claimed the bunker.' }); setTimeout(() => setGameState('gameover'), 2000); } else if (newResources.water <= 0) { setGameOverReason({ type: 'dehydration', text: 'The water supply dried up. Dehydration took everyone.' }); setTimeout(() => setGameState('gameover'), 2000); } else if (newResources.morale <= 0) { setGameOverReason({ type: 'despair', text: 'Hope died. The bunker fell to despair and chaos.' }); setTimeout(() => setGameState('gameover'), 2000); } else if (newResources.power <= 0) { setGameOverReason({ type: 'darkness', text: 'The lights went out. In the darkness, the bunker fell.' }); setTimeout(() => setGameState('gameover'), 2000); } else if (newSanity <= 0) { // Sanity collapse setGameOverReason({ type: 'madness', text: 'Madness consumed everyone. Reality fractured beyond repair.' }); setEndingTriggered('madness'); } else if (bunkerIntegrity <= 0) { // Bunker collapse setGameOverReason({ type: 'collapse', text: 'The structure gave way. The bunker collapsed.' }); setEndingTriggered('collapse'); } else if (bunkerMembers.length === 0) { setGameOverReason({ type: 'empty', text: 'Everyone is gone. The bunker stands empty.' }); setTimeout(() => setGameState('gameover'), 2000); } }; // ============ RENDER FUNCTIONS ============ const renderJobSlots = (jobId) => { const job = jobDefinitions[jobId]; const workers = bunkerMembers.filter(m => m.job === jobId); const slots = []; for (let i = 0; i < job.maxSlots; i++) { const worker = workers[i]; slots.push(
worker && setSelectedMember(worker)} title={worker ? `${worker.name} (Skill: ${worker.skills[job.skill].toFixed(1)})` : 'Empty slot'} > {worker ? ( ) : ( + )}
); } return (
{job.icon} {job.name} ({workers.length}/{job.maxSlots})
{slots}
{job.desc}
); }; // ============ SCREENS ============ // Title Screen if (gameState === 'title') { return (
{/* How to Play Modal */} {showHowToPlay && (

📖 HOW TO PLAY

🎯 Your Goal

Survive as long as possible. Manage your bunker, protect survivors, and uncover the mysteries of the wasteland.

📊 Resources (Keep These Above Zero!)

  • 🍖 Food - Decays daily. Assign Farmers to grow more, Cooks to reduce spoilage.
  • 💧 Water - Consumed daily. Scavengers can find more.
  • ⚡ Power - Drains slowly. Assign Engineers to maintain it.
  • 😊 Morale - Drops from bad events. Assign Community workers to boost it.

👥 Managing Your Bunker

  • Gate Tab - Interview visitors. Use your guitar (🎸) to reveal their true nature. Admit or deny them.
  • Jobs Tab - Drag members to job slots. Match their skills for best results.
  • Members Tab - View everyone's stats, relationships, and secrets.
  • Inner Circle - Promote trusted members. They give advice and vote on decisions.

🎒 Exploration

  • Assign members as Scavengers in the Jobs tab.
  • Select a region from the dropdown in the left sidebar.
  • Click "Deploy Scavengers" to send them out.
  • They return with supplies and may discover new map locations.

⚠️ Ways Your Bunker Can Fall

  • 🍖 Starvation - Food hits zero
  • 💧 Dehydration - Water hits zero
  • 😔 Despair - Morale hits zero
  • 🔌 Darkness - Power hits zero
  • 🌀 Madness - Collective sanity breaks
  • 🏚️ Collapse - Bunker integrity destroyed
  • ⚔️ Attacked - Faction assault succeeds
  • 👻 Abandoned - All members die or leave

🎸 Special Abilities

  • Guitar Reading - Play for visitors to see colored auras revealing their nature.
  • Inner Circle Meetings - Discuss threats, resources, and strategy with trusted advisors.
  • Radio Broadcasting - Attract traders and survivors (or unwanted attention).

⚙️ Difficulty

On the intro screen (before you seal the doors), pick Easier, Normal, or Hard. That choice is saved in the browser and scales random crises, resource drain, scavenger risk, and similar rolls for the next run.

💡 Tips

  • Read the daily newspaper for faction movements and world events.
  • Watch CCTV cameras to monitor your bunker and the wasteland.
  • Balance security with compassion - not everyone is a threat.
  • Craft items at the workbench to improve survival odds.
  • Explore The Deep for powerful rewards (and terrible dangers).
)}
A SURVIVAL HORROR EXPERIENCE

THE BUNKER

Version: HOLY SANTOS
); } // Intro Screen if (gameState === 'intro') { return (

▸ TEX SANTOS

You are Tex Santos, though some know you as Walter Tiberious Banks. Before everything fell apart, you were a musician. Now you run the last functioning bunker in Sector 7.

Your guitar reveals truth—when you play, you can see people's real intentions. But trust is complicated. Build your inner circle, assign jobs, manage crises, and keep everyone alive.

◈ INNER CIRCLE: Choose up to 3 trusted advisors. They'll give opinions on new arrivals—but they might have their own agendas.

◈ JOBS: Assign workers to roles. Skills improve with practice. Workers eat more.

◈ PROBLEMS: Issues escalate if ignored. Assign the right workers to fix them.

◈ SCAVENGING: High risk, high reward. Scavengers can die.

◈ THE NEWSPAPER: Each day ends with a report of events, rumors, and warnings.
DIFFICULTY
{['easy', 'normal', 'hard'].map((id) => ( ))}
{diffParams().blurb}
); } // Game Over if (gameState === 'gameover') { const survivors = bunkerMembers.filter(m => !m.isThreat).length; const threats = bunkerMembers.filter(m => m.isThreat).length; const reasonIcons = { starvation: '🍖', dehydration: '💧', despair: '😔', darkness: '🔌', madness: '🌀', collapse: '🏚️', empty: '👻', attacked: '⚔️', infiltrated: '🎭' }; return (

BUNKER COMPROMISED

Day {day}

{/* Reason for failure */} {gameOverReason && (
{reasonIcons[gameOverReason.type] || '💀'}

{gameOverReason.text}

)}

Survivors Protected: {survivors}

Threats Admitted: {threats}

Days Survived: {day - 1}

🍖 Food: {Math.floor(resources.food)} | 💧 Water: {Math.floor(resources.water)}

⚡ Power: {Math.floor(resources.power)} | 😊 Morale: {Math.floor(resources.morale)}

); } // ============ MAIN GAME SCREEN ============ return (
{/* HOW TO PLAY MODAL */} {showHowToPlay && (

📖 HOW TO PLAY

🎯 Your Goal

Survive as long as possible. Manage your bunker, protect survivors, and uncover the mysteries of the wasteland.

📊 Resources (Keep These Above Zero!)

  • 🍖 Food - Decays daily. Assign Farmers to grow more, Cooks to reduce spoilage.
  • 💧 Water - Consumed daily. Scavengers can find more.
  • ⚡ Power - Drains slowly. Assign Engineers to maintain it.
  • 😊 Morale - Drops from bad events. Assign Community workers to boost it.

👥 Managing Your Bunker

  • Gate Tab - Interview visitors. Use your guitar (🎸) to reveal their true nature. Admit or deny them.
  • Jobs Tab - Drag members to job slots. Match their skills for best results.
  • Members Tab - View everyone's stats, relationships, and secrets.
  • Inner Circle - Promote trusted members. They give advice and vote on decisions.

🎒 Exploration

  • Assign members as Scavengers in the Jobs tab.
  • Select a region from the dropdown in the left sidebar.
  • Click "Deploy Scavengers" to send them out.
  • They return with supplies and may discover new map locations.

⚠️ Ways Your Bunker Can Fall

  • 🍖 Starvation - Food hits zero
  • 💧 Dehydration - Water hits zero
  • 😔 Despair - Morale hits zero
  • 🔌 Darkness - Power hits zero
  • 🌀 Madness - Collective sanity breaks
  • 🏚️ Collapse - Bunker integrity destroyed
  • ⚔️ Attacked - Faction assault succeeds
  • 👻 Abandoned - All members die or leave

🎸 Special Abilities

  • Guitar Reading - Play for visitors to see colored auras revealing their nature.
  • Inner Circle Meetings - Discuss threats, resources, and strategy with trusted advisors.
  • Radio Broadcasting - Attract traders and survivors (or unwanted attention).

💡 Tips

  • Read the daily newspaper for faction movements and world events.
  • Watch CCTV cameras to monitor your bunker and the wasteland.
  • Balance security with compassion - not everyone is a threat.
  • Craft items at the workbench to improve survival odds.
  • Explore The Deep for powerful rewards (and terrible dangers).
)} {/* STATISTICS DASHBOARD MODAL */} {showStats && (

📊 BUNKER STATISTICS

{[ ['Days Survived', day - 1], ['Peak Population', stats.peakPopulation], ['Current Population', bunkerMembers.length], ['Total Admitted', stats.totalAdmitted], ['Total Expelled', stats.totalExpelled], ['Total Deaths', stats.totalDied], ['Attacks Repelled', stats.totalAttacksRepelled], ['Morale Events', stats.moraleEventsHeld], ['Trades Completed', stats.tradesCompleted], ['Notes Found', discoveredNotes.length + '/' + foundNotes.length], ['Confessions Heard', Object.keys(confessionsRevealed).length], ['Voice Stage', voiceStage + '/10'], ['Memorial Entries', memorialWall.length], ['Sanity Level', Math.floor(sanityLevel) + '%'], ['Bunker Integrity', Math.floor(bunkerIntegrity) + '%'], ['Defense Level', Math.floor(defenseLevel)] ].map(([label, value], i) => (
{label}
{value}
))}
{rivalBunker.discovered && (

🏠 {rivalBunkerData.name}

Attitude: {Math.floor(rivalBunker.attitude)}% | Pop: {rivalBunker.population} | {rivalBunker.alliance ? '🤝 ALLIED' : rivalBunker.attitude > 60 ? 'Friendly' : rivalBunker.attitude < 30 ? 'Hostile' : 'Neutral'}

)}
)} {/* MEMORIAL WALL MODAL */} {showMemorial && (

🕯️ MEMORIAL WALL

In memory of those we've lost

{memorialWall.length === 0 ? (

No deaths yet. May it stay this way.

) : (
{memorialWall.map((entry, i) => { const causeText = { 'attack': '⚔️ Killed in attack', 'battle': '⚔️ Fell in battle', 'scavenging': '🎒 Lost while scavenging', 'exposure': '❄️ Died of exposure', 'executed': '💀 Executed', 'event_failure': '❌ Event failure', 'disease': '🦠 Disease', 'unknown': '❓ Unknown cause' }; return (
🕯️
{entry.name}
Day {entry.day} — {causeText[entry.cause] || `💀 ${entry.cause}`}
); })}
)}
)} {/* FOUND NOTE MODAL */} {showNoteModal && (
Found in: {showNoteModal.location}

📜 {showNoteModal.title}

{showNoteModal.text}

)} {/* CONFESSION MODAL */} {showConfessionModal && (

🤫 {showConfessionModal.member.name}'s Confession

"{showConfessionModal.confession.text}"

Mood: {showConfessionModal.confession.mood}
)} {/* VOICE ON RADIO MODAL */} {showVoiceModal && (
📻 UNKNOWN FREQUENCY

THE VOICE

"{showVoiceModal.text}"

Transmission {showVoiceModal.stage}/10
)} {/* NEWSPAPER MODAL */} {showNewspaper && newspaper && (

{newspaper.headline}

{newspaper.articles.map((article, i) => (
{article.type === 'event' && (

📰 {article.content}

)} {article.type === 'death' && (

💀 {article.content}

)} {article.type === 'problem' && (

⚠️ {article.content}

)} {article.type === 'warning' && (

🚨 {article.content}

)} {article.type === 'gossip' && (

🗣️ {article.content}

)} {article.type === 'good' && (

✨ {article.content}

)} {article.type === 'bad' && (

📉 {article.content}

)}
))}
)} {/* MEMBER DETAIL MODAL */} {selectedMember && (

{selectedMember.name}

{selectedMember.background.name} • Day {selectedMember.daysInBunker + 1}

{/* Personality */}
{selectedMember.personality?.icon}
{selectedMember.personality?.name}
{selectedMember.personality?.desc}

Job: {selectedMember.job ? jobDefinitions[selectedMember.job].name : 'None'}

{/* Status bars */}
❤️ Health {Math.floor(selectedMember.health)}%
🍖 Hunger {Math.floor(selectedMember.hunger)}%
💧 Thirst {Math.floor(selectedMember.thirst)}%
{/* Mood bar */}
😊 Mood {Math.floor(selectedMember.mood)}% ({getMoodConsequence(selectedMember.mood, selectedMember.personality).level})

"{selectedMember.backstory}"

SKILLS

{Object.entries(selectedMember.skills).map(([skill, level]) => (
{skill}: = 3 ? '#88cc88' : '#c4a35a' }}>{level.toFixed(1)}
))}
{/* Relationships */} {(selectedMember.romanceWith || (selectedMember.enemies && selectedMember.enemies.length > 0) || selectedMember.isConverted) && (

RELATIONSHIPS

{selectedMember.romanceWith && (() => { const partner = bunkerMembers.find(m => m.id === selectedMember.romanceWith); return partner ? (
💕 In love with {partner.name}
) : null; })()} {selectedMember.enemies && selectedMember.enemies.length > 0 && (
⚔️ Rivals: {selectedMember.enemies.map(eId => bunkerMembers.find(m => m.id === eId)?.name).filter(Boolean).join(', ')}
)} {selectedMember.isConverted && (
👁️ Has been converted to a dark cause...
)} {selectedMember.conversionProgress > 0 && selectedMember.conversionProgress < 100 && (
⚠️ Being influenced... ({Math.floor(selectedMember.conversionProgress)}%)
)}
)} {/* Sanity indicator */} {selectedMember.sanity < 70 && (
🧠 Mental State: {selectedMember.sanity > 50 ? 'Stressed' : selectedMember.sanity > 30 ? 'Unstable' : 'Breaking down'}
50 ? '#aa88cc' : '#cc6688' }} />
)} {/* Inner Circle Advice */} {innerCircle.length > 0 && currentVisitor?.id === selectedMember.id && (

INNER CIRCLE OPINIONS

{getInnerCircleAdvice(selectedMember.id).map((advice, i) => (

{advice.advisor}: {advice.text}

))}
)} {/* Talk to Member */}

💬 TALK (as {texIdentity === 'tex' ? 'Tex Santos' : 'Walter Banks'})

{/* Conversation History */} {memberDialogue.length > 0 && (
{memberDialogue.map((entry, i) => (
You: {entry.question}
{selectedMember.name.split(' ')[0]}: {entry.response}
))}
)} {/* Conversation Topics */}
{memberTopics.filter(t => !memberDialogue.find(d => d.id === t.id)).map(topic => ( ))}
{memberDialogue.length >= memberTopics.length && (
You've covered all topics.
)}
{/* Job Assignment */}

ASSIGN JOB

{Object.entries(jobDefinitions).map(([jobId, job]) => { const workers = bunkerMembers.filter(m => m.job === jobId).length; const isFull = workers >= job.maxSlots; const isCurrentJob = selectedMember.job === jobId; return ( ); })}
{/* Actions */}
{!selectedMember.isInnerCircle && innerCircle.length < 3 && ( )} {selectedMember.isInnerCircle && ( )} {!selectedMember.isWatched ? ( ) : ( )}
)} {/* SECURITY REPORT MODAL */} {showSecurityReport && detectedIncidents.length > 0 && (
🚨

SECURITY ALERT

{detectedIncidents.length} incident(s) require your attention

{/* Incident List or Detail */} {selectedIncident ? (
{/* Incident Detail */}

{selectedIncident.name}

= 4 ? '#4a1010' : '#3a2020', color: selectedIncident.severity >= 4 ? '#ff4444' : '#cc8888', fontSize: '10px', borderRadius: '3px' }}> SEVERITY: {selectedIncident.severity}/5

📋 Evidence: {selectedIncident.evidence}

{selectedIncident.factionId && (

{factions.find(f => f.id === selectedIncident.factionId)?.icon} Linked to: {factions.find(f => f.id === selectedIncident.factionId)?.name}

)} {/* Perpetrator Info */} {(() => { const perp = bunkerMembers.find(m => m.id === selectedIncident.perpetratorId); if (!perp) return null; return (
SUSPECT:
{perp.name}
{perp.personality?.icon} {perp.personality?.name} • {perp.background.name}
Day {perp.daysInBunker + 1} in bunker • Loyalty: {Math.floor(perp.loyalty)}%
); })()}
{/* Inner Circle Advice */} {innerCircle.length > 0 && (

INNER CIRCLE ADVICE:

{getInnerCircleAdviceOnIncident(selectedIncident).map((advice, i) => (

{advice.advisor}: "{advice.text}"

))}
)} {/* Punishment Options */}

CHOOSE PUNISHMENT:

{punishmentOptions.map(p => ( ))}
) : (
{/* Incident List */} {detectedIncidents.map((incident, i) => ( ))}
)}
)} {/* INNER CIRCLE MEETING MODAL */} {showMeeting && (
{/* Meeting Header */}
🏛️

INNER CIRCLE MEETING

Speaking as {texIdentity === 'tex' ? 'Tex Santos' : 'Director Banks'}

{/* Attendees */}
{innerCircle.map(memberId => { const member = bunkerMembers.find(m => m.id === memberId); if (!member) return null; return (
{member.name.split(' ')[0]}
{member.personality?.icon} {member.personality?.name}
); })} {innerCircle.length === 0 && (

No advisors appointed. Add members to your Inner Circle first.

)}
{innerCircle.length > 0 && ( <> {/* Discussion History */} {meetingDiscussion.length > 0 && (
{meetingDiscussion.map((entry, i) => (
{entry.type === 'question' ? (
{texIdentity === 'tex' ? 'Tex' : 'Banks'}: "{entry.text}"
) : (
{entry.personality?.icon} {entry.speaker}: "{entry.text}"
)}
))}
)} {/* Topic Selection */}

{meetingDiscussion.length === 0 ? 'CHOOSE A TOPIC TO DISCUSS:' : 'DISCUSS ANOTHER TOPIC:'}

{meetingTopics.filter(t => !discussedTopics.includes(t.id)).map(topic => ( ))}
{discussedTopics.length >= meetingTopics.length && (
You've covered all topics for this meeting.
)}
)}
)} {/* MORAL DILEMMA / STORY EVENT MODAL */} {activeDilemma && (
{activeDilemma.type === 'story' ? '◆ STORY EVENT ◆' : '◆ MORAL DILEMMA ◆'}

{activeDilemma.title}

{activeDilemma.text}

{activeDilemma.choices.map(choice => ( ))}
)} {/* EXPEDITION EVENT MODAL */} {expeditionEvent && (
◆ EXPEDITION EVENT ◆

{expeditionEvent.title}

{expeditionEvent.text}

{expeditionEvent.choices.map(choice => ( ))}
)} {/* FACTION DIPLOMACY MODAL */} {showDiplomacy && (

🏴 FACTION RELATIONS

{factions.map(faction => { const relation = factionRelations[faction.id] || 0; const status = relation > 50 ? 'Allied' : relation > 20 ? 'Friendly' : relation > -20 ? 'Neutral' : relation > -50 ? 'Hostile' : 'At War'; const color = relation > 50 ? '#88cc88' : relation > 20 ? '#aacc88' : relation > -20 ? '#cccc88' : relation > -50 ? '#ccaa88' : '#cc8888'; return (
{faction.icon}
{faction.name}
{status} ({relation})

{faction.desc}

{knownFactions.includes(faction.id) && relation > -80 && (
{factionDiplomacyOptions[faction.id]?.trade && ( )} {factionDiplomacyOptions[faction.id]?.tribute && relation < 20 && ( )}
)}
); })}
)} {/* DREAM MODAL */} {activeDream && (
{activeDream.type === 'nightmare' ? '😰' : activeDream.type === 'peaceful' ? '😌' : activeDream.type === 'horror' ? '👁️' : activeDream.type === 'prophetic' ? '🔮' : '💭'}
◆ {activeDream.type.toUpperCase()} ◆

{activeDream.title}

{activeDream.text}

)} {/* ACHIEVEMENT POPUP */} {achievementPopup && (
{achievementPopup.icon}
ACHIEVEMENT UNLOCKED
{achievementPopup.name}
{achievementPopup.desc}
)} {/* TRADER MODAL */} {traderPresent && selectedTab === 'overview' && (

🛒 WANDERING TRADER

"Got some rare finds today, friend. What catches your eye?"

{traderInventory.map((item, i) => { const canAfford = Object.entries(item.price).every(([res, cost]) => { if (res === 'food') return resources.food >= cost; if (res === 'scrap') return inventory.scrap >= cost; if (res === 'electronics') return inventory.electronics >= cost; return false; }); return (
{item.name}
Cost: {Object.entries(item.price).map(([r, c]) => `${c} ${r}`).join(', ')}
); })}
)} {/* LORE DOCUMENT MODAL */} {selectedLore && (
{selectedLore.type.toUpperCase()}

{selectedLore.title}

{selectedLore.content}

)} {/* ENDING SCREEN */} {endingTriggered && (
{endingTriggered === 'rescue' ? '🚁' : endingTriggered === 'sacrifice' ? '💀' : endingTriggered === 'changed' ? '🧬' : endingTriggered === 'exodus' ? '🚶' : endingTriggered === 'madness' ? '🌀' : endingTriggered === 'collapse' ? '🏚️' : '🏆'}

{endingTriggered === 'rescue' ? 'RESCUED' : endingTriggered === 'sacrifice' ? 'SACRIFICE' : endingTriggered === 'changed' ? 'EVOLUTION' : endingTriggered === 'exodus' ? 'EXODUS' : endingTriggered === 'madness' ? 'MADNESS' : endingTriggered === 'collapse' ? 'COLLAPSE' : 'THE END'}

{endingTriggered === 'rescue' ? `After ${day} days of survival, the helicopter arrives. ${bunkerMembers.length} souls climb aboard, leaving the bunker behind. You made it. Against all odds, you made it.` : endingTriggered === 'sacrifice' ? `You gave your life to save the others. The reactor is stable. The bunker survives. They will remember you as a hero.` : endingTriggered === 'changed' ? `You embraced the change. Flesh twisted, minds merged. You are no longer human... but perhaps you are something more.` : endingTriggered === 'exodus' ? `With the horde approaching, you led your people into the wasteland. The bunker is lost, but hope survives.` : endingTriggered === 'madness' ? `The whispers became screams. Reality fractured. In the end, you couldn't tell friend from foe, truth from nightmare. The bunker still stands, but its people have lost themselves to the dark.` : endingTriggered === 'collapse' ? `The structure groaned, then screamed. Concrete crumbled. Steel twisted. The bunker that kept you safe became your tomb. Some made it out. Most didn't.` : `Your story ends here.`}

FINAL STATISTICS
Days Survived: {day}
Survivors: {bunkerMembers.length}
Karma Score: = 0 ? '#88cc88' : '#cc8888' }}>{karmaScore}
Sanity: 50 ? '#88cc88' : '#cc8888' }}>{Math.floor(sanityLevel)}%
)} {/* HEADER */}
THE BUNKER {texIdentity === 'tex' ? 'TEX SANTOS' : 'WALTER T. BANKS'}
DAY {day} {Math.floor((180 - gameTime) / 60)}:{String((180 - gameTime) % 60).padStart(2, '0')} POP: {bunkerMembers.length} {visitorQueue.length > 0 && ( 🚪 {visitorQueue.length + (currentVisitor ? 1 : 0)} waiting )}
{[1, 2, 3].map(speed => ( ))}
{detectedIncidents.length > 0 && ( )} {innerCircle.length > 0 && ( )} {knownFactions.length > 0 && ( )}
🧠 {Math.floor(sanityLevel)}%
{rescueCountdown && (
🚁 {rescueCountdown}d
)} {pendingAttack && (
⚔️ ATTACK: {pendingAttack.days}d
)}
{/* Tab Navigation */} {/* MAIN CONTENT */}
{/* LEFT SIDEBAR - Resources & Problems */} {/* CENTER - Main Area */}
{/* Current Visitor */} {currentVisitor && (

▸ VISITOR AT ENTRANCE

⏱️ {currentVisitor.patience}s patience

{currentVisitor.name}

Background: {currentVisitor.background.name}

{/* Personality badge */}
{currentVisitor.personality?.icon} {currentVisitor.personality?.name}

"{currentVisitor.backstory}"

{/* Condition bars */}
🍖 {Math.floor(currentVisitor.hunger)}%
💧 {Math.floor(currentVisitor.thirst)}%
😊 {Math.floor(currentVisitor.mood)}%
{currentVisitor.revealed && (
{currentVisitor.isThreat ? '⚠ THE MUSIC REVEALS DARKNESS...' : '✓ THEY SEEM GENUINE.'}
)} {/* Inner Circle Advice */} {innerCircle.length > 0 && (

INNER CIRCLE SAYS:

{getInnerCircleAdvice(currentVisitor.id).map((advice, i) => (

{advice.advisor}: {advice.text}

))}
)}
{/* Talk Panel */} {showTalkPanel && (
INTERROGATION - Speaking as {texIdentity === 'tex' ? 'TEX SANTOS' : 'WALTER BANKS'} {texIdentity === 'tex' ? '(Friendly)' : '(Formal)'}
{/* Conversation History */} {visitorDialogue.length > 0 && (
{visitorDialogue.map((entry, i) => (
{texIdentity === 'tex' ? 'Tex' : 'Banks'}: {entry.question}
{currentVisitor.name.split(' ')[0]}: {entry.response}
))}
)} {/* Question Options */}
{visitorQuestions.filter(q => !visitorDialogue.find(d => d.id === q.id)).slice(0, 4).map(q => ( ))} {visitorDialogue.length >= visitorQuestions.length && (
No more questions to ask.
)}
)} {showGuitar && (
)}
)} {/* Visitor Queue */} {visitorQueue.length > 0 && (

🚪 WAITING IN LINE ({visitorQueue.length})

{visitorQueue.slice(0, 6).map((v, i) => (
{v.patience}s
))} {visitorQueue.length > 6 && (
+{visitorQueue.length - 6}
)}
)} {/* TAB CONTENT */} {selectedTab === 'overview' && (
{/* Inner Circle */}

INNER CIRCLE ({innerCircle.length}/3)

{[0, 1, 2].map(i => { const member = bunkerMembers.find(m => m.id === innerCircle[i]); return (
member && setSelectedMember(member)} > {member ? ( <> {member.name.split(' ')[0]} ) : ( + )}
); })} {/* Call Meeting Button */}
{/* Quick Stats */}
TOTAL WORKERS
{bunkerMembers.filter(m => m.job).length}
UNEMPLOYED
{bunkerMembers.filter(m => !m.job).length}
SCAVENGERS
{bunkerMembers.filter(m => m.job === 'scavenger').length}
SECURITY
{bunkerMembers.filter(m => m.job === 'security').length}
)} {selectedTab === 'monitor' && (
{/* TV Monitor */}
{/* Screen */}
{/* Scanline effect */}
{/* Feed content */}
{/* Header */}
◉ REC {cameraFeeds.find(f => f.id === activeCameraFeed)?.name.toUpperCase()} CAM-{cameraFeeds.findIndex(f => f.id === activeCameraFeed) + 1}
{/* Feed scene */}
{/* Workers in this area */} {(() => { const feed = cameraFeeds.find(f => f.id === activeCameraFeed); const workers = feed?.jobFilter ? bunkerMembers.filter(m => m.job === feed.jobFilter) : activeCameraFeed === 'quarters' ? bunkerMembers.filter(m => !m.job) : activeCameraFeed === 'entrance' ? [] : bunkerMembers.slice(0, 4); if (activeCameraFeed === 'entrance') { return (
🚪
{visitorQueue.length > 0 ? ( <>
VISITORS DETECTED: {visitorQueue.length + (currentVisitor ? 1 : 0)}
{visitorQueue.slice(0, 4).map((v, i) => (
{v.personality.icon}
{v.name.split(' ')[0]}
⏱ {v.patience}s
))}
) : (
NO ACTIVITY DETECTED
)}
); } if (activeCameraFeed === 'wasteland') { const scavengers = bunkerMembers.filter(m => m.job === 'scavenger'); const region = scavengingRegions.find(r => r.id === scavengingRegion); return (
🏚️ ☢️ 🌵
LOCATION: {region?.name.toUpperCase()}
0.3 ? '#ff4444' : '#ffff00', marginBottom: '15px' }}> DANGER LEVEL: {Math.round((region?.danger || 0) * 100)}%
{scavengersDeployed ? ( <>
◉ LIVE FEED - {deployedScavengers.length} SCAVENGER(S) ACTIVE
{bunkerMembers.filter(m => deployedScavengers.includes(m.id)).map((s, i) => (
{['🚶', '🔍', '📦', '🏃'][Math.floor(gameTime / 2 + i) % 4]}
{s.name.split(' ')[0]}
Skill: {s.skills.scavenging.toFixed(1)}
))}
ETA: {Math.max(0, scavengerReturnTime - gameTime)}s
) : scavengers.length > 0 ? ( <>
⏸ STANDBY - {scavengers.length} SCAVENGER(S) READY
{scavengers.map((s, i) => (
🧍
{s.name.split(' ')[0]}
Skill: {s.skills.scavenging.toFixed(1)}
))}
Deploy from left panel
) : (
NO SIGNAL - NO SCAVENGERS ASSIGNED
)}
); } return (
{feed?.icon}
{workers.length > 0 ? ( <>
PERSONNEL: {workers.length}
{workers.slice(0, 6).map((w, i) => { const moodInfo = getMoodConsequence(w.mood, w.personality); const activity = w.job ? jobDefinitions[w.job]?.name : 'Idle'; return (
{w.personality.icon}
{w.name.split(' ')[0]}
{moodInfo.level.toUpperCase()}
{activity}
); })}
) : (
AREA EMPTY
)}
); })()}
{/* Footer */}
DAY {day} {cameraFeeds.find(f => f.id === activeCameraFeed)?.desc} {String(Math.floor(gameTime / 60)).padStart(2, '0')}:{String(gameTime % 60).padStart(2, '0')}
{/* TV controls */}
BUNKER SURVEILLANCE SYSTEM v2.1
{/* Camera selection */}
{cameraFeeds.map(feed => { const isActive = activeCameraFeed === feed.id; const workers = feed.jobFilter ? bunkerMembers.filter(m => m.job === feed.jobFilter) : feed.id === 'quarters' ? bunkerMembers.filter(m => !m.job) : feed.id === 'entrance' ? visitorQueue : []; return ( ); })}
)} {selectedTab === 'jobs' && (
{Object.entries(jobDefinitions).map(([jobId, job]) => (
{renderJobSlots(jobId)}
))}
)} {selectedTab === 'population' && (
{bunkerMembers.map(member => { const isDying = member.health < 30 || member.hunger < 20 || member.thirst < 20; return (
setSelectedMember(member)} style={{ padding: '12px', background: isDying ? 'rgba(80, 30, 30, 0.5)' : 'rgba(30, 25, 20, 0.5)', border: `1px solid ${isDying ? '#aa4444' : member.isInnerCircle ? '#c4a35a' : '#3a3530'}`, cursor: 'pointer', display: 'flex', gap: '10px', alignItems: 'center' }} >
{member.name}
{member.job ? jobDefinitions[member.job].icon : '❓'} {member.job ? jobDefinitions[member.job].name : 'No Job'}
{/* Mini status bars */}
❤️{Math.floor(member.health)} 🍖{Math.floor(member.hunger)} 💧{Math.floor(member.thirst)}
); })} {bunkerMembers.length === 0 && (

No one in the bunker yet. Admit survivors to build your community.

)}
)} {/* BUNKER TAB - Upgrades & Status */} {selectedTab === 'bunker' && (
{/* Bunker Status */}
SANITY LEVEL
50 ? '#aa88cc' : '#cc6688' }}>{Math.floor(sanityLevel)}%
50 ? '#aa88cc' : '#cc6688' }} />
STRUCTURE
50 ? '#aaaaaa' : '#cc8888' }}>{Math.floor(bunkerIntegrity)}%
50 ? '#888888' : '#cc6666' }} />
KARMA
= 0 ? '#88cc88' : '#cc8888' }}>{karmaScore > 0 ? '+' : ''}{karmaScore}
{karmaScore > 50 ? 'Saint' : karmaScore > 20 ? 'Good' : karmaScore > -20 ? 'Neutral' : karmaScore > -50 ? 'Questionable' : 'Ruthless'}
STORY PHASE
{storyPhase}/5
{storyPhase === 0 ? 'Beginning' : storyPhase === 1 ? 'Discovery' : storyPhase === 2 ? 'Secrets' : storyPhase === 3 ? 'Choices' : storyPhase === 4 ? 'Endgame' : 'Finale'}
{/* Active Countdowns */} {(rescueCountdown || pendingAttack || urgentEvents.length > 0) && (

⏰ ACTIVE COUNTDOWNS

{rescueCountdown && (
🚁 RESCUE ARRIVING: {rescueCountdown} days
)} {pendingAttack && (
⚔️ {pendingAttack.faction.toUpperCase()} ATTACK: {pendingAttack.days} days
)} {urgentEvents.map((event, i) => (
⚠️ {event.title}: {event.daysLeft} days left
))}
)} {/* Bunker Upgrades */}

🏗️ BUNKER UPGRADES

{bunkerUpgrades.map(upgrade => { const isBuilt = bunkerRooms[upgrade.id]; const canAfford = resources.food >= upgrade.cost.food && resources.water >= upgrade.cost.water; return (
{upgrade.icon}
{upgrade.name}
{isBuilt ? '✓ BUILT' : `Cost: ${upgrade.cost.food}🍖 ${upgrade.cost.water}💧`}

{upgrade.desc}

{!isBuilt && ( )}
); })}
{/* Surveillance List */} {watchList.length > 0 && (

👁️ UNDER SURVEILLANCE

{watchList.map(id => { const member = bunkerMembers.find(m => m.id === id); if (!member) return null; return (
setSelectedMember(member)} style={{ padding: '10px', background: 'rgba(80, 80, 120, 0.2)', border: '1px solid #5a5a8a', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: '10px' }} >
{member.name}
{member.isThreat ? '⚠️ Suspicious' : '✓ Clean so far'}
); })}
)}
)} {/* RELATIONS TAB */} {selectedTab === 'relations' && (
{/* Romances */} {romances.length > 0 && (

💕 ROMANCES

{romances.map((romance, i) => { const p1 = bunkerMembers.find(m => m.id === romance.person1); const p2 = bunkerMembers.find(m => m.id === romance.person2); if (!p1 || !p2) return null; return (
{p1.name.split(' ')[0]}
❤️
{p2.name.split(' ')[0]}
); })}
)} {/* Rivalries */} {rivalries.length > 0 && (

⚔️ RIVALRIES

{rivalries.map((rivalry, i) => { const p1 = bunkerMembers.find(m => m.id === rivalry.person1); const p2 = bunkerMembers.find(m => m.id === rivalry.person2); if (!p1 || !p2) return null; return (
{p1.name.split(' ')[0]}
⚔️
{p2.name.split(' ')[0]}
Intensity: {rivalry.intensity}
); })}
)} {/* Converted Members */} {convertedMembers.length > 0 && (

👁️ CONVERTED (COMPROMISED)

{convertedMembers.map(id => { const member = bunkerMembers.find(m => m.id === id); if (!member) return null; return (
setSelectedMember(member)} style={{ padding: '10px', background: 'rgba(100, 50, 100, 0.3)', border: '1px solid #6a3a6a', cursor: 'pointer' }} >
{member.name.split(' ')[0]}
); })}
)} {/* Missing Members */} {missingMembers.length > 0 && (

❓ MISSING

{missingMembers.map((member, i) => (
{member.name.split(' ')[0]}
Day {member.day}
))}
)} {/* All Relationships Matrix */}

📊 RELATIONSHIP MATRIX

{bunkerMembers.length > 1 ? (
{bunkerMembers.slice(0, 10).map(member => (
{member.name.split(' ')[0]}
{bunkerMembers.slice(0, 10).map(other => { if (member.id === other.id) return (
-
); const relKey = `${Math.min(member.id, other.id)}_${Math.max(member.id, other.id)}`; const rel = relationships[relKey] || 0; const color = rel > 50 ? '#88cc88' : rel > 20 ? '#aacc88' : rel > -20 ? '#888888' : rel > -50 ? '#ccaa88' : '#cc8888'; return (
{rel > 50 ? '💕' : rel > 20 ? '😊' : rel > -20 ? '😐' : rel > -50 ? '😠' : '💢'}
); })}
))}
{bunkerMembers.length > 10 && (

Showing first 10 members. Total: {bunkerMembers.length}

)}
) : (

Need at least 2 bunker members to track relationships.

)} {/* No relationships yet message */} {romances.length === 0 && rivalries.length === 0 && convertedMembers.length === 0 && missingMembers.length === 0 && (
🤝

No significant relationships yet.

Relationships develop over time as people interact.

)}
)} {/* RADIO TAB */} {selectedTab === 'radio' && (
{/* Radio Unit Display */}
{/* Radio Header */}
BUNKER RADIO SYSTEM
{/* Display Screen */}
{radioOn ? ( <> {/* Frequency Display */}
{radioFrequency.toFixed(1)} FM
{/* Station Name */}
{getCurrentStation()?.name || '-- NO SIGNAL --'}
{/* Scrolling Message */}
{radioMessage || '...scanning...'}
{/* Signal Strength */}
{[1,2,3,4,5].map(i => (
))}
) : (
-- RADIO OFF --
)}
{/* Controls */}
{/* Power Button */} {/* Frequency Dial */}
{ setRadioFrequency(parseFloat(e.target.value)); if (radioOn) { const msg = getRadioMessage(); setRadioMessage(msg.text); if (msg.type === 'static') playRadioStatic(); if (msg.type === 'music') playRadioMusic(); } }} disabled={!radioOn} style={{ width: '100%', cursor: radioOn ? 'pointer' : 'not-allowed' }} />
87 FM 108
{/* Scan Button */}
{/* Known Stations */}

📻 KNOWN FREQUENCIES

{radioStations.map(station => ( ))}
{/* Broadcasting */}

📡 BROADCAST (Requires Radio Booster)

{bunkerRooms.lab || discoveredLocations.includes('radio_boost') ? (

Broadcast to attract survivors or send messages to factions.

{isBroadcasting && (

Broadcasting... (resets at end of day)

)}
) : (

Build a Research Lab or find Radio Components to enable broadcasting.

)}
)} {/* MAP TAB */} {/* MORALE EVENTS TAB */} {selectedTab === 'morale' && (

🎉 MORALE EVENTS

Organize events to boost bunker morale. Each has a cooldown period.

{moraleEventOptions.map(event => { const onCooldown = moraleEventCooldowns[event.id] > 0; const canAfford = (!event.cost.food || resources.food >= event.cost.food) && (!event.cost.power || resources.power >= event.cost.power); const needsDeaths = event.requiresDeaths && memorialWall.length === 0; const disabled = onCooldown || !canAfford || needsDeaths; return (
{event.name}
{event.desc}
{event.cost.food ? `🍖 -${event.cost.food} ` : ''} {event.cost.power ? `⚡ -${event.cost.power} ` : ''} {!event.cost.food && !event.cost.power ? 'Free! ' : ''} → 😊 +{event.boost}
{onCooldown ? (
Cooldown: {moraleEventCooldowns[event.id]} day(s)
) : ( )}
); })}
{/* Found Notes Section */} {discoveredNotes.length > 0 && (

📜 FOUND NOTES ({discoveredNotes.length}/{foundNotes.length})

{discoveredNotes.map(noteId => { const note = foundNotes.find(n => n.id === noteId); return note ? ( ) : null; })}
)} {/* Confessions Section */} {Object.keys(confessionsRevealed).length > 0 && (

🤫 CONFESSIONS HEARD

{Object.entries(confessionsRevealed).map(([memberId, confId]) => { const member = bunkerMembers.find(m => m.id === parseInt(memberId)) || memorialWall.find(m => m.name); const conf = memberConfessions.find(c => c.id === confId); return conf ? (
{member?.name || 'Unknown'} — {conf.mood}
) : null; })}
)} {/* Voice Transmissions */} {voiceMessages.length > 0 && (

📻 THE VOICE — TRANSMISSIONS

{voiceMessages.map((msg, i) => (
Day {msg.day} — Stage {msg.stage}

"{msg.text}"

))}
)}
)} {/* DIPLOMACY TAB */} {selectedTab === 'diplomacy' && (

🏠 DIPLOMACY

{!rivalBunker.discovered ? (
📡

No other bunkers detected yet.

Keep your radio active to discover nearby settlements.

) : (
{/* Rival Bunker Info */}

🏠 {rivalBunkerData.name}

60 ? '#88cc88' : rivalBunker.attitude < 30 ? '#cc8888' : '#cccc88', fontSize: '12px' }}> {rivalBunker.alliance ? '🤝 ALLIED' : rivalBunker.attitude > 60 ? '😊 Friendly' : rivalBunker.attitude < 30 ? '😠 Hostile' : '😐 Neutral'}

Leader: {rivalBunkerData.leader}

Population: ~{rivalBunker.population}

Traits: {rivalBunkerData.traits.join(', ')}

{/* Attitude Bar */}
Attitude: {Math.floor(rivalBunker.attitude)}%
60 ? '#4a8a4a' : rivalBunker.attitude < 30 ? '#8a4a4a' : '#8a8a4a', transition: 'width 0.3s' }} />
{/* Actions */}

AVAILABLE ACTIONS

{rivalActions.map(action => { const canDo = rivalBunker.attitude >= action.attitudeReq || action.attitudeReq < 0; const canAfford = (!action.cost.food || resources.food >= action.cost.food) && (!action.cost.water || resources.water >= action.cost.water); const disabled = !canDo || !canAfford; const isHostile = action.attitudeChange < -20; return (
{action.name}
{action.desc}
{action.cost.food || action.cost.water ? (
Cost: {action.cost.food ? `🍖${action.cost.food} ` : ''}{action.cost.water ? `💧${action.cost.water}` : ''}
) : null} {!canDo && action.attitudeReq > 0 && (
Requires {action.attitudeReq}% attitude
)}
); })}
{/* Trade History */} {rivalBunker.tradeHistory.length > 0 && (

RECENT INTERACTIONS

{rivalBunker.tradeHistory.slice(-5).reverse().map((trade, i) => (
Day {trade.day}: {rivalActions.find(a => a.id === trade.action)?.name || trade.action}
))}
)}
)}
)} {selectedTab === 'map' && (() => { const selLoc = selectedLocation ? mapLocations.find(l => l.id === selectedLocation) : null; const isOnSite = scoutingTeam && scoutingTeam.status === 'on_site' && scoutingTeam.locationId === selectedLocation; const isTraveling = scoutingTeam && scoutingTeam.status === 'traveling'; const totalRooms = mapLocations.reduce((sum, l) => sum + l.rooms.length, 0); const searchedRooms = Object.values(locationLootCollected).reduce((sum, loc) => sum + Object.keys(loc).length, 0); return (

🗺️ WASTELAND MAP

Locations: {Object.keys(exploredMap).length}/{mapLocations.length} | Rooms: {searchedRooms}/{totalRooms} {scoutingTeam && 🚶 Team: {scoutingTeam.status === 'traveling' ? 'En route' : 'On site'} }
{/* LEFT - Map */}
{[80, 160, 240].map(r => (
))}
🏠
{scoutingTeam && (() => { const destLoc = mapLocations.find(l => l.id === scoutingTeam.locationId); if (!destLoc) return null; return ( ); })()} {mapLocations.map(loc => { const isExplored = !!exploredMap[loc.id]; const roomsSearched = locationLootCollected[loc.id] ? Object.keys(locationLootCollected[loc.id]).length : 0; const allSearched = roomsSearched === loc.rooms.length; const hasTeam = scoutingTeam && scoutingTeam.locationId === loc.id; const isSelected = selectedLocation === loc.id; return (
{ if (isExplored) setSelectedLocation(loc.id); }} style={{ position: 'absolute', left: loc.x + '%', top: loc.y + '%', transform: 'translate(-50%, -50%)', width: isSelected ? '34px' : '26px', height: isSelected ? '34px' : '26px', background: !isExplored ? '#1a1815' : allSearched ? '#2a3a2a' : '#2a2520', border: `2px solid ${isSelected ? '#c4a35a' : allSearched ? '#88cc88' : isExplored ? '#6a6050' : '#3a3530'}`, borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: isSelected ? '14px' : '11px', cursor: isExplored ? 'pointer' : 'default', opacity: isExplored ? 1 : 0.4, zIndex: isSelected ? 20 : 10, transition: 'all 0.15s', boxShadow: isSelected ? '0 0 12px rgba(196,163,90,0.3)' : 'none' }} title={isExplored ? `${loc.name} (${roomsSearched}/${loc.rooms.length} rooms)` : '???'} > {isExplored ? getLocIcon(loc.type) : '?'} {hasTeam &&
} {allSearched &&
{outposts[loc.id] ? '🏗️' : '✓'}
} {isExplored && !allSearched && roomsSearched > 0 && (
)}
); })}
{explorationLog.slice(-8).reverse().map((entry, i) => (
Day {entry.day}: {entry.location} - {entry.room} ({entry.result}) {entry.loot && [{entry.loot}]} {entry.member && [Lost: {entry.member}]}
))}
{/* RIGHT - Details */}
{!selLoc && !isTraveling && (
SELECT A DISCOVERED LOCATION OR SEND SCOUTS
{mapLocations.filter(l => !exploredMap[l.id]).map(loc => ( ))} {scoutingTeam &&
⚠️ Team already deployed
}
)} {isTraveling && !selLoc && (
🚶
Team en route
{scoutingTeam.memberNames.join(', ')}
Destination: {mapLocations.find(l => l.id === scoutingTeam.locationId)?.name || '???'}
Arriving: Day {scoutingTeam.returnDay} ({scoutingTeam.returnDay - day} day(s) remaining)
)} {selLoc && exploredMap[selLoc.id] && (
{getLocIcon(selLoc.type)} {selLoc.name}
Danger: {'█'.repeat(Math.ceil(selLoc.danger * 5))}{'░'.repeat(5 - Math.ceil(selLoc.danger * 5))}

{selLoc.desc}

{isOnSite && (
✅ Team on site: {scoutingTeam.memberNames.join(', ')}
)} {!scoutingTeam && ( )}
ROOMS — {locationLootCollected[selLoc.id] ? Object.keys(locationLootCollected[selLoc.id]).length : 0}/{selLoc.rooms.length} searched
{selLoc.rooms.map(room => { const searched = !!locationLootCollected[selLoc.id]?.[room.id]; const loot = locationLootCollected[selLoc.id]?.[room.id]; return (
{searched ? '✓ ' : ''}{room.name} {Math.round(room.danger * 100)}% danger
{!searched && isOnSite && ( )}
{!searched && (
{room.flavor}
)} {searched && loot && (
Found: {Object.entries(loot).filter(([k,v]) => v > 0).map(([k,v]) => `${v} ${k}`).join(', ') || 'nothing'}
)} {!searched && (
Expected: {Object.keys(room.loot).join(', ')}
)}
); })} {locationLootCollected[selLoc.id] && Object.keys(locationLootCollected[selLoc.id]).length === selLoc.rooms.length && (
{clearedLocations.includes(selLoc.id) && clearingRewards[selLoc.id] && (
⭐ {clearingRewards[selLoc.id].name}
{clearingRewards[selLoc.id].desc}
)} {outposts[selLoc.id] ? (
🏗️ {outposts[selLoc.id].name}
{outposts[selLoc.id].desc}
Producing daily: {Object.entries(outposts[selLoc.id].dailyYield).map(([k,v]) => `+${v} ${k}`).join(', ')}
) : (
BUILD OUTPOST
{outpostTypes.filter(o => o.type === 'any' || o.type === selLoc.type).map(opt => ( ))}
)}
)}
)}
); })()} {selectedTab === 'craft' && (
{/* Inventory */}

📦 INVENTORY

{Object.entries(inventory).map(([item, count]) => (
{item === 'scrap' ? '🔩' : item === 'chemicals' ? '🧪' : item === 'electronics' ? '💾' : item === 'cloth' ? '🧵' : item === 'herbs' ? '🌿' : '📦'}
{item}
0 ? '#c4a35a' : '#4a4540' }}>{count}
))}
{/* Recipes */}

🔨 CRAFTING RECIPES

{craftingRecipes.map(recipe => { const canCraft = Object.entries(recipe.materials).every(([mat, needed]) => inventory[mat] >= needed); return (
{recipe.icon}
{recipe.name}
Time: {recipe.time} day(s)
Materials: {Object.entries(recipe.materials).map(([mat, n]) => ( = n ? '#88cc88' : '#cc8888', marginLeft: '5px' }}> {n} {mat} ))}
); })}
{/* Crafting Queue */} {craftingQueue.length > 0 && (

⏳ IN PROGRESS

{craftingQueue.map((item, i) => (
{item.icon}
{item.name}
{item.daysLeft} day(s) left
))}
)}
)}
{/* RIGHT SIDEBAR - Daily Log */}
{/* Location Event Modal */} {locationEvent && (
EXPLORATION EVENT

{locationEvent.title}

{locationEvent.text}

{locationEvent.choices.map((choice, i) => ( ))}
)} {/* Travel Event Modal */} {travelEvent && (
EN ROUTE

{travelEvent.title}

{travelEvent.text}

{travelEvent.choices.map((choice, i) => ( ))}
)} {/* Clearing Reward Modal */} {showClearReward && (
LOCATION FULLY CLEARED

{showClearReward.name}

{showClearReward.desc}

)}
); }