const STORAGE_KEY = 'impostorGameStateV2'; const MAX_PLAYERS = 10; const MIN_PLAYERS = 3; const POOLS_CACHE_KEY = 'impostorWordPoolsV1'; const POOLS_MANIFEST_URL = 'word-pools/manifest.json'; const THEME_STORAGE_KEY = 'impostorGameTheme'; const LANGUAGE_STORAGE_KEY = 'impostorGameLanguage'; const SCREEN_LOCK_STORAGE_KEY = 'impostorGameScreenLock'; // ---------- Internationalization system ---------- const TRANSLATIONS = { es: { gameTitle: 'Juego del Impostor', gameSubtitle: '¿Podrás descubrir quién es el impostor?', play: 'Jugar', rules: 'Reglas', createdBy: 'Creado por Darío Sevilla', rulesTitle: 'Reglas del Juego', objective: 'Objetivo', objectiveText: 'Los civiles deben identificar a los impostores antes de que termine el tiempo.', preparation: 'Preparación', preparationSteps: ['Cada jugador recibe una palabra secreta', 'Los civiles reciben la misma palabra', 'Los impostores reciben una palabra diferente pero relacionada'], gameplay: 'Partida', gameplaySteps: ['Por turnos, cada jugador da un sinónimo o descripción de su palabra', 'Intenta ser específico pero no revelar tu palabra exacta', 'Los impostores deben intentar pasar desapercibidos'], voting: 'Votación', votingSteps: ['Tras el tiempo de juego y deliberación, vota en secreto', 'Los más votados son eliminados', 'Si todos los impostores son eliminados, ganan los civiles', 'Si queda algún impostor, ellos ganan'], back: 'Volver', configuration: 'Configuración', players: 'Jugadores', impostors: 'Impostores', gameTime: 'Tiempo de partida (seg)', deliberationTime: 'Deliberación (seg)', pools: 'Pools (toca para seleccionar)', next: 'Siguiente', playerNames: 'Nombres de jugadores', startGame: 'Comenzar partida', player: 'Jugador', readyToReveal: 'Listo para revelar', eachPlayerSecret: 'Cada jugador debe ver su rol en secreto. Desliza la cortina hacia arriba para revelar.', startReveal: 'Empezar revelación', revelation: 'Revelación', turnOf: 'Turno de', othersLookAway: 'Los demás, no miréis. Mantén levantada la cortina para ver tu rol.', liftCurtain: 'LEVANTA LA CORTINA', nextPlayer: 'Siguiente jugador', startMatch: '¡Iniciar partida!', gameInProgress: 'Partida en curso', giveSynonyms: 'A decir sinónimos!', skipToDeliberation: 'Saltar a deliberación', deliberation: 'Deliberación', lastArguments: 'Últimos argumentos antes de votar.', goToVoting: 'Ir a votación', secretVoting: 'Votación secreta', passMobileTo: 'Pasa el móvil a', chooseSuspects: 'Elige', suspect: 'sospechoso(s)', confirmVote: 'Confirmar voto', votes: 'Votos', results: 'Resultados', civiliansWin: '¡GANAN LOS CIVILES!', impostorsWin: '¡GANAN LOS IMPOSTORES!', executed: 'Ejecutados', nobody: 'Nadie', noVotes: 'Sin votos', revealedRoles: 'Roles revelados', newMatch: 'Nueva partida', civil: 'CIVIL', impostor: 'IMPOSTOR', civilians: 'civiles', poolsLabel: 'Pools', starts: 'Empieza', order: 'Orden', clockwise: 'Horario', counterclockwise: 'Antihorario', impostorsMustBeLess: 'Impostores debe ser menor que jugadores', animalsNature: 'Animales y Naturaleza', everydayObjects: 'Objetos Cotidianos', exitGame: 'Salir de la partida', poolsSelection: 'Selección de Pools', poolsSelectionText: 'Toca para seleccionar las categorías de palabras que quieres usar en la partida.' }, en: { gameTitle: 'The Impostor Game', gameSubtitle: 'Can you figure out who the impostor is?', play: 'Play', rules: 'Rules', createdBy: 'Created by Darío Sevilla', rulesTitle: 'Game Rules', objective: 'Objective', objectiveText: 'Civilians must identify the impostors before time runs out.', preparation: 'Setup', preparationSteps: ['Each player receives a secret word', 'Civilians receive the same word', 'Impostors receive a different but related word'], gameplay: 'Gameplay', gameplaySteps: ['Taking turns, each player gives a synonym or description of their word', 'Try to be specific but don\'t reveal your exact word', 'Impostors must try to blend in'], voting: 'Voting', votingSteps: ['After game time and deliberation, vote in secret', 'The most voted players are eliminated', 'If all impostors are eliminated, civilians win', 'If any impostor remains, they win'], back: 'Back', configuration: 'Setup', players: 'Players', impostors: 'Impostors', gameTime: 'Game time (sec)', deliberationTime: 'Deliberation (sec)', pools: 'Pools (tap to select)', next: 'Next', playerNames: 'Player names', startGame: 'Start game', player: 'Player', readyToReveal: 'Ready to reveal', eachPlayerSecret: 'Each player must see their role in secret. Swipe the curtain up to reveal.', startReveal: 'Start reveal', revelation: 'Revelation', turnOf: 'Turn of', othersLookAway: 'Others, look away. Keep the curtain lifted to see your role.', liftCurtain: 'LIFT THE CURTAIN', nextPlayer: 'Next player', startMatch: 'Start match!', gameInProgress: 'Game in progress', giveSynonyms: 'Give synonyms!', skipToDeliberation: 'Skip to deliberation', deliberation: 'Deliberation', lastArguments: 'Last arguments before voting.', goToVoting: 'Go to voting', secretVoting: 'Secret voting', passMobileTo: 'Pass the phone to', chooseSuspects: 'Choose', suspect: 'suspect(s)', confirmVote: 'Confirm vote', votes: 'Votes', results: 'Results', civiliansWin: 'CIVILIANS WIN!', impostorsWin: 'IMPOSTORS WIN!', executed: 'Executed', nobody: 'Nobody', noVotes: 'No votes', revealedRoles: 'Revealed roles', newMatch: 'New match', civil: 'CIVILIAN', impostor: 'IMPOSTOR', civilians: 'civilians', poolsLabel: 'Pools', starts: 'Starts', order: 'Order', clockwise: 'Clockwise', counterclockwise: 'Counterclockwise', impostorsMustBeLess: 'Impostors must be less than players', animalsNature: 'Animals and Nature', everydayObjects: 'Everyday Objects', exitGame: 'Exit Game', poolsSelection: 'Pool Selection', poolsSelectionText: 'Tap to select the word categories you want to use in the game.' } }; let currentLanguage = 'es'; function getBrowserLanguage() { const lang = navigator.language || navigator.userLanguage; return lang.startsWith('es') ? 'es' : 'en'; } function loadLanguage() { const saved = localStorage.getItem(LANGUAGE_STORAGE_KEY); return saved || getBrowserLanguage(); } function saveLanguage(lang) { localStorage.setItem(LANGUAGE_STORAGE_KEY, lang); } function t(key) { return TRANSLATIONS[currentLanguage][key] || key; } function setLanguage(lang) { currentLanguage = lang; saveLanguage(lang); document.documentElement.setAttribute('lang', lang); updateUI(); } function toggleLanguage() { const newLang = currentLanguage === 'es' ? 'en' : 'es'; setLanguage(newLang); } async function updateUI() { // Update language button const langText = document.querySelector('.language-text'); if (langText) { langText.textContent = currentLanguage.toUpperCase(); } // Update all static text elements updateStaticTexts(); // Reload pools for the new language (wait for it to complete) await loadPoolsList(); // Re-render dynamic content if in specific phases if (state.phase === 'names') { buildNameInputs(); } else if (state.phase === 'pre-reveal') { renderSummary(); } else if (state.phase === 'voting') { renderVoting(); } else if (state.phase === 'results') { showResults(); } } function updateStaticTexts() { // Welcome screen const welcomeTitle = document.querySelector('.welcome-title'); if (welcomeTitle) welcomeTitle.textContent = t('gameTitle'); const welcomeSubtitle = document.querySelector('.welcome-subtitle'); if (welcomeSubtitle) welcomeSubtitle.textContent = t('gameSubtitle'); const playBtn = document.querySelector('.btn-primary'); if (playBtn) playBtn.textContent = t('play'); const rulesBtn = document.querySelector('.btn-secondary'); if (rulesBtn) rulesBtn.textContent = t('rules'); const credits = document.querySelector('.welcome-credits'); if (credits) credits.textContent = t('createdBy'); // Rules screen const rulesTitle = document.querySelector('#rules-screen h1'); if (rulesTitle) rulesTitle.textContent = t('rulesTitle'); const ruleSections = document.querySelectorAll('.rule-section'); if (ruleSections.length >= 4) { ruleSections[0].querySelector('h3').textContent = t('objective'); ruleSections[0].querySelector('p').innerHTML = t('objectiveText'); ruleSections[1].querySelector('h3').textContent = t('preparation'); const prepSteps = t('preparationSteps'); ruleSections[1].querySelectorAll('p').forEach((p, i) => { if (prepSteps[i]) p.textContent = `${i + 1}. ${prepSteps[i]}`; }); ruleSections[2].querySelector('h3').textContent = t('gameplay'); const gameSteps = t('gameplaySteps'); ruleSections[2].querySelectorAll('p').forEach((p, i) => { if (gameSteps[i]) p.textContent = `${i + 1}. ${gameSteps[i]}`; }); ruleSections[3].querySelector('h3').textContent = t('voting'); const voteSteps = t('votingSteps'); ruleSections[3].querySelectorAll('p').forEach((p, i) => { if (voteSteps[i]) p.textContent = `${i + 1}. ${voteSteps[i]}`; }); } // Setup screen const setupTitle = document.querySelector('#setup-screen h1'); if (setupTitle) setupTitle.textContent = t('configuration'); const labels = { 'num-players': t('players'), 'num-impostors': t('impostors'), 'game-time': t('gameTime'), 'deliberation-time': t('deliberationTime') }; Object.entries(labels).forEach(([id, text]) => { const label = document.querySelector(`label[for="${id}"]`); if (label) label.textContent = text + ':'; }); // Pools screen const poolsTitle = document.querySelector('#pools-screen h1'); if (poolsTitle) poolsTitle.textContent = t('poolsSelection'); const poolsText = document.querySelector('#pools-screen .info-text'); if (poolsText) poolsText.textContent = t('poolsSelectionText'); // Names screen const namesTitle = document.querySelector('#names-screen h1'); if (namesTitle) namesTitle.textContent = t('playerNames'); // Pre-reveal screen const preRevealTitle = document.querySelector('#pre-reveal-screen h1'); if (preRevealTitle) preRevealTitle.textContent = t('readyToReveal'); const preRevealText = document.querySelector('#pre-reveal-screen .info-text:not(#config-summary)'); if (preRevealText) preRevealText.textContent = t('eachPlayerSecret'); // Reveal screen const revealTitle = document.querySelector('#reveal-screen h1'); if (revealTitle) revealTitle.textContent = t('revelation'); // Game screen const gameTitle = document.querySelector('#game-screen h1'); if (gameTitle) gameTitle.textContent = t('gameInProgress'); const gameText = document.querySelector('#game-screen .info-text'); if (gameText) gameText.textContent = t('giveSynonyms'); // Deliberation screen const delibTitle = document.querySelector('#deliberation-screen h1'); if (delibTitle) delibTitle.textContent = t('deliberation'); const delibText = document.querySelector('#deliberation-screen .info-text'); if (delibText) delibText.textContent = t('lastArguments'); // Voting screen const votingTitle = document.querySelector('#voting-screen h1'); if (votingTitle) votingTitle.textContent = t('secretVoting'); // Results screen const resultsTitle = document.querySelector('#results-screen h1'); if (resultsTitle) resultsTitle.textContent = t('results'); // Buttons const backButtons = document.querySelectorAll('button.ghost'); backButtons.forEach(btn => { if (btn.textContent.includes('Volver') || btn.textContent.includes('Back')) { btn.textContent = `← ${t('back')}`; } }); // Update all other buttons based on their onclick or content document.querySelectorAll('button').forEach(btn => { if (btn.getAttribute('onclick') === 'goToPools()') btn.textContent = t('next'); else if (btn.getAttribute('onclick') === 'goToNames()') btn.textContent = t('next'); else if (btn.getAttribute('onclick') === 'startGame()') btn.textContent = t('startGame'); else if (btn.getAttribute('onclick') === "showScreen('reveal-screen'); loadCurrentReveal();") btn.textContent = t('startReveal'); else if (btn.id === 'next-player-btn') btn.textContent = t('nextPlayer') + ' →'; else if (btn.id === 'start-game-btn') btn.textContent = t('startMatch'); else if (btn.getAttribute('onclick') === 'skipToDeliberation()') btn.textContent = t('skipToDeliberation') + ' →'; else if (btn.getAttribute('onclick') === 'skipToVoting()') btn.textContent = t('goToVoting') + ' →'; else if (btn.id === 'confirm-vote-btn') btn.textContent = t('confirmVote'); else if (btn.getAttribute('onclick') === 'newMatch()') btn.textContent = t('newMatch'); }); // Exit game button const exitText = document.querySelector('.exit-text'); if (exitText) exitText.textContent = t('exitGame'); } // Embedded pools with impostor words [civilian_word, impostor_word] const EMBEDDED_POOLS = [ // Spanish pools { id: 'animales_naturaleza', name: 'Animales y Naturaleza', emoji: '🌿', lang: 'es', words: [['Oso','Pez'],['Pavo real','Abanico'],['Camello','Arena'],['Lirio','Rana'],['Lobo','Luna'],['Represa','Castor'],['Elefante','Safari'],['Flamenco','Camarón'],['Búho','Nieve'],['Canguro','Koala'],['Jungla','Serpiente'],['Muerte','Cuervo'],['Delfín','Orca'],['Zorro','Gallina'],['Tortuga','Galápagos'],['León','Sabana'],['Polo Sur','Pingüino'],['Hormiga','Trabajo'],['Abeja','Verano'],['Ballena','Dory'],['Mandíbula','Tiburón'],['Río de Janeiro','Loro'],['Caballo','Libertad'],['Gorila','Plata'],['Murciélago','Fruta'],['Venado','Tambor'],['Misisipi','Águila'],['Cisne','Lago'],['Grillo','Campo'],['Leopardo','Manchas'],['Mascarilla','Mapache'],['Chita','Velocidad'],['Araña','Nueva York'],['Playa','Medusa'],['Glaciar','Oso polar'],['Jirafa','Madagascar'],['Maine','Langosta'],['Pulpo','Pluma'],['Cuervo','Pantera'],['Foca','Rosa'],['Mariposa','Algodoncillo'],['Burro','Santorini'],['Lluvia','Caracol'],['Cangrejo','Araña'],['Rana','Grillo'],['Siberia','Tigre'],['Gaviota','Playa'],['Cocodrilo','Nilo'],['Pingüino','Nueva Zelanda'],['Loro','Gato'],['Cuervo','Bandada'],['Conejo','Agujero'],['Tiburón','Paleozoico'],['Trueno','Júpiter'],['Sol','Playa'],['Océano Atlántico','Huracán'],['Tsunami','Derrumbe'],['Ola','Hawái'],['Papel','Árbol'],['Universo','Energía'],['Vida','Tiempo'],['Océano','Tormenta'],['Lago','Sal'],['Oxígeno','Fuego'],['Biología','Célula'],['Tiza','Hielo'],['Clima','Invierno'],['Planeta','Gas'],['Era de hielo','Bellota'],['Avalancha','Montaña'],['Bisonte','Llanuras'],['Floración','Néctar'],['Cañón','Águila'],['Ardilla listada','Nueces'],['Coral','Arrecife'],['Desierto','Espejismo'],['Ecosistema','Equilibrio'],['Halcón','Picado'],['Luciérnaga','Brillo'],['Gecko','Hoja'],['Colibrí','Azúcar'],['Koala','Eucalipto'],['Meteoro','Cráter'],['Nutria','Río'],['Selva tropical','Dosel'],['Rinoceronte','Cuerno'],['Volcán','Ceniza'],['Naturaleza salvaje','Huellas']] }, { id: 'objetos_cotidianos', name: 'Objetos Cotidianos', emoji: '🏠', lang: 'es', words: [['Martillo','Tiburón'],['Silla','Espalda'],['Mesa','Café'],['Cuchara','Crema'],['Tenedor','Posidón'],['Cuchillo','Mantequilla'],['Plata','Plato'],['Copa','Campeonato'],['Vidrio','Arena'],['Botella','Aerosol'],['Lata','Boda'],['Teléfono','Radio'],['Laptop','Tarjeta'],['Teclado','Piano'],['Ratón','Laboratorio'],['Marco','Pantalla'],['Control','Satélite'],['Lámpara','Aceite'],['Horno','Bombilla'],['Vela','Corona de flores'],['Carro','Espejo'],['Ventana','Caja'],['Puerta','Armario'],['Llave','Auto'],['Candado','Sello'],['Monedero','Piel'],['Cartera','Etiqueta'],['Mochila','Avión'],['Maleta','Toalla'],['Sombrero','Paja'],['Zapatos','Vela'],['Calcetas','Medida'],['Playera','Algodón'],['Cierre','Pantalón'],['Abrigo','Pelo'],['Paraguas','Ala'],['Vacaciones','Gafas de sol'],['Reloj de pulsera','Monitor'],['Rueda','Anillo'],['Collar','Tiara'],['Manga','Tatuaje'],['Cama','Monstruo'],['Funda','Media'],['Manta','Cuna'],['Colchón','Aire'],['Libro','Pop-up'],['Revista','Diario'],['Periódico','Columna'],['Pluma','Bola'],['Lápiz','Delineador'],['Borrador','Goma'],['Dibujo','Cuaderno'],['Tijeras','Cabello'],['Regla','Parrilla'],['Pegamento','Tubo'],['Cinta adhesiva','Clip'],['Pincel','Escoba'],['Cesto','Arco'],['Caja','Zapato'],['Sobre','Carta'],['Sello','Fecha'],['Calendario','Luna'],['Reloj','Campana'],['Radio','Onda'],['Bocina','Pared'],['DJ','Audífonos'],['Micrófono','Televisión'],['Televisión','Imagen'],['Cámara','Láser'],['Trípode','Pierna'],['Ventilador','Oxígeno'],['Calefactor','Secadora'],['Estufa','Carbón'],['Refrigerador','Leche'],['Congelador','Helado'],['Microondas','Radar'],['Tostadora','Horno'],['Licuadora','Espátula'],['Olla','Sopa'],['Acero','Sartén'],['Tetera','Cobre'],['Esponja','Gelatina'],['Jabón','Barra'],['Toalla','Ducha'],['Cepillo de dientes','Lengua'],['Pasta de dientes','Gel'],['Marfil','Peine'],['Cepillo','Rastrillo'],['Navaja','Jabón'],['Champú','Sábila'],['Acondicionador','Espuma'],['Loción','Seda'],['Balde','Tierra'],['Trapeador','Piso'],['Escoba','Avión'],['Recogedor','Nube'],['Basurero','Camión'],['Reciclaje','Papel'],['Escalera','Cuerda']] }, // English pools { id: 'animals_nature_en', name: 'Animals and Nature', emoji: '🌿', lang: 'en', words: [['Bear','Fish'],['Peacock','Fan'],['Camel','Sand'],['Lily','Frog'],['Wolf','Moon'],['Dam','Beaver'],['Elephant','Safari'],['Flamingo','Shrimp'],['Owl','Snow'],['Kangaroo','Koala'],['Jungle','Snake'],['Death','Crow'],['Dolphin','Orca'],['Fox','Chicken'],['Turtle','Galapagos'],['Lion','Savanna'],['South Pole','Penguin'],['Ant','Work'],['Bee','Summer'],['Whale','Dory'],['Jaw','Shark'],['Rio','Parrot'],['Horse','Freedom'],['Gorilla','Silver'],['Bat','Fruit'],['Deer','Drum'],['Mississippi','Eagle'],['Swan','Lake'],['Cricket','Field'],['Leopard','Spots'],['Mask','Raccoon'],['Cheetah','Speed'],['Spider','New York'],['Beach','Jellyfish'],['Glacier','Polar bear'],['Giraffe','Madagascar'],['Maine','Lobster'],['Octopus','Feather'],['Raven','Panther'],['Seal','Rose'],['Butterfly','Milkweed'],['Donkey','Santorini'],['Rain','Snail'],['Crab','Spider'],['Frog','Cricket'],['Siberia','Tiger'],['Seagull','Beach'],['Crocodile','Nile'],['Penguin','New Zealand'],['Parrot','Cat'],['Crow','Flock'],['Rabbit','Hole'],['Shark','Paleozoic'],['Thunder','Jupiter'],['Sun','Beach'],['Atlantic','Hurricane'],['Tsunami','Landslide'],['Wave','Hawaii'],['Paper','Tree'],['Universe','Energy'],['Life','Time'],['Ocean','Storm'],['Lake','Salt'],['Oxygen','Fire'],['Biology','Cell'],['Chalk','Ice'],['Climate','Winter'],['Planet','Gas'],['Ice age','Acorn'],['Avalanche','Mountain'],['Bison','Plains'],['Bloom','Nectar'],['Canyon','Eagle'],['Chipmunk','Nuts'],['Coral','Reef'],['Desert','Mirage'],['Ecosystem','Balance'],['Falcon','Dive'],['Firefly','Glow'],['Gecko','Leaf'],['Hummingbird','Sugar'],['Koala','Eucalyptus'],['Meteor','Crater'],['Otter','River'],['Rainforest','Canopy'],['Rhino','Horn'],['Volcano','Ash'],['Wilderness','Tracks']] }, { id: 'everyday_objects_en', name: 'Everyday Objects', emoji: '🏠', lang: 'en', words: [['Hammer','Shark'],['Chair','Back'],['Table','Coffee'],['Spoon','Cream'],['Fork','Poseidon'],['Knife','Butter'],['Silver','Plate'],['Cup','Championship'],['Glass','Sand'],['Bottle','Spray'],['Can','Wedding'],['Phone','Radio'],['Laptop','Card'],['Keyboard','Piano'],['Mouse','Lab'],['Frame','Screen'],['Remote','Satellite'],['Lamp','Oil'],['Oven','Bulb'],['Candle','Wreath'],['Car','Mirror'],['Window','Box'],['Door','Closet'],['Key','Car'],['Lock','Seal'],['Wallet','Leather'],['Purse','Tag'],['Backpack','Airplane'],['Suitcase','Towel'],['Hat','Straw'],['Shoes','Sail'],['Socks','Measure'],['Shirt','Cotton'],['Zipper','Pants'],['Coat','Hair'],['Umbrella','Wing'],['Vacation','Sunglasses'],['Watch','Monitor'],['Wheel','Ring'],['Necklace','Tiara'],['Sleeve','Tattoo'],['Bed','Monster'],['Pillowcase','Stocking'],['Blanket','Cradle'],['Mattress','Air'],['Book','Pop-up'],['Magazine','Journal'],['Newspaper','Column'],['Pen','Ball'],['Pencil','Eyeliner'],['Eraser','Rubber'],['Notebook','Drawing'],['Scissors','Hair'],['Ruler','Grill'],['Glue','Tube'],['Tape','Clip'],['Brush','Broom'],['Basket','Arc'],['Box','Shoe'],['Envelope','Letter'],['Stamp','Date'],['Calendar','Moon'],['Clock','Bell'],['Radio','Wave'],['Speaker','Wall'],['DJ','Headphones'],['Microphone','TV'],['Television','Picture'],['Camera','Laser'],['Tripod','Leg'],['Fan','Oxygen'],['Heater','Dryer'],['Stove','Coal'],['Fridge','Milk'],['Freezer','Ice cream'],['Microwave','Radar'],['Toaster','Oven'],['Blender','Spatula'],['Pot','Soup'],['Pan','Steel'],['Kettle','Copper'],['Sponge','Jelly'],['Soap','Bar'],['Towel','Shower'],['Toothbrush','Tongue'],['Toothpaste','Gel'],['Comb','Ivory'],['Brush','Rake'],['Razor','Soap'],['Shampoo','Aloe'],['Conditioner','Foam'],['Lotion','Silk'],['Bucket','Earth'],['Mop','Floor'],['Broom','Airplane'],['Dustpan','Cloud'],['Trash can','Truck'],['Recycling','Paper'],['Ladder','Rope']] } ]; let availablePools = []; let poolsCache = {}; let state = { phase: 'setup', numPlayers: 6, numImpostors: 1, gameTime: 180, deliberationTime: 60, playerNames: [], roles: [], civilianWord: '', impostorWord: '', currentReveal: 0, startPlayer: 0, turnDirection: 'horario', revealOrder: [], timerEndAt: null, timerPhase: null, votes: {}, votingPlayer: 0, selections: [], executed: [], selectedPools: [], // Now it's an array for multiple pools, will be populated based on language votingPool: null, isTiebreak: false, tiebreakCandidates: [] }; const saveState = () => localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); const loadState = () => { const raw = localStorage.getItem(STORAGE_KEY); if (!raw) return false; try { state = JSON.parse(raw); return true; } catch { return false; } }; const clearState = () => localStorage.removeItem(STORAGE_KEY); const loadPoolsCache = () => { try { poolsCache = JSON.parse(localStorage.getItem(POOLS_CACHE_KEY) || '{}'); } catch { poolsCache = {}; } }; const savePoolsCache = () => localStorage.setItem(POOLS_CACHE_KEY, JSON.stringify(poolsCache)); // ---------- Default values ---------- function defaultImpostors(nPlayers) { const capped = Math.min(Math.max(nPlayers, MIN_PLAYERS), MAX_PLAYERS); let impostors = 1; if (capped > 7) impostors = 3; else if (capped > 5) impostors = 2; const halfCap = Math.max(1, Math.floor(capped / 2)); return Math.min(impostors, halfCap); } function defaultGameTime(nPlayers) { const capped = Math.min(Math.max(nPlayers, MIN_PLAYERS), MAX_PLAYERS); if (capped <= 4) return 300; if (capped >= 10) return 900; const extraPlayers = capped - 4; const seconds = 300 + extraPlayers * 100; return Math.round(seconds / 30) * 30; } function defaultDeliberation(gameSeconds) { return Math.max(30, Math.round(gameSeconds / 3)); } // ---------- Word Pools ---------- async function loadPoolsList() { loadPoolsCache(); // Start with embedded pools (always available) let embeddedList = EMBEDDED_POOLS.map(p => ({ id: p.id, name: p.name, emoji: p.emoji, count: p.words.length, lang: p.lang })); // Try to load external pools from manifest let externalList = []; try { const res = await fetch(POOLS_MANIFEST_URL); if (res.ok) { const manifest = await res.json(); if (Array.isArray(manifest)) { externalList = manifest; } } } catch (e) { console.log('Failed to load manifest:', e); } // Combine pools, avoiding duplicates (prefer embedded version over manifest) const embeddedIds = new Set(embeddedList.map(p => p.id)); const uniqueExternal = externalList.filter(p => !embeddedIds.has(p.id)); const allPools = [...embeddedList, ...uniqueExternal]; // Filter pools by current language (only show pools matching current language) availablePools = allPools.filter(p => p.lang === currentLanguage); // Check if selected pools are valid for current language const validSelectedPools = (state.selectedPools || []).filter(id => availablePools.some(p => p.id === id) ); // If no valid pools or pools don't match current language, reset to defaults if (validSelectedPools.length === 0) { const defaultPools = availablePools.slice(0, 2).map(p => p.id); state.selectedPools = defaultPools.length > 0 ? defaultPools : []; saveState(); } else { state.selectedPools = validSelectedPools; saveState(); } renderPoolButtons(); } function parseWordsFile(text) { const lines = text.split(/\r?\n/).map(l => l.trim()).filter(l => l && !l.startsWith('#')); return lines.map(line => { // Format: civilian_word|impostor_word if (line.includes('|')) { const [civil, impostor] = line.split('|').map(s => s.trim()); return [civil, impostor]; } // Fallback: if no pipe, use the same word for both return [line, line]; }); } async function pickWords() { const selectedIds = state.selectedPools && state.selectedPools.length > 0 ? state.selectedPools : ['animales_naturaleza']; let allWords = []; // Collect words from all selected pools for (const poolId of selectedIds) { let words = []; // Search embedded pools first const embeddedPool = EMBEDDED_POOLS.find(p => p.id === poolId); if (embeddedPool) { words = embeddedPool.words; } else if (poolsCache[poolId]?.words) { words = poolsCache[poolId].words; } else { try { const res = await fetch(`word-pools/${poolId}.txt`); if (res.ok) { const text = await res.text(); words = parseWordsFile(text); poolsCache[poolId] = { words, ts: Date.now() }; savePoolsCache(); } } catch (_) {} } allWords = allWords.concat(words); } if (allWords.length === 0) { // Fallback to embedded pool allWords = EMBEDDED_POOLS[0].words; } const shuffled = [...allWords].sort(() => Math.random() - 0.5); const wordPair = shuffled[0]; // wordPair is [civilian_word, impostor_word] return { civilian: wordPair[0], impostor: wordPair[1] }; } function renderPoolButtons() { const container = document.getElementById('pool-buttons'); if (!container) return; container.innerHTML = ''; // Ensure selectedPools is an array and contains valid pools for current language if (!Array.isArray(state.selectedPools)) { state.selectedPools = [state.selectedPools || 'animales_naturaleza']; } // Filter out pools that don't exist in current language const validSelectedPools = state.selectedPools.filter(id => availablePools.some(p => p.id === id) ); // If no valid pools selected, select first 2 available if (validSelectedPools.length === 0 && availablePools.length > 0) { state.selectedPools = availablePools.slice(0, 2).map(p => p.id); saveState(); } else { state.selectedPools = validSelectedPools; } availablePools.forEach(pool => { const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'pool-btn'; btn.textContent = `${pool.emoji || '🎲'} ${pool.name || pool.id}`; if (state.selectedPools.includes(pool.id)) btn.classList.add('selected'); btn.onclick = () => { // Toggle multiple selection if (state.selectedPools.includes(pool.id)) { state.selectedPools = state.selectedPools.filter(id => id !== pool.id); // Ensure at least one is selected if (state.selectedPools.length === 0) { state.selectedPools = [pool.id]; } } else { state.selectedPools.push(pool.id); } saveState(); renderPoolButtons(); }; container.appendChild(btn); }); } // ---------- Setup and player names ---------- function goToPools() { let nPlayers = parseInt(document.getElementById('num-players').value) || MIN_PLAYERS; nPlayers = Math.min(Math.max(nPlayers, MIN_PLAYERS), MAX_PLAYERS); const maxImpostors = Math.max(1, Math.floor(nPlayers / 2)); let nImpostors = parseInt(document.getElementById('num-impostors').value) || defaultImpostors(nPlayers); nImpostors = Math.min(Math.max(1, nImpostors), maxImpostors); let gTime = parseInt(document.getElementById('game-time').value) || defaultGameTime(nPlayers); gTime = Math.min(Math.max(gTime, 60), 900); let dTime = parseInt(document.getElementById('deliberation-time').value) || defaultDeliberation(gTime); dTime = Math.min(Math.max(dTime, 30), Math.round(900 / 3)); if (nImpostors >= nPlayers) { alert(t('impostorsMustBeLess')); return; } state.numPlayers = nPlayers; state.numImpostors = nImpostors; state.gameTime = gTime; state.deliberationTime = dTime; showScreen('pools-screen'); } function goToNames() { buildNameInputs(); showScreen('names-screen'); } function buildNameInputs() { const list = document.getElementById('player-names-list'); list.innerHTML = ''; for (let i = 0; i < state.numPlayers; i++) { const div = document.createElement('div'); div.className = 'player-name-item'; const playerLabel = `${t('player')} ${i+1}`; div.innerHTML = `${playerLabel}:`; list.appendChild(div); } } // ---------- Game start ---------- function startGame() { state.playerNames = []; for (let i = 0; i < state.numPlayers; i++) { const val = document.getElementById(`player-name-${i}`).value.trim(); state.playerNames.push(val || `Jugador ${i+1}`); } pickWords().then(({civilian, impostor}) => { state.civilianWord = civilian; state.impostorWord = impostor; finalizeStart(); }).catch(() => { const fallback = EMBEDDED_POOLS[0].words; const shuffled = [...fallback].sort(() => Math.random() - 0.5); const wordPair = shuffled[0]; state.civilianWord = wordPair[0]; state.impostorWord = wordPair[1]; finalizeStart(); }); } function finalizeStart() { state.roles = Array(state.numPlayers - state.numImpostors).fill('CIVIL').concat(Array(state.numImpostors).fill('IMPOSTOR')).sort(() => Math.random()-0.5); state.startPlayer = Math.floor(Math.random() * state.numPlayers); state.turnDirection = Math.random() < 0.5 ? 'horario' : 'antihorario'; const step = state.turnDirection === 'horario' ? 1 : -1; state.revealOrder = Array.from({length: state.numPlayers}, (_, k) => (state.startPlayer + step * k + state.numPlayers) % state.numPlayers); state.currentReveal = 0; state.phase = 'pre-reveal'; state.votes = {}; state.votingPlayer = 0; state.selections = []; state.executed = []; state.timerEndAt = null; state.timerPhase = null; state.votingPool = null; state.isTiebreak = false; state.tiebreakCandidates = []; saveState(); renderSummary(); showScreen('pre-reveal-screen'); } // Adjust defaults when player count is edited document.getElementById('num-players').addEventListener('change', () => { let nPlayers = parseInt(document.getElementById('num-players').value) || MIN_PLAYERS; nPlayers = Math.min(Math.max(nPlayers, MIN_PLAYERS), MAX_PLAYERS); document.getElementById('num-players').value = nPlayers; const imp = defaultImpostors(nPlayers); const gTime = defaultGameTime(nPlayers); const dTime = defaultDeliberation(gTime); document.getElementById('num-impostors').max = Math.max(1, Math.floor(nPlayers / 2)); document.getElementById('num-impostors').value = imp; document.getElementById('game-time').value = gTime; document.getElementById('deliberation-time').value = dTime; }); function renderSummary() { const el = document.getElementById('config-summary'); const fmt = secs => `${Math.floor(secs/60)}:${(secs%60).toString().padStart(2,'0')}`; const startName = state.playerNames[state.startPlayer] || `${t('player')} ${state.startPlayer+1}`; // Generate list of selected pools const selectedIds = Array.isArray(state.selectedPools) ? state.selectedPools : [state.selectedPools || 'animales_naturaleza']; const poolsText = selectedIds.map(id => { const pool = availablePools.find(p => p.id === id) || EMBEDDED_POOLS.find(p => p.id === id); return pool ? `${pool.emoji || '🎲'} ${pool.name || pool.id}` : id; }).join(', '); el.innerHTML = `
${t('players')}: ${state.numPlayers}
${t('impostors')}: ${state.numImpostors}
${t('gameTime')}: ${fmt(state.gameTime)}
${t('deliberationTime')}: ${fmt(state.deliberationTime)}
${t('poolsLabel')}: ${poolsText}
${t('starts')}: ${startName} · ${t('order')}: ${state.turnDirection === 'horario' ? t('clockwise') : t('counterclockwise')}
`; } // ---------- Role revelation ---------- function loadCurrentReveal() { state.phase = 'reveal'; saveState(); // Activar Wake Lock para mantener pantalla encendida durante el juego requestWakeLock(); if (!state.revealOrder || state.revealOrder.length !== state.numPlayers) { const step = state.turnDirection === 'horario' ? 1 : -1; state.revealOrder = Array.from({length: state.numPlayers}, (_, k) => (state.startPlayer + step * k + state.numPlayers) % state.numPlayers); } const idx = state.revealOrder[state.currentReveal]; const name = state.playerNames[idx]; document.getElementById('current-player-name').textContent = name; // Update curtain text const revealText = document.querySelector('#reveal-screen .info-text'); if (revealText) { revealText.innerHTML = `${t('turnOf')}: ${name}${t('executed')}: ${executed.length ? executed.map(i => state.playerNames[i]).join(', ') : t('nobody')}
${t('votes')}: ${Object.keys(state.votes).length ? '' : t('noVotes')}