From 5bfb857f41ba5daa54be4b31eaaa7e34c0ea38a4 Mon Sep 17 00:00:00 2001 From: Dasemu Date: Wed, 14 Jan 2026 13:24:28 +0100 Subject: [PATCH 1/9] feat(styles): remove unused button styles and optimize CSS for better performance --- script.js | 1079 ++++------------------------------------- styles.css | 1374 ---------------------------------------------------- 2 files changed, 100 insertions(+), 2353 deletions(-) diff --git a/script.js b/script.js index 05c0b74..8e41f46 100644 --- a/script.js +++ b/script.js @@ -3,352 +3,14 @@ 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']] } + { id: 'animales_naturaleza', name: 'Animales y naturaleza', emoji: '🌿', words: ['Perro','Gato','Lobo','Zorro','Oso','Tigre','León','Pantera','Jaguar','Puma','Guepardo','Elefante','Rinoceronte','Hipopótamo','Jirafa','Cebra','Camello','Dromedario','Canguro','Koala','Panda','Mapache','Nutria','Castor','Foca','Morsa','Delfín','Ballena','Tiburón','Orca','Pulpo','Calamar','Medusa','Tortuga','Lagarto','Cocodrilo','Serpiente','Anaconda','Iguana','Rana','Sapo','Búho','Halcón','Águila','Cóndor','Gaviota','Loro','Flamenco','Pingüino','Avestruz','Gallina','Pato','Ganso','Cisne','Abeja','Hormiga','Mariquita','Libélula','Mariposa','Escarabajo','Grillo','Saltamontes','Araña','Escorpión','Lombriz','Caracol','Estrella de mar','Coral','Musgo','Helecho','Pino','Roble','Encina','Palmera','Cactus','Bambú','Rosa','Tulipán','Girasol','Lavanda','Montaña','Río','Lago','Mar','Playa','Desierto','Selva','Bosque','Pradera','Glaciar','Volcán'] }, + { id: 'vida_cotidiana', name: 'Vida cotidiana', emoji: '🏠', words: ['Pan','Leche','Café','Té','Agua','Jugo','Refresco','Cerveza','Vino','Pizza','Hamburguesa','Sándwich','Taco','Burrito','Pasta','Arroz','Paella','Sushi','Ramen','Ensalada','Sopa','Croqueta','Tortilla','Empanada','Arepa','Queso','Jamón','Chorizo','Pollo','Carne','Cerdo','Pescado','Marisco','Patata','Tomate','Cebolla','Ajo','Pimiento','Zanahoria','Lechuga','Brócoli','Coliflor','Manzana','Plátano','Naranja','Pera','Uva','Fresa','Mango','Piña','Melón','Sandía','Yogur','Galletas','Chocolate','Helado','Cereales','Mantequilla','Aceite','Sal','Pimienta','Azúcar','Harina','Huevo','Cuchara','Tenedor','Cuchillo','Plato','Vaso','Taza','Olla','Sartén','Microondas','Horno','Nevera','Mesa','Silla','Sofá','Cama','Almohada','Sábana','Toalla','Ducha','Jabón','Champú','Cepillo','Pasta de dientes'] }, + { id: 'deportes', name: 'Deportes', emoji: '🏅', words: ['Fútbol','Baloncesto','Tenis','Pádel','Bádminton','Voleibol','Béisbol','Rugby','Hockey hielo','Hockey césped','Golf','Boxeo','MMA','Judo','Karate','Taekwondo','Esgrima','Tiro con arco','Halterofilia','Crossfit','Atletismo','Maratón','Triatlón','Ciclismo ruta','Ciclismo montaña','BMX','Natación','Waterpolo','Surf','Vela','Remo','Piragüismo','Esquí','Snowboard','Patinaje artístico','Patinaje velocidad','Curling','Escalada','Senderismo','Trail running','Parkour','Gimnasia artística','Gimnasia rítmica','Trampolín','Skate','Breakdance','Carreras coches','Fórmula 1','Rally','Karting','Motociclismo','Enduro','Motocross','Equitación','Polo','Críquet','Billar','Dardos','Petanca','Pickleball','Ultimate frisbee','Paintball','Airsoft','eSports'] }, + { id: 'marcas', name: 'Marcas', emoji: '🛍️', words: ['Apple','Samsung','Google','Microsoft','Amazon','Meta','Tesla','Toyota','Honda','Ford','BMW','Mercedes','Audi','Volkswagen','Porsche','Ferrari','Lamborghini','Maserati','McLaren','Chevrolet','Nissan','Kia','Hyundai','Peugeot','Renault','Volvo','Jaguar','Land Rover','Fiat','Alfa Romeo','Ducati','Yamaha','Canon','Nikon','Sony','Panasonic','LG','Philips','Siemens','Bosch','Whirlpool','Ikea','Zara','H&M','Uniqlo','Nike','Adidas','Puma','Reebok','New Balance','Under Armour','Converse','Vans','Patagonia','The North Face','Columbia','Levi’s','Calvin Klein','Gucci','Prada','Louis Vuitton','Chanel','Hermès','Dior','Rolex','Omega','Casio','Pepsi','Coca-Cola','Fanta','Red Bull','Monster','Starbucks','Nespresso','Nestlé','Danone','Kellogg’s','Oreo','Intel','AMD','Nvidia','Qualcomm','TikTok','Netflix','Disney','Warner Bros','HBO','Spotify','Airbnb','Uber','Booking'] }, + { id: 'musica', name: 'Música', emoji: '🎵', words: ['Guitarra','Piano','Violín','Batería','Bajo','Saxofón','Trompeta','Flauta','Clarinete','Acordeón','Ukelele','Arpa','Sintetizador','DJ','Micrófono','Altavoz','Concierto','Festival','Vinilo','Rock','Pop','Punk','Metal','Heavy','Thrash','Death metal','Jazz','Blues','Soul','Funk','R&B','Rap','Hip hop','Trap','Reggaetón','Salsa','Bachata','Merengue','Cumbia','Vallenato','Flamenco','Rumba','Bossa nova','Samba','Tango','Country','EDM','Techno','House','Trance','Dubstep','Drum and bass','Lo-fi','Reggae','Ska','K-pop','J-pop','Indie','Gospel','Ópera','Sinfonía','Orquesta','Coro','Cantautor','Balada','Bolero','Ranchera','Corrido','Mariachi'] }, + { id: 'personajes', name: 'Personajes', emoji: '🧙', words: ['Sherlock Holmes','Harry Potter','Hermione Granger','Ron Weasley','Albus Dumbledore','Voldemort','Frodo Bolsón','Sam Gamyi','Gandalf','Aragorn','Legolas','Gimli','Gollum','Bilbo Bolsón','Katniss Everdeen','Peeta Mellark','Batman','Bruce Wayne','Joker','Harley Quinn','Superman','Clark Kent','Lois Lane','Wonder Woman','Diana Prince','Flash','Barry Allen','Aquaman','Arthur Curry','Spider-Man','Peter Parker','Iron Man','Tony Stark','Capitán América','Steve Rogers','Black Widow','Natasha Romanoff','Hulk','Bruce Banner','Thor','Loki','Thanos','Doctor Strange','Wanda Maximoff','Vision','Star-Lord','Gamora','Groot','Rocket','Drax','Deadpool','Wolverine','Magneto','Professor X','Storm','Cyclops','Jean Grey','Mystique','Darth Vader','Luke Skywalker','Leia Organa','Han Solo','Chewbacca','Yoda','Obi-Wan Kenobi','Anakin Skywalker','Rey','Kylo Ren','R2-D2','C-3PO','Indiana Jones','Lara Croft','James Bond','Mario','Luigi','Princesa Peach','Bowser','Link','Zelda','Geralt de Rivia','Ciri','Yennefer','Kratos','Atreus','Ellie','Joel Miller','Nathan Drake','Master Chief','Cortana','Sonic','Tails','Ash Ketchum','Pikachu','Goku','Vegeta','Naruto','Sasuke','Luffy','Zoro','Nami','Tanjiro','Nezuko','Saitama','Light Yagami','L Lawliet'] } ]; let availablePools = []; @@ -374,7 +36,7 @@ let state = { votingPlayer: 0, selections: [], executed: [], - selectedPools: [], // Now it's an array for multiple pools, will be populated based on language + selectedPool: 'animales_naturaleza', votingPool: null, isTiebreak: false, tiebreakCandidates: [] @@ -393,7 +55,7 @@ const loadPoolsCache = () => { }; const savePoolsCache = () => localStorage.setItem(POOLS_CACHE_KEY, JSON.stringify(poolsCache)); -// ---------- Default values ---------- +// ---------- Defaults ---------- function defaultImpostors(nPlayers) { const capped = Math.min(Math.max(nPlayers, MIN_PLAYERS), MAX_PLAYERS); let impostors = 1; @@ -416,162 +78,63 @@ function defaultDeliberation(gameSeconds) { return Math.max(30, Math.round(gameSeconds / 3)); } -// ---------- Word Pools ---------- +// ---------- 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 = []; + let list = []; 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); + if (res.ok) list = await res.json(); + } catch (_) {} + if (!Array.isArray(list) || list.length === 0) { + list = EMBEDDED_POOLS.map(p => ({ id: p.id, name: p.name, emoji: p.emoji, count: p.words.length })); } - - // 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(); - } - + availablePools = list; 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]; - }); + const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean); + if (!lines.length) return []; + if (lines[0].startsWith('#')) return lines.slice(1); + return lines; } 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); + const poolId = state.selectedPool || 'default'; + let words = []; + if (poolsCache[poolId]?.words) { + words = poolsCache[poolId].words; + } else if (poolId !== 'default') { + const res = await fetch(`word-pools/${poolId}.txt`); + if (!res.ok) throw new Error('No se pudo cargar el pool'); + const text = await res.text(); + words = parseWordsFile(text); + poolsCache[poolId] = { words, ts: Date.now() }; savePoolsCache(); + } else { + words = EMBEDDED_POOLS[0].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] }; + const shuffled = [...words].sort(() => Math.random() - 0.5); + return { civilian: shuffled[0], impostor: shuffled[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(); - }; + if (state.selectedPool === pool.id) btn.classList.add('selected'); + btn.onclick = () => { state.selectedPool = pool.id; saveState(); renderPoolButtons(); }; container.appendChild(btn); }); } -// ---------- Setup and player names ---------- -function goToPools() { +// ---------- Configuración y nombres ---------- +function goToNames() { 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)); @@ -581,15 +144,8 @@ function goToPools() { 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() { + if (nImpostors >= nPlayers) { alert('Impostores debe ser menor que jugadores'); return; } + state.numPlayers = nPlayers; state.numImpostors = nImpostors; state.gameTime = gTime; state.deliberationTime = dTime; buildNameInputs(); showScreen('names-screen'); } @@ -600,13 +156,12 @@ function buildNameInputs() { 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}:`; + div.innerHTML = `Jugador ${i+1}:`; list.appendChild(div); } } -// ---------- Game start ---------- +// ---------- Inicio de partida ---------- function startGame() { state.playerNames = []; for (let i = 0; i < state.numPlayers; i++) { @@ -620,9 +175,8 @@ function startGame() { }).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]; + state.civilianWord = shuffled[0]; + state.impostorWord = shuffled[1]; finalizeStart(); }); } @@ -640,7 +194,7 @@ function finalizeStart() { showScreen('pre-reveal-screen'); } -// Adjust defaults when player count is edited +// Ajustar defaults cuando se edita el nº de jugadores 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); @@ -657,32 +211,21 @@ document.getElementById('num-players').addEventListener('change', () => { 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(', '); - + const startName = state.playerNames[state.startPlayer] || `Jugador ${state.startPlayer+1}`; + const poolMeta = availablePools.find(p => p.id === state.selectedPool) || EMBEDDED_POOLS[0]; 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')}

+

Jugadores: ${state.numPlayers}

+

Impostores: ${state.numImpostors}

+

Tiempo de partida: ${fmt(state.gameTime)}

+

Tiempo de deliberación: ${fmt(state.deliberationTime)}

+

Pool: ${poolMeta.emoji || '🎲'} ${poolMeta.name || poolMeta.id}

+

Empieza: ${startName} · Orden: ${state.turnDirection === 'horario' ? 'Horario' : 'Antihorario'}

`; } -// ---------- Role revelation ---------- +// ---------- Revelación ---------- 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); @@ -690,24 +233,7 @@ function loadCurrentReveal() { 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('othersLookAway')}`; - } - - const curtainText = document.querySelector('.curtain-cover div:last-child'); - if (curtainText) { - curtainText.textContent = t('liftCurtain'); - } - - // Reset curtain state - curtainState.isRevealed = false; - const coverEl = document.getElementById('curtain-cover'); - coverEl.style.transform = 'translateY(0)'; - coverEl.style.transition = ''; - + document.getElementById('curtain-cover').classList.remove('lifted'); document.getElementById('next-player-btn').style.display = 'none'; document.getElementById('start-game-btn').style.display = 'none'; } @@ -715,12 +241,7 @@ function loadCurrentReveal() { function liftCurtain() { const cover = document.getElementById('curtain-cover'); if (cover.classList.contains('lifted')) return; - - // Restore CSS transition and use the class - cover.style.transition = ''; - cover.style.transform = ''; cover.classList.add('lifted'); - const idx = state.revealOrder[state.currentReveal]; const role = state.roles[idx]; const word = role === 'CIVIL' ? state.civilianWord : state.impostorWord; @@ -735,207 +256,19 @@ function liftCurtain() { function nextReveal() { state.currentReveal++; saveState(); loadCurrentReveal(); } -// Curtain system with GRAVITY - The curtain always tends to fall -// Supports both touch (mobile) and mouse (desktop) -// On desktop: curtain stays up while mouse button is held, even if cursor leaves the area -let curtainState = { isRevealed: false }; -let curtainDragState = { - startY: null, - isDragging: false, - currentTranslateY: 0 -}; - -function initCurtainHandlers() { +// swipe support +(() => { const curtain = document.getElementById('curtain'); - if (!curtain) return; - - // Function to get Y position from event (touch or mouse) - const getY = (e) => { - return e.touches ? e.touches[0].clientY : e.clientY; - }; - - // Start function (touch and mouse) - const handleStart = (e) => { - curtainDragState.startY = getY(e); - curtainDragState.isDragging = true; - curtainDragState.currentTranslateY = 0; - if (e.type === 'mousedown') { - e.preventDefault(); // Prevent text selection on desktop - } - }; - - // Move function (touch and mouse) - const handleMove = (e) => { - if (curtainDragState.startY === null || !curtainDragState.isDragging) return; - const currentY = getY(e); - const dy = currentY - curtainDragState.startY; - const coverEl = document.getElementById('curtain-cover'); - if (!coverEl) return; - - // Calculate displacement: negative = up, positive = down - // Allow going further up than the curtain height (user can keep dragging up) - // but don't allow going below initial position (0) - curtainDragState.currentTranslateY = Math.min(dy, 0); - - coverEl.style.transform = `translateY(${curtainDragState.currentTranslateY}px)`; - coverEl.style.transition = 'none'; - - // If lifted enough, show content - if (curtainDragState.currentTranslateY < -80 && !curtainState.isRevealed) { - curtainState.isRevealed = true; - const idx = state.revealOrder[state.currentReveal]; - const role = state.roles[idx]; - const word = role === 'CIVIL' ? state.civilianWord : state.impostorWord; - const roleText = t(role.toLowerCase()); - document.getElementById('role-text').textContent = roleText; - document.getElementById('role-text').className = 'role ' + (role === 'CIVIL' ? 'civil' : 'impostor'); - document.getElementById('word-text').textContent = word; - } - - e.preventDefault(); // Prevent selection - }; - - // End function (touch and mouse) - const handleEnd = (e) => { - if (!curtainDragState.isDragging || curtainDragState.startY === null) return; - const coverEl = document.getElementById('curtain-cover'); - if (!coverEl) return; - - // ALWAYS bring the curtain down when released (GRAVITY) - coverEl.style.transition = 'transform 0.4s ease'; - coverEl.style.transform = 'translateY(0)'; - - // If content was revealed, show button after it falls - if (curtainState.isRevealed) { - setTimeout(() => { - if (state.currentReveal + 1 < state.numPlayers) { - document.getElementById('next-player-btn').style.display = 'block'; - } else { - document.getElementById('start-game-btn').style.display = 'block'; - } - }, 400); - } - - curtainDragState.startY = null; - curtainDragState.isDragging = false; - curtainDragState.currentTranslateY = 0; - }; - - // Touch events (mobile) - curtain.addEventListener('touchstart', handleStart, {passive: false}); - curtain.addEventListener('touchmove', handleMove, {passive: false}); - curtain.addEventListener('touchend', handleEnd, {passive: true}); - curtain.addEventListener('touchcancel', handleEnd, {passive: true}); - - // Mouse events (desktop) - start on curtain only - curtain.addEventListener('mousedown', handleStart); - - // Mouse move and up events on WINDOW so we can track even when cursor leaves everything - window.addEventListener('mousemove', handleMove); - window.addEventListener('mouseup', handleEnd); -} - -// Initialize curtain handlers when DOM is ready -if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initCurtainHandlers); -} else { - initCurtainHandlers(); -} - -// ---------- Screen Wake Lock (prevent screen from sleeping during timers) ---------- -let wakeLock = null; -let wakeLockVideo = null; // For iOS workaround - -// Detect if device is iOS -function isIOS() { - return /iPad|iPhone|iPod/.test(navigator.userAgent) || - (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); -} - -// Check if screen lock is enabled in settings -function isScreenLockEnabled() { - const saved = localStorage.getItem(SCREEN_LOCK_STORAGE_KEY); - return saved === null ? true : saved === 'true'; // Default enabled -} - -// Save screen lock preference -function setScreenLockEnabled(enabled) { - localStorage.setItem(SCREEN_LOCK_STORAGE_KEY, enabled.toString()); - updateScreenLockButton(); -} - -async function requestWakeLock() { - if (!isScreenLockEnabled()) return; - - // Try native Wake Lock API first (works on Android Chrome, etc.) - if ('wakeLock' in navigator) { - try { - wakeLock = await navigator.wakeLock.request('screen'); - wakeLock.addEventListener('release', () => { - wakeLock = null; - }); - console.log('Wake Lock activated (native API)'); - return; - } catch (err) { - console.log('Wake lock request failed:', err); - } - } - - // Fallback for iOS - use hidden video loop - if (isIOS() && !wakeLockVideo) { - try { - wakeLockVideo = document.createElement('video'); - wakeLockVideo.setAttribute('playsinline', ''); - wakeLockVideo.setAttribute('muted', ''); - wakeLockVideo.style.position = 'fixed'; - wakeLockVideo.style.opacity = '0'; - wakeLockVideo.style.pointerEvents = 'none'; - wakeLockVideo.style.width = '1px'; - wakeLockVideo.style.height = '1px'; - - // Minimal base64 encoded video (1 frame, silent) - wakeLockVideo.src = 'data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAAAu1tZGF0AAACrQYF//+p3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE1NSByMjkwMSA3ZDBmZjIyIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAxOCAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTMgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MzoweDExMyBtZT1oZXggc3VibWU9NyBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0xIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MSA4eDhkY3Q9MSBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hyb21hX3FwX29mZnNldD0tMiB0aHJlYWRzPTMgbG9va2FoZWFkX3RocmVhZHM9MSBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRlcmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIGNvbnN0cmFpbmVkX2ludHJhPTAgYmZyYW1lcz0zIGJfcHlyYW1pZD0yIGJfYWRhcHQ9MSBiX2JpYXM9MCBkaXJlY3Q9MSB3ZWlnaHRiPTEgb3Blbl9nb3A9MCB3ZWlnaHRwPTIga2V5aW50PTI1MCBrZXlpbnRfbWluPTI1IHNjZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NDAgcmM9Y3JmIG1idHJlZT0xIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAAAwWWIhAAz//727L4FNf2f0JcRLMXaSnA+KqSAgHc0wAAAAwAAAwAAJuKiZ0WFMeJsgAAAHGAFBCwCPCVC'; - wakeLockVideo.loop = true; - - document.body.appendChild(wakeLockVideo); - await wakeLockVideo.play(); - console.log('Wake Lock activated (iOS video workaround)'); - } catch (err) { - console.log('iOS wake lock workaround failed:', err); - } - } -} - -function releaseWakeLock() { - // Release native Wake Lock - if (wakeLock) { - wakeLock.release(); - wakeLock = null; - } - - // Stop iOS video workaround - if (wakeLockVideo) { - wakeLockVideo.pause(); - wakeLockVideo.remove(); - wakeLockVideo = null; - } -} - -// Re-request wake lock when page becomes visible again -document.addEventListener('visibilitychange', async () => { - if (document.visibilityState === 'visible' && (wakeLock !== null || wakeLockVideo !== null)) { - await requestWakeLock(); - } -}); + let startY = null; + curtain.addEventListener('touchstart', e => { startY = e.touches[0].clientY; }, {passive:true}); + curtain.addEventListener('touchmove', e => { if (startY === null) return; const dy = e.touches[0].clientY - startY; if (dy < -40) { liftCurtain(); startY = null; } }, {passive:true}); + curtain.addEventListener('click', liftCurtain); +})(); // ---------- Timers ---------- let timerInterval = null; -async function startPhaseTimer(phase, seconds, elementId, onEnd) { +function startPhaseTimer(phase, seconds, elementId, onEnd) { if (timerInterval) clearInterval(timerInterval); - - // Request wake lock to keep screen on during timer - await requestWakeLock(); - const now = Date.now(); state.timerPhase = phase; state.timerEndAt = now + seconds*1000; @@ -944,12 +277,7 @@ async function startPhaseTimer(phase, seconds, elementId, onEnd) { const tick = () => { const remaining = Math.max(0, Math.round((state.timerEndAt - Date.now())/1000)); updateTimerDisplay(el, remaining); - if (remaining <= 0) { - clearInterval(timerInterval); - releaseWakeLock(); // Release wake lock when timer ends - playBeep(); - onEnd(); - } + if (remaining <= 0) { clearInterval(timerInterval); playBeep(); onEnd(); } }; tick(); timerInterval = setInterval(tick, 1000); @@ -971,48 +299,17 @@ function updateTimerDisplay(el, remaining) { } function playBeep() { - // Play alarm sound - 3 ascending beeps pattern repeated twice const ctx = new (window.AudioContext || window.webkitAudioContext)(); - const now = ctx.currentTime; - - // Frequencies for alarm pattern (ascending) - const frequencies = [523, 659, 784]; // C5, E5, G5 - const beepDuration = 0.15; - const gapDuration = 0.08; - const patternGap = 0.3; - - let time = now; - - // Play pattern twice - for (let pattern = 0; pattern < 2; pattern++) { - for (let i = 0; i < frequencies.length; i++) { - const osc = ctx.createOscillator(); - const gain = ctx.createGain(); - - osc.connect(gain); - gain.connect(ctx.destination); - osc.frequency.value = frequencies[i]; - osc.type = 'square'; // More alarm-like sound - - gain.gain.setValueAtTime(0, time); - gain.gain.linearRampToValueAtTime(0.25, time + 0.02); - gain.gain.setValueAtTime(0.25, time + beepDuration - 0.02); - gain.gain.linearRampToValueAtTime(0, time + beepDuration); - - osc.start(time); - osc.stop(time + beepDuration); - - time += beepDuration + gapDuration; - } - time += patternGap; - } + const osc = ctx.createOscillator(); const gain = ctx.createGain(); + osc.connect(gain); gain.connect(ctx.destination); osc.frequency.value = 820; osc.type = 'sine'; + gain.gain.setValueAtTime(0.3, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.45); + osc.start(); osc.stop(ctx.currentTime + 0.45); } -// ---------- Game phases ---------- +// ---------- Fases ---------- function startGamePhase() { state.phase = 'game'; saveState(); showScreen('game-screen'); startPhaseTimer('game', state.gameTime, 'game-timer', startDeliberationPhase); } function startDeliberationPhase() { state.phase = 'deliberation'; saveState(); showScreen('deliberation-screen'); startPhaseTimer('deliberation', state.deliberationTime, 'deliberation-timer', startVotingPhase); } function startVotingPhase(candidates = null, isTiebreak = false) { - releaseWakeLock(); // Release wake lock when voting starts (no timer) state.phase = 'voting'; state.votingPlayer = 0; state.votes = {}; @@ -1023,8 +320,8 @@ function startVotingPhase(candidates = null, isTiebreak = false) { renderVoting(); showScreen('voting-screen'); } -function skipToDeliberation() { if (timerInterval) clearInterval(timerInterval); releaseWakeLock(); startDeliberationPhase(); } -function skipToVoting() { if (timerInterval) clearInterval(timerInterval); releaseWakeLock(); startVotingPhase(); } +function skipToDeliberation() { if (timerInterval) clearInterval(timerInterval); startDeliberationPhase(); } +function skipToVoting() { if (timerInterval) clearInterval(timerInterval); startVotingPhase(); } function startTiebreakDeliberation(candidates) { state.phase = 'deliberation'; state.tiebreakCandidates = candidates; @@ -1033,40 +330,27 @@ function startTiebreakDeliberation(candidates) { startPhaseTimer('deliberation', 60, 'deliberation-timer', () => startVotingPhase(candidates, true)); } -// ---------- Secret voting ---------- +// ---------- Votación secreta ---------- function renderVoting() { const pool = state.votingPool || Array.from({length: state.numPlayers}, (_, i) => i); const voter = state.playerNames[state.votingPlayer]; document.getElementById('voter-name').textContent = voter; document.getElementById('votes-needed').textContent = state.numImpostors; - - // Update voting instruction text - const votingInfo = document.querySelector('#voting-screen .info-text'); - if (votingInfo) { - votingInfo.innerHTML = `${t('passMobileTo')} ${voter}. ${t('chooseSuspects')} ${state.numImpostors} ${t('suspect')}.`; - } - state.selections = state.selections || []; const list = document.getElementById('vote-list'); list.innerHTML = ''; pool.forEach(i => { const item = document.createElement('div'); item.className = 'player-item'; - - // Marcar como disabled ANTES de añadir al DOM para que la animación correcta se aplique + item.textContent = state.playerNames[i]; + if (state.votes[i]) item.innerHTML += `Votos: ${state.votes[i]}`; + if (state.selections.includes(i)) item.classList.add('selected'); if (i === state.votingPlayer) { item.classList.add('disabled'); - // NO aplicar opacity inline - dejamos que CSS lo maneje con la animación + item.style.opacity = '0.5'; item.style.pointerEvents = 'none'; - } - - item.textContent = state.playerNames[i]; - if (state.votes[i]) item.innerHTML += `${t('votes')}: ${state.votes[i]}`; - if (state.selections.includes(i)) item.classList.add('selected'); - - if (i !== state.votingPlayer) { - item.onclick = () => toggleSelection(i, item); - } - + } else { + item.onclick = () => toggleSelection(i, item); + } list.appendChild(item); }); updateConfirmButton(); @@ -1097,7 +381,7 @@ function confirmCurrentVote() { renderVoting(); } -// ---------- Vote resolution ---------- +// ---------- Resolución de voto ---------- function handleVoteOutcome() { const pool = state.votingPool || Array.from({length: state.numPlayers}, (_, i) => i); const counts = pool.map(idx => ({ idx, votes: state.votes[idx] || 0 })); @@ -1117,7 +401,7 @@ function handleVoteOutcome() { } else { // Tie for remaining slots if (state.isTiebreak) { - // Second tie: impostors win + // segunda vez empatados: ganan impostores state.executed = []; showResults(true); return; @@ -1131,217 +415,54 @@ function handleVoteOutcome() { showResults(); } -// ---------- Results ---------- +// ---------- Resultados ---------- function showResults(isTiebreak = false) { state.phase = 'results'; saveState(); - - // Liberar Wake Lock cuando termina la partida - releaseWakeLock(); - const executed = state.executed || []; let impostorsAlive = 0; state.roles.forEach((r,i) => { if (r === 'IMPOSTOR' && !executed.includes(i)) impostorsAlive++; }); const winner = impostorsAlive > 0 ? 'IMPOSTORES' : 'CIVILES'; const results = document.getElementById('results-content'); - const winText = winner === 'CIVILES' ? `✅ ${t('civiliansWin')}` : `❌ ${t('impostorsWin')}`; results.innerHTML = ` -

${winText}

-

${t('executed')}: ${executed.length ? executed.map(i => state.playerNames[i]).join(', ') : t('nobody')}

-

${t('votes')}: ${Object.keys(state.votes).length ? '' : t('noVotes')}

-

${t('revealedRoles')}

+

${winner === 'CIVILES' ? '✅ ¡GANAN LOS CIVILES!' : '❌ ¡GANAN LOS IMPOSTORES!'}

+

Ejecutados: ${executed.length ? executed.map(i => state.playerNames[i]).join(', ') : 'Nadie'}

+

Votos: ${Object.keys(state.votes).length ? '' : 'Sin votos'}

+

Roles revelados

${state.roles.map((role,i) => { const word = role === 'CIVIL' ? state.civilianWord : state.impostorWord; const killed = executed.includes(i) ? 'executed' : ''; - const roleText = t(role.toLowerCase()); - return `
${state.playerNames[i]}: ${roleText} — "${word}" ${killed ? '☠️' : ''}
`; + return `
${state.playerNames[i]}: ${role} — "${word}" ${killed ? '☠️' : ''}
`; }).join('')} `; showScreen('results-screen'); } -// ---------- Utilities ---------- +// ---------- Utilidades ---------- function showScreen(id) { document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); document.getElementById(id).classList.add('active'); state.phase = id.replace('-screen',''); saveState(); - updateExitButtonVisibility(); } -function newMatch() { - clearState(); - releaseWakeLock(); // Make sure wake lock is released when exiting game - if (timerInterval) clearInterval(timerInterval); - state = { ...state, phase:'welcome', timerEndAt:null, timerPhase:null, votingPool:null, isTiebreak:false, tiebreakCandidates:[] }; - saveState(); - showScreen('welcome-screen'); -} +function newMatch() { clearState(); state = { ...state, phase:'setup', timerEndAt:null, timerPhase:null, votingPool:null, isTiebreak:false, tiebreakCandidates:[] }; location.reload(); } -function confirmExitGame() { - const confirmMessage = currentLanguage === 'es' - ? '¿Estás seguro de que quieres salir de la partida? Se perderá todo el progreso actual.' - : 'Are you sure you want to exit the game? All current progress will be lost.'; - - if (confirm(confirmMessage)) { - newMatch(); - } -} - -function updateExitButtonVisibility() { - const exitBtn = document.getElementById('exit-game'); - const langBtn = document.getElementById('language-toggle'); - const screenLockBtn = document.getElementById('screen-lock-toggle'); - - // Show exit button and hide language/screen-lock toggles in all phases except welcome and setup - if (state.phase !== 'welcome' && state.phase !== 'setup') { - exitBtn.classList.add('visible'); - if (langBtn) langBtn.style.display = 'none'; - if (screenLockBtn) screenLockBtn.classList.remove('visible'); - } else { - exitBtn.classList.remove('visible'); - if (langBtn) langBtn.style.display = 'inline-flex'; - // Only show screen lock button on iOS - if (screenLockBtn && isIOS()) { - screenLockBtn.classList.add('visible'); - } - } -} - -// ---------- Theme system ---------- -function getSystemTheme() { - return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; -} - -function loadTheme() { - const savedTheme = localStorage.getItem(THEME_STORAGE_KEY); - return savedTheme || getSystemTheme(); -} - -function saveTheme(theme) { - localStorage.setItem(THEME_STORAGE_KEY, theme); -} - -function applyTheme(theme) { - document.documentElement.setAttribute('data-theme', theme); - const themeIcon = document.querySelector('.theme-icon'); - if (themeIcon) { - themeIcon.textContent = theme === 'dark' ? '☀️' : '🌙'; - } -} - -function toggleTheme() { - const currentTheme = document.documentElement.getAttribute('data-theme') || 'light'; - const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; - applyTheme(newTheme); - saveTheme(newTheme); -} - -// Initialize theme -const initialTheme = loadTheme(); -applyTheme(initialTheme); - -// ---------- Screen Lock Button ---------- -function updateScreenLockButton() { - const btn = document.getElementById('screen-lock-toggle'); - if (!btn) return; - - const enabled = isScreenLockEnabled(); - const icon = btn.querySelector('.screen-lock-icon'); - - if (enabled) { - btn.classList.add('active'); - btn.setAttribute('title', 'Bloqueo de pantalla activado'); - if (icon) icon.textContent = '🔒'; - } else { - btn.classList.remove('active'); - btn.setAttribute('title', 'Bloqueo de pantalla desactivado'); - if (icon) icon.textContent = '🔓'; - } -} - -function toggleScreenLock() { - const currentState = isScreenLockEnabled(); - setScreenLockEnabled(!currentState); - - // If disabling, release any active wake lock - if (currentState) { - releaseWakeLock(); - } -} - -// Event listener for theme and language buttons -document.addEventListener('DOMContentLoaded', () => { - const themeToggle = document.getElementById('theme-toggle'); - if (themeToggle) { - themeToggle.addEventListener('click', toggleTheme); - } - - const languageToggle = document.getElementById('language-toggle'); - if (languageToggle) { - languageToggle.addEventListener('click', toggleLanguage); - } - - const screenLockToggle = document.getElementById('screen-lock-toggle'); - if (screenLockToggle) { - screenLockToggle.addEventListener('click', toggleScreenLock); - updateScreenLockButton(); - } - - // Initialize language - currentLanguage = loadLanguage(); - setLanguage(currentLanguage); - - // Detect system theme changes - window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { - // Only apply automatically if user hasn't manually selected a theme - if (!localStorage.getItem(THEME_STORAGE_KEY)) { - applyTheme(e.matches ? 'dark' : 'light'); - } - }); -}); - -// ---------- State rehydration ---------- +// ---------- Rehidratación ---------- (function init() { const restored = loadState(); + showScreen('setup-screen'); loadPoolsList(); if (!state.turnDirection) state.turnDirection = 'horario'; if (typeof state.startPlayer !== 'number') state.startPlayer = 0; - - // Set default values in inputs if we're in setup - if (state.phase === 'setup' || !restored) { - const defaultPlayers = 6; - const defaultImp = defaultImpostors(defaultPlayers); - const defaultGTime = defaultGameTime(defaultPlayers); - const defaultDTime = defaultDeliberation(defaultGTime); - - document.getElementById('num-players').value = defaultPlayers; - document.getElementById('num-impostors').value = defaultImp; - document.getElementById('num-impostors').max = Math.max(1, Math.floor(defaultPlayers / 2)); - document.getElementById('game-time').value = defaultGTime; - document.getElementById('deliberation-time').value = defaultDTime; + switch (state.phase) { + case 'setup': showScreen('setup-screen'); break; + case 'names': buildNameInputs(); showScreen('names-screen'); break; + case 'pre-reveal': renderSummary(); showScreen('pre-reveal-screen'); break; + case 'reveal': showScreen('reveal-screen'); loadCurrentReveal(); break; + case 'game': showScreen('game-screen'); resumeTimerIfNeeded(); break; + case 'deliberation': showScreen('deliberation-screen'); resumeTimerIfNeeded(); break; + case 'voting': showScreen('voting-screen'); renderVoting(); break; + case 'results': showResults(); break; + default: showScreen('setup-screen'); } - - // Determine initial screen - if (!restored || state.phase === 'setup' || state.phase === 'welcome') { - // If no saved state or we're in setup/welcome, show welcome - showScreen('welcome-screen'); - } else { - // If there's a game in progress, restore it - switch (state.phase) { - case 'names': buildNameInputs(); showScreen('names-screen'); break; - case 'pre-reveal': renderSummary(); showScreen('pre-reveal-screen'); break; - case 'reveal': showScreen('reveal-screen'); loadCurrentReveal(); break; - case 'game': showScreen('game-screen'); resumeTimerIfNeeded(); break; - case 'deliberation': showScreen('deliberation-screen'); resumeTimerIfNeeded(); break; - case 'voting': showScreen('voting-screen'); renderVoting(); break; - case 'results': showResults(); break; - default: showScreen('welcome-screen'); - } - } - - // Initialize exit button visibility - updateExitButtonVisibility(); - - // Initialize screen lock button for iOS - initScreenLockButton(); })(); diff --git a/styles.css b/styles.css index 8e4ebaf..5b64e14 100644 --- a/styles.css +++ b/styles.css @@ -452,1378 +452,4 @@ button { ); } -button::before { - content: ''; - position: absolute; - top: 50%; - left: 50%; - width: 0; - height: 0; - border-radius: 50%; - background: rgba(255, 255, 255, 0.2); - transform: translate(-50%, -50%); - transition: width 0.5s ease, height 0.5s ease; -} -button:hover::before { - width: 300px; - height: 300px; -} - -button:hover { - box-shadow: 8px 8px 0px rgba(0, 0, 0, 0.3); - filter: brightness(1.1); -} - -button:active { - box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15); - filter: brightness(0.95); -} - -button.secondary { - background: linear-gradient(135deg, var(--accent-warning) 0%, #c48a2e 100%); - border-color: var(--accent-warning); - color: var(--text-inverted); - box-shadow: var(--shadow-harsh), 0 0 15px rgba(230, 167, 60, 0.25); -} - -button.ghost { - background: transparent; - color: var(--text-primary); - border-color: var(--border-medium); - box-shadow: none; -} - -button.ghost:hover { - background: var(--surface-hover); - box-shadow: var(--shadow-harsh); -} - -button:disabled { - opacity: 0.4; - cursor: not-allowed; - pointer-events: none; -} - -.btn-primary { - background: linear-gradient(135deg, var(--accent-danger) 0%, #b8301e 100%); - border-color: var(--accent-danger); - box-shadow: var(--shadow-harsh), 0 0 20px rgba(217, 54, 38, 0.3); -} - -.btn-secondary { - background: linear-gradient(135deg, var(--accent-info) 0%, #1e3a5f 100%); - border-color: var(--accent-info); - box-shadow: var(--shadow-harsh), 0 0 20px rgba(46, 78, 122, 0.3); -} - -/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - WELCOME SCREEN - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ - -.welcome-content { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - text-align: center; - height: 100%; - gap: 24px; - padding: 20px 0; -} - -.welcome-logo { - width: 140px; - height: 140px; - object-fit: contain; - filter: drop-shadow(5px 5px 0px var(--bg-secondary)) - drop-shadow(0 0 30px rgba(230, 167, 60, 0.3)) - grayscale(0.2) contrast(1.15); - animation: logoFloat 4s ease-in-out infinite, logoGlitch 8s step-end infinite; - position: relative; -} - -@keyframes logoFloat { - 0%, 100% { transform: translateY(0) rotate(0deg); } - 25% { transform: translateY(-8px) rotate(-2deg); } - 75% { transform: translateY(-8px) rotate(2deg); } -} - -@keyframes logoGlitch { - 0%, 90%, 100% { - filter: drop-shadow(4px 4px 0px var(--bg-secondary)) - drop-shadow(0 0 20px var(--border-heavy)) - grayscale(0.3) contrast(1.1); - } - 91% { - filter: drop-shadow(6px 4px 0px var(--accent-danger)) - drop-shadow(0 0 20px var(--accent-danger)) - grayscale(0) contrast(1.3); - } - 92% { - filter: drop-shadow(4px 6px 0px var(--accent-info)) - drop-shadow(0 0 20px var(--accent-info)) - grayscale(0) contrast(1.3); - } - 93% { - filter: drop-shadow(4px 4px 0px var(--bg-secondary)) - drop-shadow(0 0 20px var(--border-heavy)) - grayscale(0.3) contrast(1.1); - } -} - -.welcome-title { - font-size: 2.8em; - margin: 0; - font-family: 'Bebas Neue', 'Crimson Text', Georgia, serif; - font-weight: 400; - text-shadow: 4px 4px 0px var(--bg-secondary), 0 0 40px rgba(230, 167, 60, 0.25); - letter-spacing: 6px; - line-height: 1; - position: relative; - animation: welcomeTitleReveal 0.8s cubic-bezier(0.22, 1, 0.36, 1) forwards; -} - -@keyframes welcomeTitleReveal { - 0% { - opacity: 0; - letter-spacing: 30px; - filter: blur(10px); - transform: scale(0.9); - } - 100% { - opacity: 1; - letter-spacing: 6px; - filter: blur(0); - transform: scale(1); - } -} - -.welcome-subtitle { - font-size: 0.95em; - color: var(--text-secondary); - margin: -10px 0 0 0; - font-weight: 400; - letter-spacing: 0.5px; - font-family: 'JetBrains Mono', monospace; -} - -.welcome-buttons { - display: flex; - flex-direction: column; - gap: 12px; - width: 100%; - max-width: 320px; - margin-top: 10px; -} - -.welcome-credits { - color: var(--text-tertiary); - font-size: 0.75em; - margin-top: auto; - font-weight: 400; - letter-spacing: 1px; - text-transform: uppercase; -} - -.welcome-credits::before { - content: '───── '; -} - -.welcome-credits::after { - content: ' ─────'; -} - -/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - RULES SCREEN - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ - -.rules-content { - flex: 1; - overflow-y: auto; - overflow-x: hidden; - padding: 10px 0; - -webkit-overflow-scrolling: touch; - scrollbar-width: none; - -ms-overflow-style: none; -} - -.rules-content::-webkit-scrollbar { - display: none; -} - -.rule-section { - background: var(--surface-card); - border: 3px solid var(--border-medium); - border-left: 8px solid var(--accent-warning); - border-radius: 0; - padding: 18px; - margin-bottom: 16px; - transition: all 0.3s cubic-bezier(0.22, 1, 0.36, 1); - position: relative; - box-shadow: var(--shadow-md); - clip-path: polygon( - 0 0, - 100% 0, - 100% calc(100% - 10px), - calc(100% - 10px) 100%, - 0 100% - ); - animation: ruleSlideIn 0.4s cubic-bezier(0.22, 1, 0.36, 1) backwards; -} - -.rule-section:nth-child(1) { animation-delay: 0.1s; } -.rule-section:nth-child(2) { animation-delay: 0.2s; } -.rule-section:nth-child(3) { animation-delay: 0.3s; } -.rule-section:nth-child(4) { animation-delay: 0.4s; } - -@keyframes ruleSlideIn { - from { - opacity: 0; - transform: translateX(-20px); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -.rule-section::before { - content: '▸'; - position: absolute; - left: -3px; - top: 18px; - font-size: 1.5em; - color: var(--accent-warning); - animation: blink 2s ease-in-out infinite; -} - -@keyframes blink { - 0%, 49%, 100% { opacity: 1; } - 50%, 99% { opacity: 0; } -} - -.rule-section:hover { - background: var(--surface-hover); - border-left-color: var(--accent-danger); - box-shadow: var(--shadow-lg); -} - -.rule-section h3 { - margin: 0 0 14px 0; - color: var(--text-primary); - font-size: 0.95em; -} - -.rule-section p { - margin: 8px 0; - color: var(--text-secondary); - line-height: 1.7; - font-size: 0.85em; - letter-spacing: 0.3px; -} - -.rule-section strong { - color: var(--accent-danger); - font-weight: 800; - text-transform: uppercase; - font-size: 0.9em; - letter-spacing: 0.5px; -} - -/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - PLAYER MANAGEMENT - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ - -.player-names-list { - flex: 1 1 auto; - min-height: 0; - max-height: 360px; /* Altura máxima para activar scroll y mostrar fila parcial - efecto peek */ - overflow-y: scroll; - overflow-x: hidden; - margin-bottom: 12px; - -webkit-overflow-scrolling: touch; - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* IE y Edge */ - /* Visual frame to indicate scrollable area */ - background: var(--surface-card); - border: 4px solid var(--border-heavy); - border-radius: 0; - padding: 12px; - box-shadow: inset 0 4px 12px rgba(0, 0, 0, 0.15), - inset 0 -4px 12px rgba(0, 0, 0, 0.15), - var(--shadow-md); - /* Gradiente para crear efecto peek - texto cortado visible */ - -webkit-mask-image: linear-gradient(to bottom, - black 0%, - black calc(100% - 40px), - transparent 100%); - mask-image: linear-gradient(to bottom, - black 0%, - black calc(100% - 40px), - transparent 100%); -} - -.player-names-list::-webkit-scrollbar { - display: none; /* Chrome, Safari, Opera */ -} - -.player-name-item { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 8px; - background: var(--bg-secondary); - padding: 12px; - border-radius: 0; - border: 2px solid var(--border-light); - border-left: 4px solid var(--accent-info); - transition: all 0.2s ease; - box-shadow: var(--shadow-sm); -} - -.player-name-item:last-child { - margin-bottom: 0; -} - -.player-name-item:hover { - background: var(--surface-hover); - border-left-color: var(--accent-warning); - transform: translateX(2px); -} - -.player-name-item span { - font-weight: 800; - min-width: 80px; - font-size: 0.8em; - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 1px; -} - -.player-name-item input { - flex: 1; - padding: 10px; - margin: 0; - font-size: 0.85em; - border-width: 2px; -} - -/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - CURTAIN REVEAL MECHANISM - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ - -.curtain { - position: relative; - width: 100%; - height: 280px; - background: var(--bg-secondary); - border-radius: 0; - overflow: hidden; - margin: 12px 0; - box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.3), var(--shadow-harsh); - cursor: grab; - user-select: none; - border: 3px solid var(--border-heavy); - flex-shrink: 0; -} - -.curtain:active { - cursor: grabbing; -} - -.curtain-cover { - position: absolute; - inset: 0; - background: - repeating-linear-gradient( - 0deg, - #2a2a2a 0px, - #2a2a2a 8px, - #1a1a1a 8px, - #1a1a1a 12px - ), - linear-gradient(180deg, rgba(255,200,100,0.03) 0%, transparent 50%); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 16px; - font-size: 1.1em; - font-weight: 800; - color: #888; - transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); - z-index: 10; - user-select: none; - box-shadow: inset 0 -20px 40px rgba(0, 0, 0, 0.5), inset 0 0 60px rgba(0,0,0,0.3); - letter-spacing: 2px; - font-family: 'Bebas Neue', 'JetBrains Mono', monospace; -} - -.curtain-cover::after { - content: ''; - position: absolute; - bottom: -8px; - left: 0; - right: 0; - height: 8px; - background: linear-gradient(90deg, - transparent 0%, - rgba(0,0,0,0.3) 25%, - rgba(0,0,0,0.5) 50%, - rgba(0,0,0,0.3) 75%, - transparent 100%); -} - -.curtain-cover.lifted { - transform: translateY(-100%); -} - -.curtain-icon { - font-size: 2.5em; - animation: bounce 2s ease-in-out infinite; -} - -@keyframes bounce { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-12px); } -} - -.curtain-content { - position: absolute; - inset: 0; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - gap: 20px; - padding: 20px; - text-align: center; -} - -.role { - font-size: 2.4em; - font-weight: 400; - padding: 16px 32px; - border-radius: 0; - text-transform: uppercase; - border: 4px solid; - font-family: 'Bebas Neue', 'JetBrains Mono', monospace; - letter-spacing: 6px; - box-shadow: var(--shadow-harsh); - position: relative; - animation: roleReveal 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards; -} - -@keyframes roleReveal { - 0% { - opacity: 0; - transform: scale(0.5) rotate(-5deg); - filter: blur(10px); - } - 50% { - transform: scale(1.1) rotate(2deg); - } - 100% { - opacity: 1; - transform: scale(1) rotate(0); - filter: blur(0); - } -} - -.role.civil { - background: var(--accent-success); - color: var(--text-inverted); - border-color: #3d5a40; - animation: civilPulse 2s ease-in-out infinite; -} - -.role.impostor { - background: var(--accent-danger); - color: var(--text-inverted); - border-color: #8a2e26; - animation: impostorPulse 1.5s ease-in-out infinite; -} - -@keyframes civilPulse { - 0%, 100% { box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.3), 0 0 0px rgba(90, 125, 95, 0.5); } - 50% { box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.3), 0 0 25px rgba(90, 125, 95, 0.8); } -} - -@keyframes impostorPulse { - 0%, 100% { box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.3), 0 0 0px rgba(196, 69, 54, 0.5); } - 50% { box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.3), 0 0 30px rgba(196, 69, 54, 0.9); } -} - -.word { - font-size: 2em; - font-weight: 400; - background: var(--surface-card); - padding: 20px 36px; - border-radius: 0; - border: 3px solid var(--border-heavy); - font-family: 'Special Elite', 'Crimson Text', serif; - letter-spacing: 2px; - box-shadow: var(--shadow-harsh); - color: var(--text-primary); - text-transform: uppercase; - animation: wordReveal 0.5s cubic-bezier(0.22, 1, 0.36, 1) 0.2s forwards; - opacity: 0; -} - -@keyframes wordReveal { - 0% { - opacity: 0; - transform: translateY(20px); - filter: blur(5px); - } - 100% { - opacity: 1; - transform: translateY(0); - filter: blur(0); - } -} - -/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - TIMER - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ - -.timer { - font-size: 4em; - font-weight: 800; - text-align: center; - margin: 20px 0; - padding: 24px; - background: var(--surface-card); - border-radius: 0; - border: 5px solid var(--border-heavy); - font-family: 'Bebas Neue', 'JetBrains Mono', monospace; - letter-spacing: 8px; - box-shadow: var(--shadow-harsh), inset 0 0 30px rgba(0, 0, 0, 0.2); - position: relative; - clip-path: polygon( - 16px 0, - calc(100% - 16px) 0, - 100% 16px, - 100% calc(100% - 16px), - calc(100% - 16px) 100%, - 16px 100%, - 0 calc(100% - 16px), - 0 16px - ); - animation: timerAppear 0.5s cubic-bezier(0.22, 1, 0.36, 1) forwards; -} - -@keyframes timerAppear { - from { - opacity: 0; - transform: scale(0.8); - filter: blur(5px); - } - to { - opacity: 1; - transform: scale(1); - filter: blur(0); - } -} - -.timer::before { - content: ''; - position: absolute; - top: 8px; - right: 8px; - width: 12px; - height: 12px; - background: var(--accent-success); - border-radius: 50%; - box-shadow: 0 0 10px var(--accent-success); - animation: statusBlink 2s ease-in-out infinite; -} - -@keyframes statusBlink { - 0%, 49%, 100% { opacity: 1; } - 50%, 99% { opacity: 0.3; } -} - -.timer.warning { - color: var(--accent-warning); - border-color: var(--accent-warning); - animation: timerShake 0.5s ease-in-out infinite; -} - -.timer.warning::before { - background: var(--accent-warning); - box-shadow: 0 0 10px var(--accent-warning); -} - -.timer.danger { - color: var(--accent-danger); - border-color: var(--accent-danger); - animation: timerShake 0.25s ease-in-out infinite, dangerFlash 1s ease-in-out infinite; -} - -.timer.danger::before { - background: var(--accent-danger); - box-shadow: 0 0 15px var(--accent-danger); - animation: statusBlink 0.5s ease-in-out infinite; -} - -@keyframes timerShake { - 0%, 100% { transform: translateX(0); } - 25% { transform: translateX(-4px); } - 75% { transform: translateX(4px); } -} - -@keyframes dangerFlash { - 0%, 100% { background: var(--surface-card); } - 50% { background: rgba(196, 69, 54, 0.15); } -} - -/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - INFO BOXES & CONTENT - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ - -.info-text { - text-align: center; - margin: 14px 0; - font-size: 0.85em; - line-height: 1.7; - background: var(--surface-card); - padding: 14px 16px; - border-radius: 0; - color: var(--text-secondary); - border: 2px solid var(--border-light); - border-left: 5px solid var(--accent-info); - box-shadow: var(--shadow-sm), inset 4px 0 8px rgba(46, 78, 122, 0.1); - letter-spacing: 0.3px; -} - -/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - PLAYER SELECTION GRID - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ - -.player-list { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); - gap: 10px; - margin: 12px 0; - flex: 1; - overflow-y: auto; - overflow-x: hidden; - -webkit-overflow-scrolling: touch; - padding: 4px; - scrollbar-width: none; - -ms-overflow-style: none; -} - -.player-list::-webkit-scrollbar { - display: none; -} - -.player-item { - padding: 18px 14px; - min-height: 80px; /* Altura fija para evitar cambios de tamaño con vote-count */ - background: var(--surface-card); - border-radius: 0; - text-align: center; - cursor: pointer; - transition: all 0.25s cubic-bezier(0.22, 1, 0.36, 1); - font-weight: 800; - font-size: 0.85em; - border: 3px solid var(--border-medium); - box-shadow: var(--shadow-sm); - letter-spacing: 0.5px; - text-transform: uppercase; - position: relative; - display: flex; - flex-direction: column; - justify-content: center; - align-items: center; - clip-path: polygon( - 8px 0, - 100% 0, - 100% calc(100% - 8px), - calc(100% - 8px) 100%, - 0 100%, - 0 8px - ); - animation: playerItemAppear 0.3s cubic-bezier(0.22, 1, 0.36, 1) backwards; -} - -.player-item:nth-child(1) { animation-delay: 0.05s; } -.player-item:nth-child(2) { animation-delay: 0.1s; } -.player-item:nth-child(3) { animation-delay: 0.15s; } -.player-item:nth-child(4) { animation-delay: 0.2s; } -.player-item:nth-child(5) { animation-delay: 0.25s; } -.player-item:nth-child(6) { animation-delay: 0.3s; } -.player-item:nth-child(7) { animation-delay: 0.35s; } -.player-item:nth-child(8) { animation-delay: 0.4s; } -.player-item:nth-child(9) { animation-delay: 0.45s; } -.player-item:nth-child(10) { animation-delay: 0.5s; } - -@keyframes playerItemAppear { - from { - opacity: 0; - transform: scale(0.8); - } - to { - opacity: 1; - transform: scale(1); - } -} - -.player-item::before { - content: '□'; - position: absolute; - top: 6px; - right: 6px; - font-size: 1.2em; - transition: all 0.2s ease; -} - -.player-item:hover { - background: var(--surface-hover); - box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.25); - filter: brightness(1.05); -} - -.player-item:active { - box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15); - filter: brightness(0.95); -} - -.player-item.selected { - background: var(--accent-danger); - border-color: var(--text-primary); - color: var(--text-inverted); - box-shadow: 0 0 0 4px rgba(217, 54, 38, 0.5), 6px 6px 0px rgba(0, 0, 0, 0.4); - animation: selectPulse 0.3s ease-out; -} - -@keyframes selectPulse { - 0% { transform: scale(1); } - 50% { transform: scale(1.08); } - 100% { transform: scale(1); } -} - -.player-item.selected::before { - content: '☑'; - animation: checkAppear 0.2s ease-out; -} - -@keyframes checkAppear { - from { transform: scale(0) rotate(-180deg); } - to { transform: scale(1) rotate(0); } -} - -.player-item.disabled { - opacity: 0.5; - cursor: not-allowed; - pointer-events: none; - background: var(--bg-secondary); - border-color: var(--border-light); - filter: grayscale(0.6); - animation: playerItemAppearDisabled 0.3s cubic-bezier(0.22, 1, 0.36, 1) backwards !important; -} - -.player-item.disabled::before { - content: '✕'; - color: var(--text-tertiary); -} - -.player-item .vote-count { - display: block; - font-size: 0.7em; - margin-top: 4px; - opacity: 0.75; - font-weight: 600; - letter-spacing: 0.3px; - min-height: 1em; /* Reservar espacio siempre */ -} - -/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - RESULTS - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ - -.results { - background: var(--surface-card); - border-radius: 0; - padding: 14px; - margin: 8px 0; - flex: 1; - overflow: visible; - border: 2px solid var(--border-medium); - box-shadow: var(--shadow-md); - animation: resultsReveal 0.5s cubic-bezier(0.22, 1, 0.36, 1) forwards; -} - -@keyframes resultsReveal { - from { - opacity: 0; - transform: translateY(20px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -.results h2 { - font-family: 'Bebas Neue', 'Crimson Text', Georgia, serif; - font-size: 1.6em; - letter-spacing: 3px; - margin-bottom: 10px; - animation: winnerReveal 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards; -} - -@keyframes winnerReveal { - 0% { - opacity: 0; - transform: scale(0.5); - filter: blur(10px); - } - 60% { - transform: scale(1.1); - } - 100% { - opacity: 1; - transform: scale(1); - filter: blur(0); - } -} - -.role-reveal { - background: var(--bg-secondary); - padding: 8px 10px; - border-radius: 0; - margin: 5px 0; - border-left: 4px solid; - font-size: 0.8em; - letter-spacing: 0.2px; - box-shadow: var(--shadow-sm); - transition: all 0.2s ease; - animation: roleRevealSlide 0.4s cubic-bezier(0.22, 1, 0.36, 1) backwards; -} - -.role-reveal:nth-child(1) { animation-delay: 0.3s; } -.role-reveal:nth-child(2) { animation-delay: 0.4s; } -.role-reveal:nth-child(3) { animation-delay: 0.5s; } -.role-reveal:nth-child(4) { animation-delay: 0.6s; } -.role-reveal:nth-child(5) { animation-delay: 0.7s; } -.role-reveal:nth-child(6) { animation-delay: 0.8s; } -.role-reveal:nth-child(7) { animation-delay: 0.9s; } -.role-reveal:nth-child(8) { animation-delay: 1s; } -.role-reveal:nth-child(9) { animation-delay: 1.1s; } -.role-reveal:nth-child(10) { animation-delay: 1.2s; } - -@keyframes roleRevealSlide { - from { - opacity: 0; - transform: translateX(-20px); - } - to { - opacity: 1; - transform: translateX(0); - } -} - -.role-reveal:hover { - transform: translateX(3px); -} - -.role-reveal.civil-reveal { - border-left-color: var(--accent-success); -} - -.role-reveal.impostor-reveal { - border-left-color: var(--accent-danger); -} - -.role-reveal.executed { - opacity: 0.5; - background: rgba(0, 0, 0, 0.2); - text-decoration: line-through; -} - -.tag { - display: inline-block; - padding: 6px 10px; - border-radius: 0; - background: var(--surface-hover); - margin: 4px 0; - font-weight: 800; - font-size: 0.75em; - border: 2px solid var(--border-medium); - letter-spacing: 1px; - text-transform: uppercase; -} - -/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - POOL SELECTION - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ - -.pool-buttons { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 8px; - padding: 0; -} - -.pool-buttons-wrapper { - position: relative; - flex: 1 1 auto; - min-height: 0; - max-height: 320px; /* Ajustado para mostrar fila parcial - efecto peek */ - overflow-y: scroll; - overflow-x: hidden; - -webkit-overflow-scrolling: touch; - scrollbar-width: none; /* Firefox */ - -ms-overflow-style: none; /* IE y Edge */ - /* Visual frame to indicate scrollable area */ - background: var(--surface-card); - border: 4px solid var(--border-heavy); - border-radius: 0; - padding: 12px; - margin: 12px 0; - box-shadow: inset 0 4px 12px rgba(0, 0, 0, 0.15), - inset 0 -4px 12px rgba(0, 0, 0, 0.15), - var(--shadow-md); - /* Gradiente para crear efecto peek - texto cortado visible */ - -webkit-mask-image: linear-gradient(to bottom, - black 0%, - black calc(100% - 50px), - transparent 100%); - mask-image: linear-gradient(to bottom, - black 0%, - black calc(100% - 50px), - transparent 100%); -} - -.pool-buttons-wrapper::-webkit-scrollbar { - display: none; /* Chrome, Safari, Opera */ -} - -.pool-btn { - padding: 12px 10px; - border-radius: 0; - border: 2px solid var(--border-medium); - background: var(--surface-card); - color: var(--text-primary); - font-weight: 700; - font-size: 0.8em; - cursor: pointer; - transition: all 0.18s ease; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - text-transform: uppercase; - letter-spacing: 0.5px; - box-shadow: var(--shadow-sm); -} - -.pool-btn:hover { - background: var(--surface-hover); - box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.15); - filter: brightness(1.05); -} - -.pool-btn.selected { - border-color: var(--text-primary); - background: var(--accent-warning); - color: var(--text-inverted); - box-shadow: 0 0 0 3px rgba(212, 165, 116, 0.3), 3px 3px 0px rgba(0, 0, 0, 0.2); -} - -/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - FIXED UI CONTROLS (Theme, Language, Exit) - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ - -.theme-toggle { - position: fixed; - top: 20px; - right: 20px; - width: 56px; - height: 56px; - border-radius: 0; - border: 3px solid var(--border-heavy); - background: var(--surface-glass); - backdrop-filter: blur(20px); - cursor: pointer; - display: flex; - align-items: center; - justify-content: center; - font-size: 1.6em; - box-shadow: var(--shadow-harsh); - transition: all 0.2s ease; - z-index: 1000; - margin: 0; - padding: 0; -} - -.theme-toggle:hover { - box-shadow: 8px 8px 0px rgba(0, 0, 0, 0.2); - filter: brightness(1.1); -} - -.theme-toggle:active { - box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15); - filter: brightness(0.95); -} - -.theme-icon { - transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55); - display: inline-block; -} - -.theme-toggle:hover .theme-icon { - transform: rotate(180deg) scale(1.1); -} - -.language-toggle { - position: fixed; - top: 86px; - right: 20px; - width: auto; - min-width: 56px; - height: 56px; - padding: 0 16px; - border-radius: 0; - border: 3px solid var(--border-heavy); - background: var(--surface-glass); - backdrop-filter: blur(20px); - cursor: pointer; - display: inline-flex; - align-items: center; - justify-content: center; - gap: 8px; - font-size: 1em; - font-weight: 800; - box-shadow: var(--shadow-harsh); - transition: all 0.2s ease; - z-index: 1000; - color: var(--text-primary); - margin: 0; - text-transform: uppercase; - letter-spacing: 1px; -} - -.language-toggle:hover { - box-shadow: 8px 8px 0px rgba(0, 0, 0, 0.2); - filter: brightness(1.1); -} - -.language-toggle:active { - box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15); - filter: brightness(0.95); -} - -.language-icon { - font-size: 1.3em; - transition: transform 0.3s ease; - display: inline-block; -} - -.language-text { - font-size: 0.85em; - letter-spacing: 1.5px; - font-family: 'JetBrains Mono', monospace; -} - -.exit-game { - position: fixed; - top: 20px; - left: 20px; - width: auto; - height: 56px; - padding: 0 16px; - border-radius: 0; - border: 3px solid var(--border-heavy); - background: var(--surface-glass); - backdrop-filter: blur(20px); - cursor: pointer; - display: none; - align-items: center; - justify-content: center; - gap: 10px; - font-size: 0.85em; - font-weight: 800; - box-shadow: var(--shadow-harsh); - transition: all 0.2s ease; - z-index: 1000; - color: var(--text-primary); - text-transform: uppercase; - letter-spacing: 1px; -} - -.exit-game.visible { - display: inline-flex; -} - -.exit-game:hover { - box-shadow: 8px 8px 0px rgba(0, 0, 0, 0.2); - background: var(--accent-danger); - color: var(--text-inverted); -} - -.exit-game:active { - box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15); -} - -.exit-icon { - font-size: 1.3em; -} - -.exit-text { - font-size: 0.9em; - font-family: 'JetBrains Mono', monospace; -} - -.screen-lock-toggle { - position: fixed; - top: 152px; - right: 20px; - width: 56px; - height: 56px; - border-radius: 0; - border: 3px solid var(--border-heavy); - background: var(--surface-glass); - backdrop-filter: blur(20px); - cursor: pointer; - display: none; - align-items: center; - justify-content: center; - font-size: 1.6em; - box-shadow: var(--shadow-harsh); - transition: all 0.2s ease; - z-index: 1000; - margin: 0; - padding: 0; -} - -.screen-lock-toggle.visible { - display: inline-flex; -} - -.screen-lock-toggle:hover { - box-shadow: 8px 8px 0px rgba(0, 0, 0, 0.2); - filter: brightness(1.1); -} - -.screen-lock-toggle:active { - box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15); - filter: brightness(0.95); -} - -.screen-lock-toggle.active { - background: var(--accent-success); - color: var(--text-inverted); - border-color: var(--accent-success); -} - -.screen-lock-icon { - transition: transform 0.3s ease; - display: inline-block; -} - -.screen-lock-toggle:hover .screen-lock-icon { - transform: scale(1.1); -} - -/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - RESPONSIVE DESIGN - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ - -@media (max-width: 600px) { - body { - padding: 60px 10px 10px 10px; - font-size: 13px; - } - - h1 { - font-size: 1.7em; - margin-bottom: 14px; - } - - .container { - padding: 20px 16px; - } - - .theme-toggle, - .language-toggle, - .exit-game, - .screen-lock-toggle { - top: 8px; - width: 44px; - height: 44px; - min-width: 44px; - } - - .language-toggle { - top: 58px; - } - - .screen-lock-toggle { - top: 108px; - } - - .exit-game { - padding: 0 12px; - font-size: 0.75em; - height: 44px; - } - - /* Ocultar textos en móvil, solo emojis */ - .exit-text, - .language-text { - display: none; - } - - .exit-game { - padding: 0; - width: 44px; - min-width: 44px; - } - - .language-toggle { - padding: 0; - width: 44px; - min-width: 44px; - } - - .exit-icon, - .language-icon { - font-size: 1.4em; - } - - .timer { - font-size: 2.5em; - padding: 16px; - } - - .welcome-title { - font-size: 1.8em; - } - - .role { - font-size: 1.6em; - padding: 10px 18px; - } - - .word { - font-size: 1.3em; - padding: 12px 20px; - } - - .form-group { - margin-bottom: 10px; - } - - .form-group.compact { - margin-bottom: 8px; - } - - button { - padding: 12px 16px; - margin-top: 8px; - } - - .rule-section { - padding: 12px; - margin-bottom: 12px; - } - - .rule-section h3 { - font-size: 0.85em; - margin-bottom: 10px; - } - - .rule-section p { - font-size: 0.8em; - margin: 6px 0; - } - - .player-name-item { - padding: 10px; - margin-bottom: 6px; - } - - .player-name-item span { - font-size: 0.75em; - min-width: 70px; - } - - .player-item { - padding: 14px 10px; - min-height: 72px; /* Altura fija también en móvil */ - font-size: 0.8em; - } - - .pool-btn { - padding: 10px 8px; - font-size: 0.75em; - } - - .pool-buttons-wrapper { - max-height: 240px; /* Ajustado para mostrar fila parcial en móvil - efecto peek */ - padding: 10px; - margin: 10px 0; - } - - .player-names-list { - max-height: 280px; /* Ajustado para mostrar fila parcial en móvil - efecto peek */ - padding: 10px; - } - - .info-text { - padding: 12px 14px; - font-size: 0.8em; - margin: 10px 0; - } - - .curtain { - height: 240px; - margin: 10px 0; - } -} - -/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - UTILITY ANIMATIONS - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ - -@keyframes typewriter { - from { width: 0; } - to { width: 100%; } -} - -@keyframes glitch { - 0% { transform: translate(0); } - 20% { transform: translate(-2px, 2px); } - 40% { transform: translate(-2px, -2px); } - 60% { transform: translate(2px, 2px); } - 80% { transform: translate(2px, -2px); } - 100% { transform: translate(0); } -} - -/* Smooth scrolling */ -* { - scrollbar-width: thin; - scrollbar-color: var(--border-medium) transparent; -} - -::-webkit-scrollbar { - width: 8px; - height: 8px; -} - -::-webkit-scrollbar-track { - background: transparent; -} - -::-webkit-scrollbar-thumb { - background: var(--border-medium); - border-radius: 0; -} - -::-webkit-scrollbar-thumb:hover { - background: var(--border-heavy); -} From 750a90f059c4526829ec9efe1f3a390a826b8c80 Mon Sep 17 00:00:00 2001 From: Dasemu Date: Wed, 14 Jan 2026 13:26:17 +0100 Subject: [PATCH 2/9] feat(versioning): add script and workflow for versioning static assets --- .github/workflows/version-assets.yml | 51 +++++++++++++++++++ version-assets.sh | 76 ++++++++++++++++++++++++++++ 2 files changed, 127 insertions(+) create mode 100644 .github/workflows/version-assets.yml create mode 100755 version-assets.sh diff --git a/.github/workflows/version-assets.yml b/.github/workflows/version-assets.yml new file mode 100644 index 0000000..9f577da --- /dev/null +++ b/.github/workflows/version-assets.yml @@ -0,0 +1,51 @@ +name: Version Static Assets + +on: + push: + branches: + - main + paths: + - 'script.js' + - 'styles.css' + - 'logo.png' + +jobs: + version-assets: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.APP_SECRET }} + fetch-depth: 0 + + - name: Delete old versioned assets + run: | + echo "🗑️ Borrando archivos hasheados antiguos..." + rm -f *.*.js *.*.css *.*.png || true + git add -A + + - name: Run asset versioning + run: | + chmod +x version-assets.sh + ./version-assets.sh + + - name: Check for changes + id: check_changes + run: | + if git diff --quiet && git diff --cached --quiet; then + echo "changes=false" >> $GITHUB_OUTPUT + echo "No hay cambios para commitear" + else + echo "changes=true" >> $GITHUB_OUTPUT + fi + + - name: Commit and push versioned assets + if: steps.check_changes.outputs.changes == 'true' + run: | + git config --local user.email "ci@dariosevilla.es" + git config --local user.name "CI Action" + git add *.*.js *.*.css *.*.png index.html + git commit -m "chore: update asset versions [skip ci]" + git push \ No newline at end of file diff --git a/version-assets.sh b/version-assets.sh new file mode 100755 index 0000000..b729216 --- /dev/null +++ b/version-assets.sh @@ -0,0 +1,76 @@ +#!/bin/bash +# Script de versionado de archivos estáticos +# Genera hashes basados en el contenido y actualiza referencias en HTML + +set -e + +echo "🚀 Iniciando versionado de archivos estáticos..." +echo "" + +# Archivos a versionar +ASSETS=("script.js" "styles.css" "logo.png") +HTML_FILE="index.html" + +# Función para generar hash MD5 de un archivo +generate_hash() { + local file=$1 + md5sum "$file" | cut -c1-8 +} + +# Función para obtener el nombre versionado +get_versioned_name() { + local file=$1 + local hash=$2 + local base="${file%.*}" + local ext="${file##*.}" + echo "${base}.${hash}.${ext}" +} + +# Limpiar archivos versionados antiguos +echo "🗑️ Limpiando versiones antiguas..." +rm -f *.*.js *.*.css *.*.png 2>/dev/null || true +echo "" + +# Crear backup del HTML +cp "$HTML_FILE" "${HTML_FILE}.bak" + +# Versionar cada archivo +for asset in "${ASSETS[@]}"; do + if [[ ! -f "$asset" ]]; then + echo "⚠️ Advertencia: $asset no encontrado, saltando..." + continue + fi + + # Generar hash + hash=$(generate_hash "$asset") + versioned=$(get_versioned_name "$asset" "$hash") + + # Copiar archivo con versión + echo "📦 Versionando: $asset → $versioned" + cp "$asset" "$versioned" + + # Actualizar referencia en HTML + case "$asset" in + *.js) + sed -i "s|src=\"${asset}\"|src=\"${versioned}\"|g" "$HTML_FILE" + ;; + *.css) + sed -i "s|href=\"${asset}\"|href=\"${versioned}\"|g" "$HTML_FILE" + ;; + *.png) + sed -i "s|href=\"${asset}\"|href=\"${versioned}\"|g" "$HTML_FILE" + sed -i "s|src=\"${asset}\"|src=\"${versioned}\"|g" "$HTML_FILE" + ;; + esac + + echo "✏️ Actualizado: $asset → $versioned en $HTML_FILE" + echo "" +done + +# Limpiar backup +rm -f "${HTML_FILE}.bak" + +echo "✅ Versionado completado exitosamente!" +echo "" +echo "📋 Archivos versionados:" +ls -1 *.*.{js,css,png} 2>/dev/null || echo " (ninguno)" \ No newline at end of file From ce1a0ab8730fc4ba111b82daf4632c36e63e29a5 Mon Sep 17 00:00:00 2001 From: CI Action Date: Wed, 14 Jan 2026 12:26:56 +0000 Subject: [PATCH 3/9] chore: update asset versions [skip ci] --- index.html | 8 +- logo.78f51359.png | Bin 0 -> 6286 bytes script.d5454706.js | 468 ++++++++++++++++++++++++++++++++++++++++++++ styles.26a5b74f.css | 455 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 927 insertions(+), 4 deletions(-) create mode 100644 logo.78f51359.png create mode 100644 script.d5454706.js create mode 100644 styles.26a5b74f.css diff --git a/index.html b/index.html index 6875738..74489f2 100644 --- a/index.html +++ b/index.html @@ -7,8 +7,8 @@ - - + + @@ -38,7 +38,7 @@
- +

Juego del Impostor

¿Podrás descubrir quién es el impostor?

@@ -186,7 +186,7 @@
- + diff --git a/logo.78f51359.png b/logo.78f51359.png new file mode 100644 index 0000000000000000000000000000000000000000..61cc5bf930b3f56101360075ccc05cbc47ddfee4 GIT binary patch literal 6286 zcmcI|dpOkF_y0&t?nQ_UIdUEMOU$^BL~e-?xg=ubZpLj&3PU$Dr-U$*nUhN@Xhwv254n_a7vDL*-(TN9zW;r{`&oOh=e5>ed+q(~XYIY7b<^9+MM_** z90Gwzope3!1A*)T{_T5(1rqnnphf}P8|~=n2!S*aCAPyw1o%jVtB)rHg42RPuH``> zn*z}_5CTaxhCpV|LmZ~+*(9LLs)oyeZ9E2 zcz1XAYUstU0`(rOkC!hbiir~Q6Cj}r?k>m0etxO`AplV9Nl$0!w1|kfxcCLe*j>T; zVJD9}p2=X%vrnary_FSBvXA2+5w8wP_6GGKg>Gq|FD5RfKoY^0V>3-*`hhnk2PzjH zK3q`xwxv*a%lXejHG2!)o3i#-9QN;Z{NE-Xa(O&+O|8mtCgU~#yFI4|cGOJw)#U-I z+3=s#5Jf|?$C`I_ddgu4v&V=!Ouo5E%Ygl4mbCEkT%`}wIwnp?HQO1b3Z$H0w0Es( zjI|T8M0CJRVa3k#fjZq29_77zuddpxYMl8%iFm_)q+IEPQPcT+TG=vhjUOqRl@Mb+ z<7*`2J|h=^GflIrquDMaszXRaFKR+ar*!70u5?VBv^!Ql86gbR-djzz8(vLHvn!-o zE(ezMO%2*q{>8L4uB0-vtSTX_bL;v{q+8|g2GNEiOSjJ6vB=)}eM)UNkM?Al8~2&@ z>fECct)`!SL$v2KI-&zpqlc-_wRC-Y))@1cC~lx5$xi-YB3GT;b{nVPCR)H6o8Hej$oppSefHU#?XsmmGxWim+-Gk+ z?yk1}#EeF2c|#Lnz3Xv>ZQGgUZf4^3L%}7|96M=-!GnG#$06uv)J|~F2*=?q!?RVi zj`^y>RdcF$7-&B_ACFbxzW%jzg6XO@Aj11rA343o2^6(R38IBHEqTD4tP6bY&4#*KyakgN|TH5s!APwy+7aNs7`ILz45At zkZp)A@hl|Th+C_HN(V{0{1;(Ok>bCbdQe0OHg#E@YKBk4FyaZqsI@>aoazGSKeo5{ z(FAaF(y&*Ne_)$?lMhs1L1Ud|mPFiz(Ml#3F1aNMgx2?^;bH9+!Na!7M9KOWh&U2a zbdaNRV&D-99#7CVn>n3bs3|h2OQZgJ0rVhPS`OXYVbQB%;MKDJ?1kE)8h0xB{%yebmj>fNg0wyNN0<_c zsg^>BbG9~(rus5qHoXdQ1nqzsN$kCLXv|eAo%|{AzMaJHrgJkM)XthH$IEiv$Iqe@ zd2d{QBl{)hJ8SCzPSmpmrnH00s&vDeOiBT zC!(tg1DxX244<*y(kJp~mPF(C&3p5XQ@PrSj7WbGG?cfOKRH-N$ZziXiV3?IT*ZjGL%%2hZ)%j+gZS89NrA^nSVxRX8 z^dc&pO1%avH-6hNt#2efmoU%7d^XE3 zPD5TkLrwyTxR4#V7chViUSDmEaOUeoNM%8|noxQTLEP3syLQgBG){$5=ZotxX%0Tl z+yOXAUk{RCJ3q|prRQw?-SDlu-<#xOJQ6Ll(98A(DtJ{+*-{AS)#!OW;oVYMV`fu9&! zuIiVvHMmzkP#5Tu8YSN$44x*pcQjsH zdA|A4W#`3-r2S~i=oWUkh^@{t-k%rYF!Ep=_U#B;ivQt5W^tvxIz0Sc0_8CB=~utv zHis8C-?8O!IiW8`+LlzIxJwhyuGIRmE2p0lU>{FWOK)L_HGb7D1*2&-Wv&SgGT_NC z#~bvE?T%p&N6SidGJ7u%?F}k=l<___{V$r}XfDwKDWjzeLO7oyz^CTR^M#3I>R9o{ zc+#a1F<6-$wh5u|x~hhWA5)XZX#+WB+9B!AO%ce?D*g!B zE8VkNaY4Nt@O9^=lx{HPi=&*Z<#Fny{;j3^|G00h1NVm*ZqYuC)1A4OKdr|}k4~O9 zlq$hCJb+#c)rtOVA~^KOBzt0?XWGoc4zKp$SX6a8@N+W5p~%i+g|y6MvP~fTn&UhP ze?uwO9^C-kS@b39%7f;5g#`j1_CXmZi>Q^TpCgqR2H-qEylL?)OXp;)lZ%n+fF-mZ z$Of&o6;qp-PN)%a_8Ww5NKUAj_64Y|uEHj9x8=H*YpLE~E+M^7x%>(xNw*?xjF#-IzM4kax?2i zv6g(TBKq}le@2sAtYhCPJGA4n1gs2v^&a8Ro7MsHP-6%ey{v=aOL5p!nlxXT{#e~S zJk6c!TKxsMEzEyo$iI4I1m&Vf9)rPoB-}ME$;i2`Mw&Esa@(4v@)l1xv$+q*8w{Tp z2t~2{oO}SLfD7Y&T{yHCrz?}N*!2|a1X8AKk|pRBE}`HpxDMf;0x!rR$2K^B%jZ{} zQgO{nErJ$6ja@WM1yc!qqUGLbC4S2)Zl63%tlYTOt#Ts(Yv5Hb|3ea|OVrTiTi_zP zVmlOE{BrzL7*29;hc85hDJqp`MAcDAl)0oo945U=qvyqO?-54R$r+W4VdCAkXP0lj z%H@z*EJc2TieL07JIz4)=^mIqr5|1%2+jq&_GbBuk;~tSb=2hC536ph!yZO!a`nx_ zo#EEa4c_v&c?q1QS8pxOhU8XydH4W!p(-`N{GayOQy-5~9_60{r))%lj*47xpnb7H z6nG_qvo;mR&knTzzjVnF(QaW;D1vQ2Y(RqBxmy7r#xJd1SEvY%#_8F4P67YKNdCJK zkJrhgVN5?Dh~c}L*I$wA_By2@kf{P{rbm?k?n)hnnBE1Tz<*3d0q}^{K!2Af=Nq2# z*gJs?(C1AL0CC(~+8I9J#{)sJn0=(nIio3F_MLF2B5{M{l0||J7Ik+j4pRR&qM=#m z+Ky)7Y_PCxKgH~kc|qtbwXxZX?NA_1ac_B=RN`bL3tHITkl;r>gqAp%wjt54M+nMj z3+_qjLBwvP9s4h!dPux5<)?PWx{k>YRr41b3LNC!ttGZyU!0`Gdb_;e>P&Uf>ad8 zka-m{d}ca~6j5B`l$cN?MaC@1Zz6q{d8_U?y!0SdOh#8eo`dW}ac6!AZ93jkN#-mP z3hXE=B>Tu{isSO1+^|p0NdD_QuPaNMxA0L8iCVIM4BfSJNrU#ZZ_B&O0KVG_h6mAN zDX-fNu&WX!Umx26sDOLT5sxsjwKDVs)$&1^>L^)!uvg45GC@zxEYHo@`&Gg>FKmFvi&7!sPY; z451ZRvdBeAv;jC)Zw+=e&(1v0HAEzq(Dbkus`k(>QNIJ5jLKUK9B`ZY!wyJgT%6GZ z=P1D4!KP7Tx!JE8v?pfr9dt8FEcGUX?Kqbpt;`Ra@Q7TH3Hp?Ii=B7djPi6sCe|Rt#x;VZkh}{k$ z!)ChYNp$Lpgh9a$WqR8pZC*Q-r7{epcREQiC`(;)wV?jM79bOB z@ddXtL>V6!iD#jLw^`DPoTdu z5&n(dGnnq5moQDrA;{E<)Q5oEGSLWNP@J4YASSn6Q?TNOB^aynABoVa030w#3Ix)H z(RZ7bv7IPw%A;ns0mYed3798o3@!TLuWk#~Pm->e$ZtN`%Ldxb&}GdESmOc$AvsaT z0BhJ4=C6paakAG#WqXpJh|~C;Rh|;COmmMrX#!9FLC}tRj3mwznf>Xymr;xqcw4d~ znVLl@Do%jV8CA42j+4u-VgDmTT0(WBGEh&x38Y^HBdh1S)3tLGLcx954#5_m5DO|| z!GpM8*GGd@D>j^gL2?X`j^um%KG_~r-6#&sOVGz*?j1lLci5;@`(*aVCw$Q${tt#Q zQWnWSMNgdCIxd96yx7P3PuZTVT2yz-{Z%O)%@`5%ZPYTQI~G;j>hf`BzvDTpXpZ8x z0r3pj*sbFDc|;V5HNq!Ae~0M(_HITTJ%#ijiFJO5@yTybll&u`4kuRtJHBZ&{13RU#bwKO^*4K7r zByL|8<_0AlALTTrh-|Z7$Im!s7oirlE+$GtxsrU1PN1vwxEpm1KV#RvoaB$c8tLd_ zr{M~0oh+-+xC134bNCb|;ErQ9yQ z>ZRrY=dV8e!(p*+DnVL}`-FY>x?w!8b3f8ls~HGolmqFe{E&TFutPYvp)*3{(w&kQ zIkMP+vUbGl8^D)-U-7{_0s@o0JwFq??7TiRS>fkvXucMMje8p|9@13R<}pT8B&S!( z%EU#_^sM>?-~SOG|zIkQcRIyy6lgAUAFQ2Xh8JvC;cH>zm}S4 zyC5|09xDK~X$QFL&OJkeyAl}fu&Zy<4!}UTn3Y0 zZbmsMK6-(;q0|WEh5Z!|3CX&xs7kZ^Tak~Mmon@coQU66hJv$)r{s|ta@|qNGPrQZ z^vP$ZW3>3qt(dDANKm`TEr~14%E~znRd~tvwEa0L+prKEvXvVh{bc;H17DUx+mKGO!%vV`lcBrn+M? zG<-~X?mdWdJrL1qpHiAIVY3e#Qk`DS2s`TowwOCEX_?=<3STvw@Yw_YD#i)ld$h;% z_Ej=8#s7)9<`0xygIUJ1$pz8+Fz#LP_<)!w>Pg0{!$f%v^l!S!gJ!?#l`WeOlg33+ z@zN|m(L}2>eagouvtV^t-{03e|qTo%Ipa-#!QMJU>>`elgeYelz&^h}Q8-h;+`CyPD5O z(u)rYDyy*y8xD+F=IyAvVRBtJH}8)UE&mMaQXiW9+D@}%1Xks?6((pU4i#Cg!RP1y zQcE0ewp!Djk55tHee+C_o$W!UYt0uX%L-7kLQLX~nX)eYsmZU<)TLuqH))ptIr%tu z?Mvlx*4Bq16s+si_R$a8*fi>@-S;9_=;HE<Y@X$wp1>QvEmeIU-Z3O z#Y^%-C~=DVQq3OTt!|nvYwNQ`k$wLc#5CT8 zo`}X=pDEy+5metbvCe0)k>S{j$cU7S0tPWPHZ?IYwh+)66Jw-+jLqT3g3)-b&ei(A e8N??>#zbZQ&j$JYr2>HgGk3 literal 0 HcmV?d00001 diff --git a/script.d5454706.js b/script.d5454706.js new file mode 100644 index 0000000..8e41f46 --- /dev/null +++ b/script.d5454706.js @@ -0,0 +1,468 @@ +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 EMBEDDED_POOLS = [ + { id: 'animales_naturaleza', name: 'Animales y naturaleza', emoji: '🌿', words: ['Perro','Gato','Lobo','Zorro','Oso','Tigre','León','Pantera','Jaguar','Puma','Guepardo','Elefante','Rinoceronte','Hipopótamo','Jirafa','Cebra','Camello','Dromedario','Canguro','Koala','Panda','Mapache','Nutria','Castor','Foca','Morsa','Delfín','Ballena','Tiburón','Orca','Pulpo','Calamar','Medusa','Tortuga','Lagarto','Cocodrilo','Serpiente','Anaconda','Iguana','Rana','Sapo','Búho','Halcón','Águila','Cóndor','Gaviota','Loro','Flamenco','Pingüino','Avestruz','Gallina','Pato','Ganso','Cisne','Abeja','Hormiga','Mariquita','Libélula','Mariposa','Escarabajo','Grillo','Saltamontes','Araña','Escorpión','Lombriz','Caracol','Estrella de mar','Coral','Musgo','Helecho','Pino','Roble','Encina','Palmera','Cactus','Bambú','Rosa','Tulipán','Girasol','Lavanda','Montaña','Río','Lago','Mar','Playa','Desierto','Selva','Bosque','Pradera','Glaciar','Volcán'] }, + { id: 'vida_cotidiana', name: 'Vida cotidiana', emoji: '🏠', words: ['Pan','Leche','Café','Té','Agua','Jugo','Refresco','Cerveza','Vino','Pizza','Hamburguesa','Sándwich','Taco','Burrito','Pasta','Arroz','Paella','Sushi','Ramen','Ensalada','Sopa','Croqueta','Tortilla','Empanada','Arepa','Queso','Jamón','Chorizo','Pollo','Carne','Cerdo','Pescado','Marisco','Patata','Tomate','Cebolla','Ajo','Pimiento','Zanahoria','Lechuga','Brócoli','Coliflor','Manzana','Plátano','Naranja','Pera','Uva','Fresa','Mango','Piña','Melón','Sandía','Yogur','Galletas','Chocolate','Helado','Cereales','Mantequilla','Aceite','Sal','Pimienta','Azúcar','Harina','Huevo','Cuchara','Tenedor','Cuchillo','Plato','Vaso','Taza','Olla','Sartén','Microondas','Horno','Nevera','Mesa','Silla','Sofá','Cama','Almohada','Sábana','Toalla','Ducha','Jabón','Champú','Cepillo','Pasta de dientes'] }, + { id: 'deportes', name: 'Deportes', emoji: '🏅', words: ['Fútbol','Baloncesto','Tenis','Pádel','Bádminton','Voleibol','Béisbol','Rugby','Hockey hielo','Hockey césped','Golf','Boxeo','MMA','Judo','Karate','Taekwondo','Esgrima','Tiro con arco','Halterofilia','Crossfit','Atletismo','Maratón','Triatlón','Ciclismo ruta','Ciclismo montaña','BMX','Natación','Waterpolo','Surf','Vela','Remo','Piragüismo','Esquí','Snowboard','Patinaje artístico','Patinaje velocidad','Curling','Escalada','Senderismo','Trail running','Parkour','Gimnasia artística','Gimnasia rítmica','Trampolín','Skate','Breakdance','Carreras coches','Fórmula 1','Rally','Karting','Motociclismo','Enduro','Motocross','Equitación','Polo','Críquet','Billar','Dardos','Petanca','Pickleball','Ultimate frisbee','Paintball','Airsoft','eSports'] }, + { id: 'marcas', name: 'Marcas', emoji: '🛍️', words: ['Apple','Samsung','Google','Microsoft','Amazon','Meta','Tesla','Toyota','Honda','Ford','BMW','Mercedes','Audi','Volkswagen','Porsche','Ferrari','Lamborghini','Maserati','McLaren','Chevrolet','Nissan','Kia','Hyundai','Peugeot','Renault','Volvo','Jaguar','Land Rover','Fiat','Alfa Romeo','Ducati','Yamaha','Canon','Nikon','Sony','Panasonic','LG','Philips','Siemens','Bosch','Whirlpool','Ikea','Zara','H&M','Uniqlo','Nike','Adidas','Puma','Reebok','New Balance','Under Armour','Converse','Vans','Patagonia','The North Face','Columbia','Levi’s','Calvin Klein','Gucci','Prada','Louis Vuitton','Chanel','Hermès','Dior','Rolex','Omega','Casio','Pepsi','Coca-Cola','Fanta','Red Bull','Monster','Starbucks','Nespresso','Nestlé','Danone','Kellogg’s','Oreo','Intel','AMD','Nvidia','Qualcomm','TikTok','Netflix','Disney','Warner Bros','HBO','Spotify','Airbnb','Uber','Booking'] }, + { id: 'musica', name: 'Música', emoji: '🎵', words: ['Guitarra','Piano','Violín','Batería','Bajo','Saxofón','Trompeta','Flauta','Clarinete','Acordeón','Ukelele','Arpa','Sintetizador','DJ','Micrófono','Altavoz','Concierto','Festival','Vinilo','Rock','Pop','Punk','Metal','Heavy','Thrash','Death metal','Jazz','Blues','Soul','Funk','R&B','Rap','Hip hop','Trap','Reggaetón','Salsa','Bachata','Merengue','Cumbia','Vallenato','Flamenco','Rumba','Bossa nova','Samba','Tango','Country','EDM','Techno','House','Trance','Dubstep','Drum and bass','Lo-fi','Reggae','Ska','K-pop','J-pop','Indie','Gospel','Ópera','Sinfonía','Orquesta','Coro','Cantautor','Balada','Bolero','Ranchera','Corrido','Mariachi'] }, + { id: 'personajes', name: 'Personajes', emoji: '🧙', words: ['Sherlock Holmes','Harry Potter','Hermione Granger','Ron Weasley','Albus Dumbledore','Voldemort','Frodo Bolsón','Sam Gamyi','Gandalf','Aragorn','Legolas','Gimli','Gollum','Bilbo Bolsón','Katniss Everdeen','Peeta Mellark','Batman','Bruce Wayne','Joker','Harley Quinn','Superman','Clark Kent','Lois Lane','Wonder Woman','Diana Prince','Flash','Barry Allen','Aquaman','Arthur Curry','Spider-Man','Peter Parker','Iron Man','Tony Stark','Capitán América','Steve Rogers','Black Widow','Natasha Romanoff','Hulk','Bruce Banner','Thor','Loki','Thanos','Doctor Strange','Wanda Maximoff','Vision','Star-Lord','Gamora','Groot','Rocket','Drax','Deadpool','Wolverine','Magneto','Professor X','Storm','Cyclops','Jean Grey','Mystique','Darth Vader','Luke Skywalker','Leia Organa','Han Solo','Chewbacca','Yoda','Obi-Wan Kenobi','Anakin Skywalker','Rey','Kylo Ren','R2-D2','C-3PO','Indiana Jones','Lara Croft','James Bond','Mario','Luigi','Princesa Peach','Bowser','Link','Zelda','Geralt de Rivia','Ciri','Yennefer','Kratos','Atreus','Ellie','Joel Miller','Nathan Drake','Master Chief','Cortana','Sonic','Tails','Ash Ketchum','Pikachu','Goku','Vegeta','Naruto','Sasuke','Luffy','Zoro','Nami','Tanjiro','Nezuko','Saitama','Light Yagami','L Lawliet'] } +]; + +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: [], + selectedPool: 'animales_naturaleza', + 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)); + +// ---------- Defaults ---------- +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)); +} + +// ---------- Pools ---------- +async function loadPoolsList() { + loadPoolsCache(); + let list = []; + try { + const res = await fetch(POOLS_MANIFEST_URL); + if (res.ok) list = await res.json(); + } catch (_) {} + if (!Array.isArray(list) || list.length === 0) { + list = EMBEDDED_POOLS.map(p => ({ id: p.id, name: p.name, emoji: p.emoji, count: p.words.length })); + } + availablePools = list; + renderPoolButtons(); +} + +function parseWordsFile(text) { + const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean); + if (!lines.length) return []; + if (lines[0].startsWith('#')) return lines.slice(1); + return lines; +} + +async function pickWords() { + const poolId = state.selectedPool || 'default'; + let words = []; + if (poolsCache[poolId]?.words) { + words = poolsCache[poolId].words; + } else if (poolId !== 'default') { + const res = await fetch(`word-pools/${poolId}.txt`); + if (!res.ok) throw new Error('No se pudo cargar el pool'); + const text = await res.text(); + words = parseWordsFile(text); + poolsCache[poolId] = { words, ts: Date.now() }; savePoolsCache(); + } else { + words = EMBEDDED_POOLS[0].words; + } + const shuffled = [...words].sort(() => Math.random() - 0.5); + return { civilian: shuffled[0], impostor: shuffled[1] }; +} + +function renderPoolButtons() { + const container = document.getElementById('pool-buttons'); + if (!container) return; + container.innerHTML = ''; + 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.selectedPool === pool.id) btn.classList.add('selected'); + btn.onclick = () => { state.selectedPool = pool.id; saveState(); renderPoolButtons(); }; + container.appendChild(btn); + }); +} + +// ---------- Configuración y nombres ---------- +function goToNames() { + 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('Impostores debe ser menor que jugadores'); return; } + state.numPlayers = nPlayers; state.numImpostors = nImpostors; state.gameTime = gTime; state.deliberationTime = dTime; + 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'; + div.innerHTML = `Jugador ${i+1}:`; + list.appendChild(div); + } +} + +// ---------- Inicio de partida ---------- +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); + state.civilianWord = shuffled[0]; + state.impostorWord = shuffled[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'); +} + +// Ajustar defaults cuando se edita el nº de jugadores +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] || `Jugador ${state.startPlayer+1}`; + const poolMeta = availablePools.find(p => p.id === state.selectedPool) || EMBEDDED_POOLS[0]; + el.innerHTML = ` +

Jugadores: ${state.numPlayers}

+

Impostores: ${state.numImpostors}

+

Tiempo de partida: ${fmt(state.gameTime)}

+

Tiempo de deliberación: ${fmt(state.deliberationTime)}

+

Pool: ${poolMeta.emoji || '🎲'} ${poolMeta.name || poolMeta.id}

+

Empieza: ${startName} · Orden: ${state.turnDirection === 'horario' ? 'Horario' : 'Antihorario'}

+ `; +} + +// ---------- Revelación ---------- +function loadCurrentReveal() { + state.phase = 'reveal'; saveState(); + 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; + document.getElementById('curtain-cover').classList.remove('lifted'); + document.getElementById('next-player-btn').style.display = 'none'; + document.getElementById('start-game-btn').style.display = 'none'; +} + +function liftCurtain() { + const cover = document.getElementById('curtain-cover'); + if (cover.classList.contains('lifted')) return; + cover.classList.add('lifted'); + const idx = state.revealOrder[state.currentReveal]; + const role = state.roles[idx]; + const word = role === 'CIVIL' ? state.civilianWord : state.impostorWord; + document.getElementById('role-text').textContent = role; + document.getElementById('role-text').className = 'role ' + (role === 'CIVIL' ? 'civil' : 'impostor'); + document.getElementById('word-text').textContent = word; + setTimeout(() => { + if (state.currentReveal + 1 < state.numPlayers) document.getElementById('next-player-btn').style.display = 'block'; + else document.getElementById('start-game-btn').style.display = 'block'; + }, 700); +} + +function nextReveal() { state.currentReveal++; saveState(); loadCurrentReveal(); } + +// swipe support +(() => { + const curtain = document.getElementById('curtain'); + let startY = null; + curtain.addEventListener('touchstart', e => { startY = e.touches[0].clientY; }, {passive:true}); + curtain.addEventListener('touchmove', e => { if (startY === null) return; const dy = e.touches[0].clientY - startY; if (dy < -40) { liftCurtain(); startY = null; } }, {passive:true}); + curtain.addEventListener('click', liftCurtain); +})(); + +// ---------- Timers ---------- +let timerInterval = null; +function startPhaseTimer(phase, seconds, elementId, onEnd) { + if (timerInterval) clearInterval(timerInterval); + const now = Date.now(); + state.timerPhase = phase; + state.timerEndAt = now + seconds*1000; + saveState(); + const el = document.getElementById(elementId); + const tick = () => { + const remaining = Math.max(0, Math.round((state.timerEndAt - Date.now())/1000)); + updateTimerDisplay(el, remaining); + if (remaining <= 0) { clearInterval(timerInterval); playBeep(); onEnd(); } + }; + tick(); + timerInterval = setInterval(tick, 1000); +} + +function resumeTimerIfNeeded() { + if (!state.timerEndAt || !state.timerPhase) return; + const remaining = Math.round((state.timerEndAt - Date.now())/1000); + if (remaining <= 0) { state.timerEndAt = null; saveState(); return; } + if (state.timerPhase === 'game') { showScreen('game-screen'); startPhaseTimer('game', remaining, 'game-timer', startDeliberationPhase); } + else if (state.timerPhase === 'deliberation') { showScreen('deliberation-screen'); startPhaseTimer('deliberation', remaining, 'deliberation-timer', startVotingPhase); } +} + +function updateTimerDisplay(el, remaining) { + const minutes = Math.floor(remaining/60); const secs = remaining%60; + el.textContent = `${minutes}:${secs.toString().padStart(2,'0')}`; + el.className = 'timer'; + if (remaining <= 10) el.classList.add('danger'); else if (remaining <= 30) el.classList.add('warning'); +} + +function playBeep() { + const ctx = new (window.AudioContext || window.webkitAudioContext)(); + const osc = ctx.createOscillator(); const gain = ctx.createGain(); + osc.connect(gain); gain.connect(ctx.destination); osc.frequency.value = 820; osc.type = 'sine'; + gain.gain.setValueAtTime(0.3, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.45); + osc.start(); osc.stop(ctx.currentTime + 0.45); +} + +// ---------- Fases ---------- +function startGamePhase() { state.phase = 'game'; saveState(); showScreen('game-screen'); startPhaseTimer('game', state.gameTime, 'game-timer', startDeliberationPhase); } +function startDeliberationPhase() { state.phase = 'deliberation'; saveState(); showScreen('deliberation-screen'); startPhaseTimer('deliberation', state.deliberationTime, 'deliberation-timer', startVotingPhase); } +function startVotingPhase(candidates = null, isTiebreak = false) { + state.phase = 'voting'; + state.votingPlayer = 0; + state.votes = {}; + state.selections = []; + state.votingPool = candidates; + state.isTiebreak = isTiebreak; + saveState(); + renderVoting(); + showScreen('voting-screen'); +} +function skipToDeliberation() { if (timerInterval) clearInterval(timerInterval); startDeliberationPhase(); } +function skipToVoting() { if (timerInterval) clearInterval(timerInterval); startVotingPhase(); } +function startTiebreakDeliberation(candidates) { + state.phase = 'deliberation'; + state.tiebreakCandidates = candidates; + saveState(); + showScreen('deliberation-screen'); + startPhaseTimer('deliberation', 60, 'deliberation-timer', () => startVotingPhase(candidates, true)); +} + +// ---------- Votación secreta ---------- +function renderVoting() { + const pool = state.votingPool || Array.from({length: state.numPlayers}, (_, i) => i); + const voter = state.playerNames[state.votingPlayer]; + document.getElementById('voter-name').textContent = voter; + document.getElementById('votes-needed').textContent = state.numImpostors; + state.selections = state.selections || []; + const list = document.getElementById('vote-list'); list.innerHTML = ''; + pool.forEach(i => { + const item = document.createElement('div'); + item.className = 'player-item'; + item.textContent = state.playerNames[i]; + if (state.votes[i]) item.innerHTML += `Votos: ${state.votes[i]}`; + if (state.selections.includes(i)) item.classList.add('selected'); + if (i === state.votingPlayer) { + item.classList.add('disabled'); + item.style.opacity = '0.5'; + item.style.pointerEvents = 'none'; + } else { + item.onclick = () => toggleSelection(i, item); + } + list.appendChild(item); + }); + updateConfirmButton(); +} + +function toggleSelection(idx, el) { + if (idx === state.votingPlayer) return; + if (state.selections.includes(idx)) state.selections = state.selections.filter(x => x !== idx); + else { + if (state.selections.length >= state.numImpostors) return; + state.selections.push(idx); + } + saveState(); + renderVoting(); +} + +function updateConfirmButton() { + const btn = document.getElementById('confirm-vote-btn'); + btn.disabled = state.selections.length !== state.numImpostors; +} + +function confirmCurrentVote() { + state.selections.forEach(t => { state.votes[t] = (state.votes[t] || 0) + 1; }); + state.votingPlayer++; + state.selections = []; + saveState(); + if (state.votingPlayer >= state.numPlayers) { handleVoteOutcome(); return; } + renderVoting(); +} + +// ---------- Resolución de voto ---------- +function handleVoteOutcome() { + const pool = state.votingPool || Array.from({length: state.numPlayers}, (_, i) => i); + const counts = pool.map(idx => ({ idx, votes: state.votes[idx] || 0 })); + counts.sort((a, b) => b.votes - a.votes); + + let slots = state.numImpostors; + const executed = []; + for (let i = 0; i < counts.length && slots > 0; ) { + const currentVotes = counts[i].votes; + const group = []; + let j = i; + while (j < counts.length && counts[j].votes === currentVotes) { group.push(counts[j].idx); j++; } + if (group.length <= slots) { + executed.push(...group); + slots -= group.length; + i = j; + } else { + // Tie for remaining slots + if (state.isTiebreak) { + // segunda vez empatados: ganan impostores + state.executed = []; + showResults(true); + return; + } + startTiebreakDeliberation(group); + return; + } + } + + state.executed = executed; + showResults(); +} + +// ---------- Resultados ---------- +function showResults(isTiebreak = false) { + state.phase = 'results'; saveState(); + const executed = state.executed || []; + let impostorsAlive = 0; + state.roles.forEach((r,i) => { if (r === 'IMPOSTOR' && !executed.includes(i)) impostorsAlive++; }); + const winner = impostorsAlive > 0 ? 'IMPOSTORES' : 'CIVILES'; + const results = document.getElementById('results-content'); + results.innerHTML = ` +

${winner === 'CIVILES' ? '✅ ¡GANAN LOS CIVILES!' : '❌ ¡GANAN LOS IMPOSTORES!'}

+

Ejecutados: ${executed.length ? executed.map(i => state.playerNames[i]).join(', ') : 'Nadie'}

+

Votos: ${Object.keys(state.votes).length ? '' : 'Sin votos'}

+

Roles revelados

+ ${state.roles.map((role,i) => { + const word = role === 'CIVIL' ? state.civilianWord : state.impostorWord; + const killed = executed.includes(i) ? 'executed' : ''; + return `
${state.playerNames[i]}: ${role} — "${word}" ${killed ? '☠️' : ''}
`; + }).join('')} + `; + showScreen('results-screen'); +} + +// ---------- Utilidades ---------- +function showScreen(id) { + document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); + document.getElementById(id).classList.add('active'); + state.phase = id.replace('-screen',''); + saveState(); +} + +function newMatch() { clearState(); state = { ...state, phase:'setup', timerEndAt:null, timerPhase:null, votingPool:null, isTiebreak:false, tiebreakCandidates:[] }; location.reload(); } + +// ---------- Rehidratación ---------- +(function init() { + const restored = loadState(); + showScreen('setup-screen'); + loadPoolsList(); + if (!state.turnDirection) state.turnDirection = 'horario'; + if (typeof state.startPlayer !== 'number') state.startPlayer = 0; + switch (state.phase) { + case 'setup': showScreen('setup-screen'); break; + case 'names': buildNameInputs(); showScreen('names-screen'); break; + case 'pre-reveal': renderSummary(); showScreen('pre-reveal-screen'); break; + case 'reveal': showScreen('reveal-screen'); loadCurrentReveal(); break; + case 'game': showScreen('game-screen'); resumeTimerIfNeeded(); break; + case 'deliberation': showScreen('deliberation-screen'); resumeTimerIfNeeded(); break; + case 'voting': showScreen('voting-screen'); renderVoting(); break; + case 'results': showResults(); break; + default: showScreen('setup-screen'); + } +})(); diff --git a/styles.26a5b74f.css b/styles.26a5b74f.css new file mode 100644 index 0000000..5b64e14 --- /dev/null +++ b/styles.26a5b74f.css @@ -0,0 +1,455 @@ +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + EXPEDIENTE CLASIFICADO - IMPOSTOR GAME + Noir Cyberpunk Interrogation Aesthetic + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Special+Elite&display=swap'); + +:root { + /* LIGHT THEME: Interrogation Room */ + --bg-primary: #dcd9d2; + --bg-secondary: #c8c3b8; + --bg-overlay: rgba(0, 0, 0, 0.05); + + --surface-glass: rgba(255, 255, 255, 0.85); + --surface-card: rgba(255, 255, 255, 0.95); + --surface-hover: rgba(255, 255, 255, 1); + + --text-primary: #0a0a0a; + --text-secondary: #2a2a2a; + --text-tertiary: #5a5a5a; + --text-inverted: #ffffff; + + --accent-warning: #e6a73c; + --accent-danger: #d93626; + --accent-success: #2d8b3d; + --accent-info: #2e4e7a; + + --border-light: rgba(0, 0, 0, 0.18); + --border-medium: rgba(0, 0, 0, 0.35); + --border-heavy: rgba(0, 0, 0, 0.55); + + --shadow-sm: 0 3px 12px rgba(0, 0, 0, 0.15); + --shadow-md: 0 6px 24px rgba(0, 0, 0, 0.22); + --shadow-lg: 0 12px 48px rgba(0, 0, 0, 0.28); + --shadow-harsh: 6px 6px 0px rgba(0, 0, 0, 0.25); + + --grain-opacity: 0.05; + --scanline-opacity: 0.025; + + /* Spotlight effect */ + --spotlight-color: rgba(255, 235, 180, 0.08); + --vignette-intensity: 0.4; +} + +[data-theme="dark"] { + /* DARK THEME: Night Investigation */ + --bg-primary: #050505; + --bg-secondary: #0f0f0f; + --bg-overlay: rgba(255, 255, 255, 0.03); + + --surface-glass: rgba(25, 25, 25, 0.9); + --surface-card: rgba(35, 35, 35, 0.95); + --surface-hover: rgba(45, 45, 45, 1); + + --text-primary: #f5f5f5; + --text-secondary: #d0d0d0; + --text-tertiary: #909090; + --text-inverted: #0a0a0a; + + --accent-warning: #ffb84d; + --accent-danger: #ff3d2e; + --accent-success: #3dd46b; + --accent-info: #4d8ce0; + + --border-light: rgba(255, 255, 255, 0.12); + --border-medium: rgba(255, 255, 255, 0.22); + --border-heavy: rgba(255, 255, 255, 0.35); + + --shadow-sm: 0 3px 12px rgba(0, 0, 0, 0.6); + --shadow-md: 0 6px 24px rgba(0, 0, 0, 0.8); + --shadow-lg: 0 12px 48px rgba(0, 0, 0, 0.95); + --shadow-harsh: 6px 6px 0px rgba(0, 0, 0, 0.7); + + --grain-opacity: 0.07; + --scanline-opacity: 0.035; + + /* Spotlight effect */ + --spotlight-color: rgba(255, 200, 100, 0.04); + --vignette-intensity: 0.6; +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + BASE STYLES & TYPOGRAPHY + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + -webkit-tap-highlight-color: transparent; +} + +body { + font-family: 'JetBrains Mono', 'Courier Prime', 'Courier New', monospace; + background: + radial-gradient(ellipse 80% 50% at 50% 20%, var(--spotlight-color) 0%, transparent 50%), + radial-gradient(circle at 20% 30%, rgba(230, 167, 60, 0.08) 0%, transparent 40%), + radial-gradient(circle at 80% 70%, rgba(217, 54, 38, 0.06) 0%, transparent 40%), + var(--bg-primary); + min-height: 100vh; + min-height: 100dvh; + display: flex; + justify-content: center; + align-items: center; + padding: 70px 16px 16px; + color: var(--text-primary); + position: relative; + overflow: hidden; + font-size: 14px; + letter-spacing: 0px; + transition: background 0.5s ease, color 0.3s ease; +} + +/* Film grain texture overlay */ +body::before { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.5'/%3E%3C/svg%3E"); + opacity: var(--grain-opacity); + pointer-events: none; + z-index: 9999; + mix-blend-mode: overlay; + animation: grain 8s steps(10) infinite; +} + +@keyframes grain { + 0%, 100% { transform: translate(0, 0); } + 10% { transform: translate(-5%, -10%); } + 20% { transform: translate(-15%, 5%); } + 30% { transform: translate(7%, -25%); } + 40% { transform: translate(-5%, 25%); } + 50% { transform: translate(-15%, 10%); } + 60% { transform: translate(15%, 0%); } + 70% { transform: translate(0%, 15%); } + 80% { transform: translate(3%, 35%); } + 90% { transform: translate(-10%, 10%); } +} + +/* Scanlines */ +body::after { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(0, 0, 0, 0.1) 2px, + rgba(0, 0, 0, 0.1) 4px + ); + opacity: var(--scanline-opacity); + pointer-events: none; + z-index: 9998; +} + +/* Dramatic vignette overlay */ +.vignette-overlay { + position: fixed; + inset: 0; + background: radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,var(--vignette-intensity)) 100%); + pointer-events: none; + z-index: 9997; +} + +/* VHS interference effect */ +@keyframes vhsInterference { + 0%, 100% { opacity: 0; } + 5% { opacity: 0.03; transform: translateX(-2px); } + 10% { opacity: 0; } + 15% { opacity: 0.02; transform: translateX(1px); } + 20% { opacity: 0; } +} + +.vhs-line { + position: fixed; + left: 0; + width: 100%; + height: 3px; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.8), transparent); + pointer-events: none; + z-index: 9996; + animation: vhsScan 8s linear infinite; + opacity: 0.04; +} + +@keyframes vhsScan { + 0% { top: -10px; } + 100% { top: 110%; } +} + +h1 { + font-family: 'Bebas Neue', 'Crimson Text', Georgia, serif; + text-align: center; + margin-bottom: 20px; + font-size: 2.6em; + font-weight: 400; + letter-spacing: 4px; + text-transform: uppercase; + position: relative; + text-shadow: 3px 3px 0px var(--bg-secondary), 0 0 30px rgba(230, 167, 60, 0.2); + line-height: 1.1; + animation: titleReveal 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards; +} + +@keyframes titleReveal { + from { + opacity: 0; + letter-spacing: 20px; + filter: blur(8px); + } + to { + opacity: 1; + letter-spacing: 4px; + filter: blur(0); + } +} + +h1::after { + content: ''; + display: block; + width: 80px; + height: 4px; + background: linear-gradient(90deg, var(--accent-danger) 0%, var(--accent-warning) 50%, var(--accent-danger) 100%); + background-size: 200% 100%; + margin: 14px auto 0; + box-shadow: 0 0 15px rgba(230, 167, 60, 0.5); + animation: shimmer 3s ease-in-out infinite; +} + +@keyframes shimmer { + 0%, 100% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } +} + +h2 { + font-family: 'Crimson Text', Georgia, serif; + text-align: center; + margin: 16px 0; + font-size: 1.4em; + font-weight: 700; + letter-spacing: 0.5px; +} + +h3 { + font-family: 'JetBrains Mono', monospace; + font-size: 1em; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 1.5px; + margin-bottom: 12px; +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + CONTAINER & SCREENS + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.container { + width: 100%; + max-width: 480px; + background: var(--surface-glass); + backdrop-filter: blur(20px) saturate(150%); + border-radius: 0; + padding: 28px 22px; + box-shadow: var(--shadow-harsh), var(--shadow-lg); + border: 4px solid var(--border-heavy); + display: flex; + flex-direction: column; + transition: all 0.4s ease; + margin-bottom: 20px; + position: relative; + overflow: hidden; + clip-path: polygon( + 0 20px, + 20px 0, + 100% 0, + 100% calc(100% - 20px), + calc(100% - 20px) 100%, + 0 100% + ); +} + +.container::before { + content: '⬢ CLASSIFIED ⬢'; + position: absolute; + top: 8px; + left: 50%; + transform: translateX(-50%); + font-size: 0.65em; + letter-spacing: 3px; + opacity: 0.4; + font-weight: 800; + color: var(--accent-danger); + text-shadow: 0 0 10px rgba(217, 54, 38, 0.3); + animation: classifiedPulse 4s ease-in-out infinite; +} + +@keyframes classifiedPulse { + 0%, 100% { opacity: 0.4; text-shadow: 0 0 10px rgba(217, 54, 38, 0.3); } + 50% { opacity: 0.6; text-shadow: 0 0 20px rgba(217, 54, 38, 0.6); } +} + +/* Diagonal classified stamp */ +.container::after { + content: 'EXPEDIENTE'; + position: absolute; + bottom: 15px; + right: -30px; + font-family: 'Special Elite', 'Courier Prime', monospace; + font-size: 0.7em; + letter-spacing: 3px; + color: var(--accent-danger); + opacity: 0.12; + transform: rotate(-45deg); + font-weight: 400; + white-space: nowrap; + pointer-events: none; +} + +.screen { + display: none; + animation: screenEnter 0.35s cubic-bezier(0.22, 1, 0.36, 1); + flex: 1; + overflow: hidden; + min-height: 0; +} + +.screen.active { + display: flex; + flex-direction: column; +} + +@keyframes screenEnter { + 0% { + opacity: 0; + transform: translateY(30px) scale(0.95); + filter: blur(4px); + } + 60% { + opacity: 1; + filter: blur(0); + } + 100% { + opacity: 1; + transform: translateY(0) scale(1); + filter: blur(0); + } +} + +/* Staggered children animation */ +.screen.active > * { + animation: fadeSlideUp 0.5s cubic-bezier(0.22, 1, 0.36, 1) backwards; +} + +.screen.active > *:nth-child(1) { animation-delay: 0.05s; } +.screen.active > *:nth-child(2) { animation-delay: 0.1s; } +.screen.active > *:nth-child(3) { animation-delay: 0.15s; } +.screen.active > *:nth-child(4) { animation-delay: 0.2s; } +.screen.active > *:nth-child(5) { animation-delay: 0.25s; } +.screen.active > *:nth-child(6) { animation-delay: 0.3s; } +.screen.active > *:nth-child(7) { animation-delay: 0.35s; } +.screen.active > *:nth-child(8) { animation-delay: 0.4s; } + +@keyframes fadeSlideUp { + from { + opacity: 0; + transform: translateY(15px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + FORM CONTROLS + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.form-group { + margin-bottom: 16px; +} + +.form-group.compact { + margin-bottom: 12px; +} + +label { + display: block; + margin-bottom: 8px; + font-weight: 700; + font-size: 0.8em; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 1.2px; +} + +input { + width: 100%; + padding: 12px 14px; + border: 2px solid var(--border-medium); + border-radius: 0; + font-size: 0.95em; + font-family: 'JetBrains Mono', monospace; + background: var(--surface-card); + color: var(--text-primary); + transition: all 0.2s ease; + box-shadow: inset 2px 2px 4px rgba(0, 0, 0, 0.1); +} + +input:focus { + outline: none; + border-color: var(--accent-warning); + box-shadow: inset 2px 2px 4px rgba(0, 0, 0, 0.1), 0 0 0 3px rgba(212, 165, 116, 0.2); + transform: translateY(-1px); +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + BUTTONS + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +button { + width: 100%; + padding: 16px 20px; + border: 3px solid var(--text-primary); + border-radius: 0; + font-size: 0.9em; + font-weight: 800; + font-family: 'JetBrains Mono', monospace; + cursor: pointer; + background: var(--text-primary); + color: var(--text-inverted); + box-shadow: var(--shadow-harsh); + transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1); + margin-top: 12px; + text-transform: uppercase; + letter-spacing: 0.8px; + position: relative; + overflow: hidden; + clip-path: polygon( + 0 0, + calc(100% - 12px) 0, + 100% 12px, + 100% 100%, + 12px 100%, + 0 calc(100% - 12px) + ); +} + + From 31541d66f05304b6af70ae0cf02223fc6afc4e7b Mon Sep 17 00:00:00 2001 From: Dasemu Date: Wed, 14 Jan 2026 13:29:00 +0100 Subject: [PATCH 4/9] Revert "feat(styles): remove unused button styles and optimize CSS for better performance" This reverts commit 5bfb857f41ba5daa54be4b31eaaa7e34c0ea38a4. --- script.js | 1079 +++++++++++++++++++++++++++++++++++++---- styles.css | 1374 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 2353 insertions(+), 100 deletions(-) diff --git a/script.js b/script.js index 8e41f46..05c0b74 100644 --- a/script.js +++ b/script.js @@ -3,14 +3,352 @@ 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 = [ - { id: 'animales_naturaleza', name: 'Animales y naturaleza', emoji: '🌿', words: ['Perro','Gato','Lobo','Zorro','Oso','Tigre','León','Pantera','Jaguar','Puma','Guepardo','Elefante','Rinoceronte','Hipopótamo','Jirafa','Cebra','Camello','Dromedario','Canguro','Koala','Panda','Mapache','Nutria','Castor','Foca','Morsa','Delfín','Ballena','Tiburón','Orca','Pulpo','Calamar','Medusa','Tortuga','Lagarto','Cocodrilo','Serpiente','Anaconda','Iguana','Rana','Sapo','Búho','Halcón','Águila','Cóndor','Gaviota','Loro','Flamenco','Pingüino','Avestruz','Gallina','Pato','Ganso','Cisne','Abeja','Hormiga','Mariquita','Libélula','Mariposa','Escarabajo','Grillo','Saltamontes','Araña','Escorpión','Lombriz','Caracol','Estrella de mar','Coral','Musgo','Helecho','Pino','Roble','Encina','Palmera','Cactus','Bambú','Rosa','Tulipán','Girasol','Lavanda','Montaña','Río','Lago','Mar','Playa','Desierto','Selva','Bosque','Pradera','Glaciar','Volcán'] }, - { id: 'vida_cotidiana', name: 'Vida cotidiana', emoji: '🏠', words: ['Pan','Leche','Café','Té','Agua','Jugo','Refresco','Cerveza','Vino','Pizza','Hamburguesa','Sándwich','Taco','Burrito','Pasta','Arroz','Paella','Sushi','Ramen','Ensalada','Sopa','Croqueta','Tortilla','Empanada','Arepa','Queso','Jamón','Chorizo','Pollo','Carne','Cerdo','Pescado','Marisco','Patata','Tomate','Cebolla','Ajo','Pimiento','Zanahoria','Lechuga','Brócoli','Coliflor','Manzana','Plátano','Naranja','Pera','Uva','Fresa','Mango','Piña','Melón','Sandía','Yogur','Galletas','Chocolate','Helado','Cereales','Mantequilla','Aceite','Sal','Pimienta','Azúcar','Harina','Huevo','Cuchara','Tenedor','Cuchillo','Plato','Vaso','Taza','Olla','Sartén','Microondas','Horno','Nevera','Mesa','Silla','Sofá','Cama','Almohada','Sábana','Toalla','Ducha','Jabón','Champú','Cepillo','Pasta de dientes'] }, - { id: 'deportes', name: 'Deportes', emoji: '🏅', words: ['Fútbol','Baloncesto','Tenis','Pádel','Bádminton','Voleibol','Béisbol','Rugby','Hockey hielo','Hockey césped','Golf','Boxeo','MMA','Judo','Karate','Taekwondo','Esgrima','Tiro con arco','Halterofilia','Crossfit','Atletismo','Maratón','Triatlón','Ciclismo ruta','Ciclismo montaña','BMX','Natación','Waterpolo','Surf','Vela','Remo','Piragüismo','Esquí','Snowboard','Patinaje artístico','Patinaje velocidad','Curling','Escalada','Senderismo','Trail running','Parkour','Gimnasia artística','Gimnasia rítmica','Trampolín','Skate','Breakdance','Carreras coches','Fórmula 1','Rally','Karting','Motociclismo','Enduro','Motocross','Equitación','Polo','Críquet','Billar','Dardos','Petanca','Pickleball','Ultimate frisbee','Paintball','Airsoft','eSports'] }, - { id: 'marcas', name: 'Marcas', emoji: '🛍️', words: ['Apple','Samsung','Google','Microsoft','Amazon','Meta','Tesla','Toyota','Honda','Ford','BMW','Mercedes','Audi','Volkswagen','Porsche','Ferrari','Lamborghini','Maserati','McLaren','Chevrolet','Nissan','Kia','Hyundai','Peugeot','Renault','Volvo','Jaguar','Land Rover','Fiat','Alfa Romeo','Ducati','Yamaha','Canon','Nikon','Sony','Panasonic','LG','Philips','Siemens','Bosch','Whirlpool','Ikea','Zara','H&M','Uniqlo','Nike','Adidas','Puma','Reebok','New Balance','Under Armour','Converse','Vans','Patagonia','The North Face','Columbia','Levi’s','Calvin Klein','Gucci','Prada','Louis Vuitton','Chanel','Hermès','Dior','Rolex','Omega','Casio','Pepsi','Coca-Cola','Fanta','Red Bull','Monster','Starbucks','Nespresso','Nestlé','Danone','Kellogg’s','Oreo','Intel','AMD','Nvidia','Qualcomm','TikTok','Netflix','Disney','Warner Bros','HBO','Spotify','Airbnb','Uber','Booking'] }, - { id: 'musica', name: 'Música', emoji: '🎵', words: ['Guitarra','Piano','Violín','Batería','Bajo','Saxofón','Trompeta','Flauta','Clarinete','Acordeón','Ukelele','Arpa','Sintetizador','DJ','Micrófono','Altavoz','Concierto','Festival','Vinilo','Rock','Pop','Punk','Metal','Heavy','Thrash','Death metal','Jazz','Blues','Soul','Funk','R&B','Rap','Hip hop','Trap','Reggaetón','Salsa','Bachata','Merengue','Cumbia','Vallenato','Flamenco','Rumba','Bossa nova','Samba','Tango','Country','EDM','Techno','House','Trance','Dubstep','Drum and bass','Lo-fi','Reggae','Ska','K-pop','J-pop','Indie','Gospel','Ópera','Sinfonía','Orquesta','Coro','Cantautor','Balada','Bolero','Ranchera','Corrido','Mariachi'] }, - { id: 'personajes', name: 'Personajes', emoji: '🧙', words: ['Sherlock Holmes','Harry Potter','Hermione Granger','Ron Weasley','Albus Dumbledore','Voldemort','Frodo Bolsón','Sam Gamyi','Gandalf','Aragorn','Legolas','Gimli','Gollum','Bilbo Bolsón','Katniss Everdeen','Peeta Mellark','Batman','Bruce Wayne','Joker','Harley Quinn','Superman','Clark Kent','Lois Lane','Wonder Woman','Diana Prince','Flash','Barry Allen','Aquaman','Arthur Curry','Spider-Man','Peter Parker','Iron Man','Tony Stark','Capitán América','Steve Rogers','Black Widow','Natasha Romanoff','Hulk','Bruce Banner','Thor','Loki','Thanos','Doctor Strange','Wanda Maximoff','Vision','Star-Lord','Gamora','Groot','Rocket','Drax','Deadpool','Wolverine','Magneto','Professor X','Storm','Cyclops','Jean Grey','Mystique','Darth Vader','Luke Skywalker','Leia Organa','Han Solo','Chewbacca','Yoda','Obi-Wan Kenobi','Anakin Skywalker','Rey','Kylo Ren','R2-D2','C-3PO','Indiana Jones','Lara Croft','James Bond','Mario','Luigi','Princesa Peach','Bowser','Link','Zelda','Geralt de Rivia','Ciri','Yennefer','Kratos','Atreus','Ellie','Joel Miller','Nathan Drake','Master Chief','Cortana','Sonic','Tails','Ash Ketchum','Pikachu','Goku','Vegeta','Naruto','Sasuke','Luffy','Zoro','Nami','Tanjiro','Nezuko','Saitama','Light Yagami','L Lawliet'] } + // 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 = []; @@ -36,7 +374,7 @@ let state = { votingPlayer: 0, selections: [], executed: [], - selectedPool: 'animales_naturaleza', + selectedPools: [], // Now it's an array for multiple pools, will be populated based on language votingPool: null, isTiebreak: false, tiebreakCandidates: [] @@ -55,7 +393,7 @@ const loadPoolsCache = () => { }; const savePoolsCache = () => localStorage.setItem(POOLS_CACHE_KEY, JSON.stringify(poolsCache)); -// ---------- Defaults ---------- +// ---------- Default values ---------- function defaultImpostors(nPlayers) { const capped = Math.min(Math.max(nPlayers, MIN_PLAYERS), MAX_PLAYERS); let impostors = 1; @@ -78,63 +416,162 @@ function defaultDeliberation(gameSeconds) { return Math.max(30, Math.round(gameSeconds / 3)); } -// ---------- Pools ---------- +// ---------- Word Pools ---------- async function loadPoolsList() { loadPoolsCache(); - let list = []; + + // 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) list = await res.json(); - } catch (_) {} - if (!Array.isArray(list) || list.length === 0) { - list = EMBEDDED_POOLS.map(p => ({ id: p.id, name: p.name, emoji: p.emoji, count: p.words.length })); + if (res.ok) { + const manifest = await res.json(); + if (Array.isArray(manifest)) { + externalList = manifest; + } + } + } catch (e) { + console.log('Failed to load manifest:', e); } - availablePools = list; + + // 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(Boolean); - if (!lines.length) return []; - if (lines[0].startsWith('#')) return lines.slice(1); - return lines; + 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 poolId = state.selectedPool || 'default'; - let words = []; - if (poolsCache[poolId]?.words) { - words = poolsCache[poolId].words; - } else if (poolId !== 'default') { - const res = await fetch(`word-pools/${poolId}.txt`); - if (!res.ok) throw new Error('No se pudo cargar el pool'); - const text = await res.text(); - words = parseWordsFile(text); - poolsCache[poolId] = { words, ts: Date.now() }; savePoolsCache(); - } else { - words = EMBEDDED_POOLS[0].words; + 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); } - const shuffled = [...words].sort(() => Math.random() - 0.5); - return { civilian: shuffled[0], impostor: shuffled[1] }; + + 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.selectedPool === pool.id) btn.classList.add('selected'); - btn.onclick = () => { state.selectedPool = pool.id; saveState(); renderPoolButtons(); }; + 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); }); } -// ---------- Configuración y nombres ---------- -function goToNames() { +// ---------- 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)); @@ -144,8 +581,15 @@ function goToNames() { 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('Impostores debe ser menor que jugadores'); return; } - state.numPlayers = nPlayers; state.numImpostors = nImpostors; state.gameTime = gTime; state.deliberationTime = dTime; + 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'); } @@ -156,12 +600,13 @@ function buildNameInputs() { for (let i = 0; i < state.numPlayers; i++) { const div = document.createElement('div'); div.className = 'player-name-item'; - div.innerHTML = `Jugador ${i+1}:`; + const playerLabel = `${t('player')} ${i+1}`; + div.innerHTML = `${playerLabel}:`; list.appendChild(div); } } -// ---------- Inicio de partida ---------- +// ---------- Game start ---------- function startGame() { state.playerNames = []; for (let i = 0; i < state.numPlayers; i++) { @@ -175,8 +620,9 @@ function startGame() { }).catch(() => { const fallback = EMBEDDED_POOLS[0].words; const shuffled = [...fallback].sort(() => Math.random() - 0.5); - state.civilianWord = shuffled[0]; - state.impostorWord = shuffled[1]; + const wordPair = shuffled[0]; + state.civilianWord = wordPair[0]; + state.impostorWord = wordPair[1]; finalizeStart(); }); } @@ -194,7 +640,7 @@ function finalizeStart() { showScreen('pre-reveal-screen'); } -// Ajustar defaults cuando se edita el nº de jugadores +// 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); @@ -211,21 +657,32 @@ document.getElementById('num-players').addEventListener('change', () => { 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] || `Jugador ${state.startPlayer+1}`; - const poolMeta = availablePools.find(p => p.id === state.selectedPool) || EMBEDDED_POOLS[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 = ` -

Jugadores: ${state.numPlayers}

-

Impostores: ${state.numImpostors}

-

Tiempo de partida: ${fmt(state.gameTime)}

-

Tiempo de deliberación: ${fmt(state.deliberationTime)}

-

Pool: ${poolMeta.emoji || '🎲'} ${poolMeta.name || poolMeta.id}

-

Empieza: ${startName} · Orden: ${state.turnDirection === 'horario' ? 'Horario' : 'Antihorario'}

+

${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')}

`; } -// ---------- Revelación ---------- +// ---------- 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); @@ -233,7 +690,24 @@ function loadCurrentReveal() { const idx = state.revealOrder[state.currentReveal]; const name = state.playerNames[idx]; document.getElementById('current-player-name').textContent = name; - document.getElementById('curtain-cover').classList.remove('lifted'); + + // Update curtain text + const revealText = document.querySelector('#reveal-screen .info-text'); + if (revealText) { + revealText.innerHTML = `${t('turnOf')}: ${name}
${t('othersLookAway')}`; + } + + const curtainText = document.querySelector('.curtain-cover div:last-child'); + if (curtainText) { + curtainText.textContent = t('liftCurtain'); + } + + // Reset curtain state + curtainState.isRevealed = false; + const coverEl = document.getElementById('curtain-cover'); + coverEl.style.transform = 'translateY(0)'; + coverEl.style.transition = ''; + document.getElementById('next-player-btn').style.display = 'none'; document.getElementById('start-game-btn').style.display = 'none'; } @@ -241,7 +715,12 @@ function loadCurrentReveal() { function liftCurtain() { const cover = document.getElementById('curtain-cover'); if (cover.classList.contains('lifted')) return; + + // Restore CSS transition and use the class + cover.style.transition = ''; + cover.style.transform = ''; cover.classList.add('lifted'); + const idx = state.revealOrder[state.currentReveal]; const role = state.roles[idx]; const word = role === 'CIVIL' ? state.civilianWord : state.impostorWord; @@ -256,19 +735,207 @@ function liftCurtain() { function nextReveal() { state.currentReveal++; saveState(); loadCurrentReveal(); } -// swipe support -(() => { +// Curtain system with GRAVITY - The curtain always tends to fall +// Supports both touch (mobile) and mouse (desktop) +// On desktop: curtain stays up while mouse button is held, even if cursor leaves the area +let curtainState = { isRevealed: false }; +let curtainDragState = { + startY: null, + isDragging: false, + currentTranslateY: 0 +}; + +function initCurtainHandlers() { const curtain = document.getElementById('curtain'); - let startY = null; - curtain.addEventListener('touchstart', e => { startY = e.touches[0].clientY; }, {passive:true}); - curtain.addEventListener('touchmove', e => { if (startY === null) return; const dy = e.touches[0].clientY - startY; if (dy < -40) { liftCurtain(); startY = null; } }, {passive:true}); - curtain.addEventListener('click', liftCurtain); -})(); + if (!curtain) return; + + // Function to get Y position from event (touch or mouse) + const getY = (e) => { + return e.touches ? e.touches[0].clientY : e.clientY; + }; + + // Start function (touch and mouse) + const handleStart = (e) => { + curtainDragState.startY = getY(e); + curtainDragState.isDragging = true; + curtainDragState.currentTranslateY = 0; + if (e.type === 'mousedown') { + e.preventDefault(); // Prevent text selection on desktop + } + }; + + // Move function (touch and mouse) + const handleMove = (e) => { + if (curtainDragState.startY === null || !curtainDragState.isDragging) return; + const currentY = getY(e); + const dy = currentY - curtainDragState.startY; + const coverEl = document.getElementById('curtain-cover'); + if (!coverEl) return; + + // Calculate displacement: negative = up, positive = down + // Allow going further up than the curtain height (user can keep dragging up) + // but don't allow going below initial position (0) + curtainDragState.currentTranslateY = Math.min(dy, 0); + + coverEl.style.transform = `translateY(${curtainDragState.currentTranslateY}px)`; + coverEl.style.transition = 'none'; + + // If lifted enough, show content + if (curtainDragState.currentTranslateY < -80 && !curtainState.isRevealed) { + curtainState.isRevealed = true; + const idx = state.revealOrder[state.currentReveal]; + const role = state.roles[idx]; + const word = role === 'CIVIL' ? state.civilianWord : state.impostorWord; + const roleText = t(role.toLowerCase()); + document.getElementById('role-text').textContent = roleText; + document.getElementById('role-text').className = 'role ' + (role === 'CIVIL' ? 'civil' : 'impostor'); + document.getElementById('word-text').textContent = word; + } + + e.preventDefault(); // Prevent selection + }; + + // End function (touch and mouse) + const handleEnd = (e) => { + if (!curtainDragState.isDragging || curtainDragState.startY === null) return; + const coverEl = document.getElementById('curtain-cover'); + if (!coverEl) return; + + // ALWAYS bring the curtain down when released (GRAVITY) + coverEl.style.transition = 'transform 0.4s ease'; + coverEl.style.transform = 'translateY(0)'; + + // If content was revealed, show button after it falls + if (curtainState.isRevealed) { + setTimeout(() => { + if (state.currentReveal + 1 < state.numPlayers) { + document.getElementById('next-player-btn').style.display = 'block'; + } else { + document.getElementById('start-game-btn').style.display = 'block'; + } + }, 400); + } + + curtainDragState.startY = null; + curtainDragState.isDragging = false; + curtainDragState.currentTranslateY = 0; + }; + + // Touch events (mobile) + curtain.addEventListener('touchstart', handleStart, {passive: false}); + curtain.addEventListener('touchmove', handleMove, {passive: false}); + curtain.addEventListener('touchend', handleEnd, {passive: true}); + curtain.addEventListener('touchcancel', handleEnd, {passive: true}); + + // Mouse events (desktop) - start on curtain only + curtain.addEventListener('mousedown', handleStart); + + // Mouse move and up events on WINDOW so we can track even when cursor leaves everything + window.addEventListener('mousemove', handleMove); + window.addEventListener('mouseup', handleEnd); +} + +// Initialize curtain handlers when DOM is ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initCurtainHandlers); +} else { + initCurtainHandlers(); +} + +// ---------- Screen Wake Lock (prevent screen from sleeping during timers) ---------- +let wakeLock = null; +let wakeLockVideo = null; // For iOS workaround + +// Detect if device is iOS +function isIOS() { + return /iPad|iPhone|iPod/.test(navigator.userAgent) || + (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); +} + +// Check if screen lock is enabled in settings +function isScreenLockEnabled() { + const saved = localStorage.getItem(SCREEN_LOCK_STORAGE_KEY); + return saved === null ? true : saved === 'true'; // Default enabled +} + +// Save screen lock preference +function setScreenLockEnabled(enabled) { + localStorage.setItem(SCREEN_LOCK_STORAGE_KEY, enabled.toString()); + updateScreenLockButton(); +} + +async function requestWakeLock() { + if (!isScreenLockEnabled()) return; + + // Try native Wake Lock API first (works on Android Chrome, etc.) + if ('wakeLock' in navigator) { + try { + wakeLock = await navigator.wakeLock.request('screen'); + wakeLock.addEventListener('release', () => { + wakeLock = null; + }); + console.log('Wake Lock activated (native API)'); + return; + } catch (err) { + console.log('Wake lock request failed:', err); + } + } + + // Fallback for iOS - use hidden video loop + if (isIOS() && !wakeLockVideo) { + try { + wakeLockVideo = document.createElement('video'); + wakeLockVideo.setAttribute('playsinline', ''); + wakeLockVideo.setAttribute('muted', ''); + wakeLockVideo.style.position = 'fixed'; + wakeLockVideo.style.opacity = '0'; + wakeLockVideo.style.pointerEvents = 'none'; + wakeLockVideo.style.width = '1px'; + wakeLockVideo.style.height = '1px'; + + // Minimal base64 encoded video (1 frame, silent) + wakeLockVideo.src = 'data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAAAu1tZGF0AAACrQYF//+p3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE1NSByMjkwMSA3ZDBmZjIyIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAxOCAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTMgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MzoweDExMyBtZT1oZXggc3VibWU9NyBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0xIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MSA4eDhkY3Q9MSBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hyb21hX3FwX29mZnNldD0tMiB0aHJlYWRzPTMgbG9va2FoZWFkX3RocmVhZHM9MSBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRlcmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIGNvbnN0cmFpbmVkX2ludHJhPTAgYmZyYW1lcz0zIGJfcHlyYW1pZD0yIGJfYWRhcHQ9MSBiX2JpYXM9MCBkaXJlY3Q9MSB3ZWlnaHRiPTEgb3Blbl9nb3A9MCB3ZWlnaHRwPTIga2V5aW50PTI1MCBrZXlpbnRfbWluPTI1IHNjZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NDAgcmM9Y3JmIG1idHJlZT0xIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAAAwWWIhAAz//727L4FNf2f0JcRLMXaSnA+KqSAgHc0wAAAAwAAAwAAJuKiZ0WFMeJsgAAAHGAFBCwCPCVC'; + wakeLockVideo.loop = true; + + document.body.appendChild(wakeLockVideo); + await wakeLockVideo.play(); + console.log('Wake Lock activated (iOS video workaround)'); + } catch (err) { + console.log('iOS wake lock workaround failed:', err); + } + } +} + +function releaseWakeLock() { + // Release native Wake Lock + if (wakeLock) { + wakeLock.release(); + wakeLock = null; + } + + // Stop iOS video workaround + if (wakeLockVideo) { + wakeLockVideo.pause(); + wakeLockVideo.remove(); + wakeLockVideo = null; + } +} + +// Re-request wake lock when page becomes visible again +document.addEventListener('visibilitychange', async () => { + if (document.visibilityState === 'visible' && (wakeLock !== null || wakeLockVideo !== null)) { + await requestWakeLock(); + } +}); // ---------- Timers ---------- let timerInterval = null; -function startPhaseTimer(phase, seconds, elementId, onEnd) { +async function startPhaseTimer(phase, seconds, elementId, onEnd) { if (timerInterval) clearInterval(timerInterval); + + // Request wake lock to keep screen on during timer + await requestWakeLock(); + const now = Date.now(); state.timerPhase = phase; state.timerEndAt = now + seconds*1000; @@ -277,7 +944,12 @@ function startPhaseTimer(phase, seconds, elementId, onEnd) { const tick = () => { const remaining = Math.max(0, Math.round((state.timerEndAt - Date.now())/1000)); updateTimerDisplay(el, remaining); - if (remaining <= 0) { clearInterval(timerInterval); playBeep(); onEnd(); } + if (remaining <= 0) { + clearInterval(timerInterval); + releaseWakeLock(); // Release wake lock when timer ends + playBeep(); + onEnd(); + } }; tick(); timerInterval = setInterval(tick, 1000); @@ -299,17 +971,48 @@ function updateTimerDisplay(el, remaining) { } function playBeep() { + // Play alarm sound - 3 ascending beeps pattern repeated twice const ctx = new (window.AudioContext || window.webkitAudioContext)(); - const osc = ctx.createOscillator(); const gain = ctx.createGain(); - osc.connect(gain); gain.connect(ctx.destination); osc.frequency.value = 820; osc.type = 'sine'; - gain.gain.setValueAtTime(0.3, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.45); - osc.start(); osc.stop(ctx.currentTime + 0.45); + const now = ctx.currentTime; + + // Frequencies for alarm pattern (ascending) + const frequencies = [523, 659, 784]; // C5, E5, G5 + const beepDuration = 0.15; + const gapDuration = 0.08; + const patternGap = 0.3; + + let time = now; + + // Play pattern twice + for (let pattern = 0; pattern < 2; pattern++) { + for (let i = 0; i < frequencies.length; i++) { + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + + osc.connect(gain); + gain.connect(ctx.destination); + osc.frequency.value = frequencies[i]; + osc.type = 'square'; // More alarm-like sound + + gain.gain.setValueAtTime(0, time); + gain.gain.linearRampToValueAtTime(0.25, time + 0.02); + gain.gain.setValueAtTime(0.25, time + beepDuration - 0.02); + gain.gain.linearRampToValueAtTime(0, time + beepDuration); + + osc.start(time); + osc.stop(time + beepDuration); + + time += beepDuration + gapDuration; + } + time += patternGap; + } } -// ---------- Fases ---------- +// ---------- Game phases ---------- function startGamePhase() { state.phase = 'game'; saveState(); showScreen('game-screen'); startPhaseTimer('game', state.gameTime, 'game-timer', startDeliberationPhase); } function startDeliberationPhase() { state.phase = 'deliberation'; saveState(); showScreen('deliberation-screen'); startPhaseTimer('deliberation', state.deliberationTime, 'deliberation-timer', startVotingPhase); } function startVotingPhase(candidates = null, isTiebreak = false) { + releaseWakeLock(); // Release wake lock when voting starts (no timer) state.phase = 'voting'; state.votingPlayer = 0; state.votes = {}; @@ -320,8 +1023,8 @@ function startVotingPhase(candidates = null, isTiebreak = false) { renderVoting(); showScreen('voting-screen'); } -function skipToDeliberation() { if (timerInterval) clearInterval(timerInterval); startDeliberationPhase(); } -function skipToVoting() { if (timerInterval) clearInterval(timerInterval); startVotingPhase(); } +function skipToDeliberation() { if (timerInterval) clearInterval(timerInterval); releaseWakeLock(); startDeliberationPhase(); } +function skipToVoting() { if (timerInterval) clearInterval(timerInterval); releaseWakeLock(); startVotingPhase(); } function startTiebreakDeliberation(candidates) { state.phase = 'deliberation'; state.tiebreakCandidates = candidates; @@ -330,27 +1033,40 @@ function startTiebreakDeliberation(candidates) { startPhaseTimer('deliberation', 60, 'deliberation-timer', () => startVotingPhase(candidates, true)); } -// ---------- Votación secreta ---------- +// ---------- Secret voting ---------- function renderVoting() { const pool = state.votingPool || Array.from({length: state.numPlayers}, (_, i) => i); const voter = state.playerNames[state.votingPlayer]; document.getElementById('voter-name').textContent = voter; document.getElementById('votes-needed').textContent = state.numImpostors; + + // Update voting instruction text + const votingInfo = document.querySelector('#voting-screen .info-text'); + if (votingInfo) { + votingInfo.innerHTML = `${t('passMobileTo')} ${voter}. ${t('chooseSuspects')} ${state.numImpostors} ${t('suspect')}.`; + } + state.selections = state.selections || []; const list = document.getElementById('vote-list'); list.innerHTML = ''; pool.forEach(i => { const item = document.createElement('div'); item.className = 'player-item'; - item.textContent = state.playerNames[i]; - if (state.votes[i]) item.innerHTML += `Votos: ${state.votes[i]}`; - if (state.selections.includes(i)) item.classList.add('selected'); + + // Marcar como disabled ANTES de añadir al DOM para que la animación correcta se aplique if (i === state.votingPlayer) { item.classList.add('disabled'); - item.style.opacity = '0.5'; + // NO aplicar opacity inline - dejamos que CSS lo maneje con la animación item.style.pointerEvents = 'none'; - } else { - item.onclick = () => toggleSelection(i, item); - } + } + + item.textContent = state.playerNames[i]; + if (state.votes[i]) item.innerHTML += `${t('votes')}: ${state.votes[i]}`; + if (state.selections.includes(i)) item.classList.add('selected'); + + if (i !== state.votingPlayer) { + item.onclick = () => toggleSelection(i, item); + } + list.appendChild(item); }); updateConfirmButton(); @@ -381,7 +1097,7 @@ function confirmCurrentVote() { renderVoting(); } -// ---------- Resolución de voto ---------- +// ---------- Vote resolution ---------- function handleVoteOutcome() { const pool = state.votingPool || Array.from({length: state.numPlayers}, (_, i) => i); const counts = pool.map(idx => ({ idx, votes: state.votes[idx] || 0 })); @@ -401,7 +1117,7 @@ function handleVoteOutcome() { } else { // Tie for remaining slots if (state.isTiebreak) { - // segunda vez empatados: ganan impostores + // Second tie: impostors win state.executed = []; showResults(true); return; @@ -415,54 +1131,217 @@ function handleVoteOutcome() { showResults(); } -// ---------- Resultados ---------- +// ---------- Results ---------- function showResults(isTiebreak = false) { state.phase = 'results'; saveState(); + + // Liberar Wake Lock cuando termina la partida + releaseWakeLock(); + const executed = state.executed || []; let impostorsAlive = 0; state.roles.forEach((r,i) => { if (r === 'IMPOSTOR' && !executed.includes(i)) impostorsAlive++; }); const winner = impostorsAlive > 0 ? 'IMPOSTORES' : 'CIVILES'; const results = document.getElementById('results-content'); + const winText = winner === 'CIVILES' ? `✅ ${t('civiliansWin')}` : `❌ ${t('impostorsWin')}`; results.innerHTML = ` -

${winner === 'CIVILES' ? '✅ ¡GANAN LOS CIVILES!' : '❌ ¡GANAN LOS IMPOSTORES!'}

-

Ejecutados: ${executed.length ? executed.map(i => state.playerNames[i]).join(', ') : 'Nadie'}

-

Votos: ${Object.keys(state.votes).length ? '' : 'Sin votos'}

-

Roles revelados

+

${winText}

+

${t('executed')}: ${executed.length ? executed.map(i => state.playerNames[i]).join(', ') : t('nobody')}

+

${t('votes')}: ${Object.keys(state.votes).length ? '' : t('noVotes')}

+

${t('revealedRoles')}

${state.roles.map((role,i) => { const word = role === 'CIVIL' ? state.civilianWord : state.impostorWord; const killed = executed.includes(i) ? 'executed' : ''; - return `
${state.playerNames[i]}: ${role} — "${word}" ${killed ? '☠️' : ''}
`; + const roleText = t(role.toLowerCase()); + return `
${state.playerNames[i]}: ${roleText} — "${word}" ${killed ? '☠️' : ''}
`; }).join('')} `; showScreen('results-screen'); } -// ---------- Utilidades ---------- +// ---------- Utilities ---------- function showScreen(id) { document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); document.getElementById(id).classList.add('active'); state.phase = id.replace('-screen',''); saveState(); + updateExitButtonVisibility(); } -function newMatch() { clearState(); state = { ...state, phase:'setup', timerEndAt:null, timerPhase:null, votingPool:null, isTiebreak:false, tiebreakCandidates:[] }; location.reload(); } +function newMatch() { + clearState(); + releaseWakeLock(); // Make sure wake lock is released when exiting game + if (timerInterval) clearInterval(timerInterval); + state = { ...state, phase:'welcome', timerEndAt:null, timerPhase:null, votingPool:null, isTiebreak:false, tiebreakCandidates:[] }; + saveState(); + showScreen('welcome-screen'); +} -// ---------- Rehidratación ---------- +function confirmExitGame() { + const confirmMessage = currentLanguage === 'es' + ? '¿Estás seguro de que quieres salir de la partida? Se perderá todo el progreso actual.' + : 'Are you sure you want to exit the game? All current progress will be lost.'; + + if (confirm(confirmMessage)) { + newMatch(); + } +} + +function updateExitButtonVisibility() { + const exitBtn = document.getElementById('exit-game'); + const langBtn = document.getElementById('language-toggle'); + const screenLockBtn = document.getElementById('screen-lock-toggle'); + + // Show exit button and hide language/screen-lock toggles in all phases except welcome and setup + if (state.phase !== 'welcome' && state.phase !== 'setup') { + exitBtn.classList.add('visible'); + if (langBtn) langBtn.style.display = 'none'; + if (screenLockBtn) screenLockBtn.classList.remove('visible'); + } else { + exitBtn.classList.remove('visible'); + if (langBtn) langBtn.style.display = 'inline-flex'; + // Only show screen lock button on iOS + if (screenLockBtn && isIOS()) { + screenLockBtn.classList.add('visible'); + } + } +} + +// ---------- Theme system ---------- +function getSystemTheme() { + return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; +} + +function loadTheme() { + const savedTheme = localStorage.getItem(THEME_STORAGE_KEY); + return savedTheme || getSystemTheme(); +} + +function saveTheme(theme) { + localStorage.setItem(THEME_STORAGE_KEY, theme); +} + +function applyTheme(theme) { + document.documentElement.setAttribute('data-theme', theme); + const themeIcon = document.querySelector('.theme-icon'); + if (themeIcon) { + themeIcon.textContent = theme === 'dark' ? '☀️' : '🌙'; + } +} + +function toggleTheme() { + const currentTheme = document.documentElement.getAttribute('data-theme') || 'light'; + const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; + applyTheme(newTheme); + saveTheme(newTheme); +} + +// Initialize theme +const initialTheme = loadTheme(); +applyTheme(initialTheme); + +// ---------- Screen Lock Button ---------- +function updateScreenLockButton() { + const btn = document.getElementById('screen-lock-toggle'); + if (!btn) return; + + const enabled = isScreenLockEnabled(); + const icon = btn.querySelector('.screen-lock-icon'); + + if (enabled) { + btn.classList.add('active'); + btn.setAttribute('title', 'Bloqueo de pantalla activado'); + if (icon) icon.textContent = '🔒'; + } else { + btn.classList.remove('active'); + btn.setAttribute('title', 'Bloqueo de pantalla desactivado'); + if (icon) icon.textContent = '🔓'; + } +} + +function toggleScreenLock() { + const currentState = isScreenLockEnabled(); + setScreenLockEnabled(!currentState); + + // If disabling, release any active wake lock + if (currentState) { + releaseWakeLock(); + } +} + +// Event listener for theme and language buttons +document.addEventListener('DOMContentLoaded', () => { + const themeToggle = document.getElementById('theme-toggle'); + if (themeToggle) { + themeToggle.addEventListener('click', toggleTheme); + } + + const languageToggle = document.getElementById('language-toggle'); + if (languageToggle) { + languageToggle.addEventListener('click', toggleLanguage); + } + + const screenLockToggle = document.getElementById('screen-lock-toggle'); + if (screenLockToggle) { + screenLockToggle.addEventListener('click', toggleScreenLock); + updateScreenLockButton(); + } + + // Initialize language + currentLanguage = loadLanguage(); + setLanguage(currentLanguage); + + // Detect system theme changes + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { + // Only apply automatically if user hasn't manually selected a theme + if (!localStorage.getItem(THEME_STORAGE_KEY)) { + applyTheme(e.matches ? 'dark' : 'light'); + } + }); +}); + +// ---------- State rehydration ---------- (function init() { const restored = loadState(); - showScreen('setup-screen'); loadPoolsList(); if (!state.turnDirection) state.turnDirection = 'horario'; if (typeof state.startPlayer !== 'number') state.startPlayer = 0; - switch (state.phase) { - case 'setup': showScreen('setup-screen'); break; - case 'names': buildNameInputs(); showScreen('names-screen'); break; - case 'pre-reveal': renderSummary(); showScreen('pre-reveal-screen'); break; - case 'reveal': showScreen('reveal-screen'); loadCurrentReveal(); break; - case 'game': showScreen('game-screen'); resumeTimerIfNeeded(); break; - case 'deliberation': showScreen('deliberation-screen'); resumeTimerIfNeeded(); break; - case 'voting': showScreen('voting-screen'); renderVoting(); break; - case 'results': showResults(); break; - default: showScreen('setup-screen'); + + // Set default values in inputs if we're in setup + if (state.phase === 'setup' || !restored) { + const defaultPlayers = 6; + const defaultImp = defaultImpostors(defaultPlayers); + const defaultGTime = defaultGameTime(defaultPlayers); + const defaultDTime = defaultDeliberation(defaultGTime); + + document.getElementById('num-players').value = defaultPlayers; + document.getElementById('num-impostors').value = defaultImp; + document.getElementById('num-impostors').max = Math.max(1, Math.floor(defaultPlayers / 2)); + document.getElementById('game-time').value = defaultGTime; + document.getElementById('deliberation-time').value = defaultDTime; } + + // Determine initial screen + if (!restored || state.phase === 'setup' || state.phase === 'welcome') { + // If no saved state or we're in setup/welcome, show welcome + showScreen('welcome-screen'); + } else { + // If there's a game in progress, restore it + switch (state.phase) { + case 'names': buildNameInputs(); showScreen('names-screen'); break; + case 'pre-reveal': renderSummary(); showScreen('pre-reveal-screen'); break; + case 'reveal': showScreen('reveal-screen'); loadCurrentReveal(); break; + case 'game': showScreen('game-screen'); resumeTimerIfNeeded(); break; + case 'deliberation': showScreen('deliberation-screen'); resumeTimerIfNeeded(); break; + case 'voting': showScreen('voting-screen'); renderVoting(); break; + case 'results': showResults(); break; + default: showScreen('welcome-screen'); + } + } + + // Initialize exit button visibility + updateExitButtonVisibility(); + + // Initialize screen lock button for iOS + initScreenLockButton(); })(); diff --git a/styles.css b/styles.css index 5b64e14..8e4ebaf 100644 --- a/styles.css +++ b/styles.css @@ -452,4 +452,1378 @@ button { ); } +button::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + transform: translate(-50%, -50%); + transition: width 0.5s ease, height 0.5s ease; +} +button:hover::before { + width: 300px; + height: 300px; +} + +button:hover { + box-shadow: 8px 8px 0px rgba(0, 0, 0, 0.3); + filter: brightness(1.1); +} + +button:active { + box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15); + filter: brightness(0.95); +} + +button.secondary { + background: linear-gradient(135deg, var(--accent-warning) 0%, #c48a2e 100%); + border-color: var(--accent-warning); + color: var(--text-inverted); + box-shadow: var(--shadow-harsh), 0 0 15px rgba(230, 167, 60, 0.25); +} + +button.ghost { + background: transparent; + color: var(--text-primary); + border-color: var(--border-medium); + box-shadow: none; +} + +button.ghost:hover { + background: var(--surface-hover); + box-shadow: var(--shadow-harsh); +} + +button:disabled { + opacity: 0.4; + cursor: not-allowed; + pointer-events: none; +} + +.btn-primary { + background: linear-gradient(135deg, var(--accent-danger) 0%, #b8301e 100%); + border-color: var(--accent-danger); + box-shadow: var(--shadow-harsh), 0 0 20px rgba(217, 54, 38, 0.3); +} + +.btn-secondary { + background: linear-gradient(135deg, var(--accent-info) 0%, #1e3a5f 100%); + border-color: var(--accent-info); + box-shadow: var(--shadow-harsh), 0 0 20px rgba(46, 78, 122, 0.3); +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + WELCOME SCREEN + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.welcome-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + height: 100%; + gap: 24px; + padding: 20px 0; +} + +.welcome-logo { + width: 140px; + height: 140px; + object-fit: contain; + filter: drop-shadow(5px 5px 0px var(--bg-secondary)) + drop-shadow(0 0 30px rgba(230, 167, 60, 0.3)) + grayscale(0.2) contrast(1.15); + animation: logoFloat 4s ease-in-out infinite, logoGlitch 8s step-end infinite; + position: relative; +} + +@keyframes logoFloat { + 0%, 100% { transform: translateY(0) rotate(0deg); } + 25% { transform: translateY(-8px) rotate(-2deg); } + 75% { transform: translateY(-8px) rotate(2deg); } +} + +@keyframes logoGlitch { + 0%, 90%, 100% { + filter: drop-shadow(4px 4px 0px var(--bg-secondary)) + drop-shadow(0 0 20px var(--border-heavy)) + grayscale(0.3) contrast(1.1); + } + 91% { + filter: drop-shadow(6px 4px 0px var(--accent-danger)) + drop-shadow(0 0 20px var(--accent-danger)) + grayscale(0) contrast(1.3); + } + 92% { + filter: drop-shadow(4px 6px 0px var(--accent-info)) + drop-shadow(0 0 20px var(--accent-info)) + grayscale(0) contrast(1.3); + } + 93% { + filter: drop-shadow(4px 4px 0px var(--bg-secondary)) + drop-shadow(0 0 20px var(--border-heavy)) + grayscale(0.3) contrast(1.1); + } +} + +.welcome-title { + font-size: 2.8em; + margin: 0; + font-family: 'Bebas Neue', 'Crimson Text', Georgia, serif; + font-weight: 400; + text-shadow: 4px 4px 0px var(--bg-secondary), 0 0 40px rgba(230, 167, 60, 0.25); + letter-spacing: 6px; + line-height: 1; + position: relative; + animation: welcomeTitleReveal 0.8s cubic-bezier(0.22, 1, 0.36, 1) forwards; +} + +@keyframes welcomeTitleReveal { + 0% { + opacity: 0; + letter-spacing: 30px; + filter: blur(10px); + transform: scale(0.9); + } + 100% { + opacity: 1; + letter-spacing: 6px; + filter: blur(0); + transform: scale(1); + } +} + +.welcome-subtitle { + font-size: 0.95em; + color: var(--text-secondary); + margin: -10px 0 0 0; + font-weight: 400; + letter-spacing: 0.5px; + font-family: 'JetBrains Mono', monospace; +} + +.welcome-buttons { + display: flex; + flex-direction: column; + gap: 12px; + width: 100%; + max-width: 320px; + margin-top: 10px; +} + +.welcome-credits { + color: var(--text-tertiary); + font-size: 0.75em; + margin-top: auto; + font-weight: 400; + letter-spacing: 1px; + text-transform: uppercase; +} + +.welcome-credits::before { + content: '───── '; +} + +.welcome-credits::after { + content: ' ─────'; +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + RULES SCREEN + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.rules-content { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 10px 0; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + -ms-overflow-style: none; +} + +.rules-content::-webkit-scrollbar { + display: none; +} + +.rule-section { + background: var(--surface-card); + border: 3px solid var(--border-medium); + border-left: 8px solid var(--accent-warning); + border-radius: 0; + padding: 18px; + margin-bottom: 16px; + transition: all 0.3s cubic-bezier(0.22, 1, 0.36, 1); + position: relative; + box-shadow: var(--shadow-md); + clip-path: polygon( + 0 0, + 100% 0, + 100% calc(100% - 10px), + calc(100% - 10px) 100%, + 0 100% + ); + animation: ruleSlideIn 0.4s cubic-bezier(0.22, 1, 0.36, 1) backwards; +} + +.rule-section:nth-child(1) { animation-delay: 0.1s; } +.rule-section:nth-child(2) { animation-delay: 0.2s; } +.rule-section:nth-child(3) { animation-delay: 0.3s; } +.rule-section:nth-child(4) { animation-delay: 0.4s; } + +@keyframes ruleSlideIn { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.rule-section::before { + content: '▸'; + position: absolute; + left: -3px; + top: 18px; + font-size: 1.5em; + color: var(--accent-warning); + animation: blink 2s ease-in-out infinite; +} + +@keyframes blink { + 0%, 49%, 100% { opacity: 1; } + 50%, 99% { opacity: 0; } +} + +.rule-section:hover { + background: var(--surface-hover); + border-left-color: var(--accent-danger); + box-shadow: var(--shadow-lg); +} + +.rule-section h3 { + margin: 0 0 14px 0; + color: var(--text-primary); + font-size: 0.95em; +} + +.rule-section p { + margin: 8px 0; + color: var(--text-secondary); + line-height: 1.7; + font-size: 0.85em; + letter-spacing: 0.3px; +} + +.rule-section strong { + color: var(--accent-danger); + font-weight: 800; + text-transform: uppercase; + font-size: 0.9em; + letter-spacing: 0.5px; +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + PLAYER MANAGEMENT + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.player-names-list { + flex: 1 1 auto; + min-height: 0; + max-height: 360px; /* Altura máxima para activar scroll y mostrar fila parcial - efecto peek */ + overflow-y: scroll; + overflow-x: hidden; + margin-bottom: 12px; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE y Edge */ + /* Visual frame to indicate scrollable area */ + background: var(--surface-card); + border: 4px solid var(--border-heavy); + border-radius: 0; + padding: 12px; + box-shadow: inset 0 4px 12px rgba(0, 0, 0, 0.15), + inset 0 -4px 12px rgba(0, 0, 0, 0.15), + var(--shadow-md); + /* Gradiente para crear efecto peek - texto cortado visible */ + -webkit-mask-image: linear-gradient(to bottom, + black 0%, + black calc(100% - 40px), + transparent 100%); + mask-image: linear-gradient(to bottom, + black 0%, + black calc(100% - 40px), + transparent 100%); +} + +.player-names-list::-webkit-scrollbar { + display: none; /* Chrome, Safari, Opera */ +} + +.player-name-item { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; + background: var(--bg-secondary); + padding: 12px; + border-radius: 0; + border: 2px solid var(--border-light); + border-left: 4px solid var(--accent-info); + transition: all 0.2s ease; + box-shadow: var(--shadow-sm); +} + +.player-name-item:last-child { + margin-bottom: 0; +} + +.player-name-item:hover { + background: var(--surface-hover); + border-left-color: var(--accent-warning); + transform: translateX(2px); +} + +.player-name-item span { + font-weight: 800; + min-width: 80px; + font-size: 0.8em; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 1px; +} + +.player-name-item input { + flex: 1; + padding: 10px; + margin: 0; + font-size: 0.85em; + border-width: 2px; +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + CURTAIN REVEAL MECHANISM + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.curtain { + position: relative; + width: 100%; + height: 280px; + background: var(--bg-secondary); + border-radius: 0; + overflow: hidden; + margin: 12px 0; + box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.3), var(--shadow-harsh); + cursor: grab; + user-select: none; + border: 3px solid var(--border-heavy); + flex-shrink: 0; +} + +.curtain:active { + cursor: grabbing; +} + +.curtain-cover { + position: absolute; + inset: 0; + background: + repeating-linear-gradient( + 0deg, + #2a2a2a 0px, + #2a2a2a 8px, + #1a1a1a 8px, + #1a1a1a 12px + ), + linear-gradient(180deg, rgba(255,200,100,0.03) 0%, transparent 50%); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + font-size: 1.1em; + font-weight: 800; + color: #888; + transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); + z-index: 10; + user-select: none; + box-shadow: inset 0 -20px 40px rgba(0, 0, 0, 0.5), inset 0 0 60px rgba(0,0,0,0.3); + letter-spacing: 2px; + font-family: 'Bebas Neue', 'JetBrains Mono', monospace; +} + +.curtain-cover::after { + content: ''; + position: absolute; + bottom: -8px; + left: 0; + right: 0; + height: 8px; + background: linear-gradient(90deg, + transparent 0%, + rgba(0,0,0,0.3) 25%, + rgba(0,0,0,0.5) 50%, + rgba(0,0,0,0.3) 75%, + transparent 100%); +} + +.curtain-cover.lifted { + transform: translateY(-100%); +} + +.curtain-icon { + font-size: 2.5em; + animation: bounce 2s ease-in-out infinite; +} + +@keyframes bounce { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-12px); } +} + +.curtain-content { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 20px; + padding: 20px; + text-align: center; +} + +.role { + font-size: 2.4em; + font-weight: 400; + padding: 16px 32px; + border-radius: 0; + text-transform: uppercase; + border: 4px solid; + font-family: 'Bebas Neue', 'JetBrains Mono', monospace; + letter-spacing: 6px; + box-shadow: var(--shadow-harsh); + position: relative; + animation: roleReveal 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards; +} + +@keyframes roleReveal { + 0% { + opacity: 0; + transform: scale(0.5) rotate(-5deg); + filter: blur(10px); + } + 50% { + transform: scale(1.1) rotate(2deg); + } + 100% { + opacity: 1; + transform: scale(1) rotate(0); + filter: blur(0); + } +} + +.role.civil { + background: var(--accent-success); + color: var(--text-inverted); + border-color: #3d5a40; + animation: civilPulse 2s ease-in-out infinite; +} + +.role.impostor { + background: var(--accent-danger); + color: var(--text-inverted); + border-color: #8a2e26; + animation: impostorPulse 1.5s ease-in-out infinite; +} + +@keyframes civilPulse { + 0%, 100% { box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.3), 0 0 0px rgba(90, 125, 95, 0.5); } + 50% { box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.3), 0 0 25px rgba(90, 125, 95, 0.8); } +} + +@keyframes impostorPulse { + 0%, 100% { box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.3), 0 0 0px rgba(196, 69, 54, 0.5); } + 50% { box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.3), 0 0 30px rgba(196, 69, 54, 0.9); } +} + +.word { + font-size: 2em; + font-weight: 400; + background: var(--surface-card); + padding: 20px 36px; + border-radius: 0; + border: 3px solid var(--border-heavy); + font-family: 'Special Elite', 'Crimson Text', serif; + letter-spacing: 2px; + box-shadow: var(--shadow-harsh); + color: var(--text-primary); + text-transform: uppercase; + animation: wordReveal 0.5s cubic-bezier(0.22, 1, 0.36, 1) 0.2s forwards; + opacity: 0; +} + +@keyframes wordReveal { + 0% { + opacity: 0; + transform: translateY(20px); + filter: blur(5px); + } + 100% { + opacity: 1; + transform: translateY(0); + filter: blur(0); + } +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + TIMER + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.timer { + font-size: 4em; + font-weight: 800; + text-align: center; + margin: 20px 0; + padding: 24px; + background: var(--surface-card); + border-radius: 0; + border: 5px solid var(--border-heavy); + font-family: 'Bebas Neue', 'JetBrains Mono', monospace; + letter-spacing: 8px; + box-shadow: var(--shadow-harsh), inset 0 0 30px rgba(0, 0, 0, 0.2); + position: relative; + clip-path: polygon( + 16px 0, + calc(100% - 16px) 0, + 100% 16px, + 100% calc(100% - 16px), + calc(100% - 16px) 100%, + 16px 100%, + 0 calc(100% - 16px), + 0 16px + ); + animation: timerAppear 0.5s cubic-bezier(0.22, 1, 0.36, 1) forwards; +} + +@keyframes timerAppear { + from { + opacity: 0; + transform: scale(0.8); + filter: blur(5px); + } + to { + opacity: 1; + transform: scale(1); + filter: blur(0); + } +} + +.timer::before { + content: ''; + position: absolute; + top: 8px; + right: 8px; + width: 12px; + height: 12px; + background: var(--accent-success); + border-radius: 50%; + box-shadow: 0 0 10px var(--accent-success); + animation: statusBlink 2s ease-in-out infinite; +} + +@keyframes statusBlink { + 0%, 49%, 100% { opacity: 1; } + 50%, 99% { opacity: 0.3; } +} + +.timer.warning { + color: var(--accent-warning); + border-color: var(--accent-warning); + animation: timerShake 0.5s ease-in-out infinite; +} + +.timer.warning::before { + background: var(--accent-warning); + box-shadow: 0 0 10px var(--accent-warning); +} + +.timer.danger { + color: var(--accent-danger); + border-color: var(--accent-danger); + animation: timerShake 0.25s ease-in-out infinite, dangerFlash 1s ease-in-out infinite; +} + +.timer.danger::before { + background: var(--accent-danger); + box-shadow: 0 0 15px var(--accent-danger); + animation: statusBlink 0.5s ease-in-out infinite; +} + +@keyframes timerShake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-4px); } + 75% { transform: translateX(4px); } +} + +@keyframes dangerFlash { + 0%, 100% { background: var(--surface-card); } + 50% { background: rgba(196, 69, 54, 0.15); } +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + INFO BOXES & CONTENT + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.info-text { + text-align: center; + margin: 14px 0; + font-size: 0.85em; + line-height: 1.7; + background: var(--surface-card); + padding: 14px 16px; + border-radius: 0; + color: var(--text-secondary); + border: 2px solid var(--border-light); + border-left: 5px solid var(--accent-info); + box-shadow: var(--shadow-sm), inset 4px 0 8px rgba(46, 78, 122, 0.1); + letter-spacing: 0.3px; +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + PLAYER SELECTION GRID + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.player-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 10px; + margin: 12px 0; + flex: 1; + overflow-y: auto; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; + padding: 4px; + scrollbar-width: none; + -ms-overflow-style: none; +} + +.player-list::-webkit-scrollbar { + display: none; +} + +.player-item { + padding: 18px 14px; + min-height: 80px; /* Altura fija para evitar cambios de tamaño con vote-count */ + background: var(--surface-card); + border-radius: 0; + text-align: center; + cursor: pointer; + transition: all 0.25s cubic-bezier(0.22, 1, 0.36, 1); + font-weight: 800; + font-size: 0.85em; + border: 3px solid var(--border-medium); + box-shadow: var(--shadow-sm); + letter-spacing: 0.5px; + text-transform: uppercase; + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + clip-path: polygon( + 8px 0, + 100% 0, + 100% calc(100% - 8px), + calc(100% - 8px) 100%, + 0 100%, + 0 8px + ); + animation: playerItemAppear 0.3s cubic-bezier(0.22, 1, 0.36, 1) backwards; +} + +.player-item:nth-child(1) { animation-delay: 0.05s; } +.player-item:nth-child(2) { animation-delay: 0.1s; } +.player-item:nth-child(3) { animation-delay: 0.15s; } +.player-item:nth-child(4) { animation-delay: 0.2s; } +.player-item:nth-child(5) { animation-delay: 0.25s; } +.player-item:nth-child(6) { animation-delay: 0.3s; } +.player-item:nth-child(7) { animation-delay: 0.35s; } +.player-item:nth-child(8) { animation-delay: 0.4s; } +.player-item:nth-child(9) { animation-delay: 0.45s; } +.player-item:nth-child(10) { animation-delay: 0.5s; } + +@keyframes playerItemAppear { + from { + opacity: 0; + transform: scale(0.8); + } + to { + opacity: 1; + transform: scale(1); + } +} + +.player-item::before { + content: '□'; + position: absolute; + top: 6px; + right: 6px; + font-size: 1.2em; + transition: all 0.2s ease; +} + +.player-item:hover { + background: var(--surface-hover); + box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.25); + filter: brightness(1.05); +} + +.player-item:active { + box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15); + filter: brightness(0.95); +} + +.player-item.selected { + background: var(--accent-danger); + border-color: var(--text-primary); + color: var(--text-inverted); + box-shadow: 0 0 0 4px rgba(217, 54, 38, 0.5), 6px 6px 0px rgba(0, 0, 0, 0.4); + animation: selectPulse 0.3s ease-out; +} + +@keyframes selectPulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.08); } + 100% { transform: scale(1); } +} + +.player-item.selected::before { + content: '☑'; + animation: checkAppear 0.2s ease-out; +} + +@keyframes checkAppear { + from { transform: scale(0) rotate(-180deg); } + to { transform: scale(1) rotate(0); } +} + +.player-item.disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; + background: var(--bg-secondary); + border-color: var(--border-light); + filter: grayscale(0.6); + animation: playerItemAppearDisabled 0.3s cubic-bezier(0.22, 1, 0.36, 1) backwards !important; +} + +.player-item.disabled::before { + content: '✕'; + color: var(--text-tertiary); +} + +.player-item .vote-count { + display: block; + font-size: 0.7em; + margin-top: 4px; + opacity: 0.75; + font-weight: 600; + letter-spacing: 0.3px; + min-height: 1em; /* Reservar espacio siempre */ +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + RESULTS + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.results { + background: var(--surface-card); + border-radius: 0; + padding: 14px; + margin: 8px 0; + flex: 1; + overflow: visible; + border: 2px solid var(--border-medium); + box-shadow: var(--shadow-md); + animation: resultsReveal 0.5s cubic-bezier(0.22, 1, 0.36, 1) forwards; +} + +@keyframes resultsReveal { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.results h2 { + font-family: 'Bebas Neue', 'Crimson Text', Georgia, serif; + font-size: 1.6em; + letter-spacing: 3px; + margin-bottom: 10px; + animation: winnerReveal 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards; +} + +@keyframes winnerReveal { + 0% { + opacity: 0; + transform: scale(0.5); + filter: blur(10px); + } + 60% { + transform: scale(1.1); + } + 100% { + opacity: 1; + transform: scale(1); + filter: blur(0); + } +} + +.role-reveal { + background: var(--bg-secondary); + padding: 8px 10px; + border-radius: 0; + margin: 5px 0; + border-left: 4px solid; + font-size: 0.8em; + letter-spacing: 0.2px; + box-shadow: var(--shadow-sm); + transition: all 0.2s ease; + animation: roleRevealSlide 0.4s cubic-bezier(0.22, 1, 0.36, 1) backwards; +} + +.role-reveal:nth-child(1) { animation-delay: 0.3s; } +.role-reveal:nth-child(2) { animation-delay: 0.4s; } +.role-reveal:nth-child(3) { animation-delay: 0.5s; } +.role-reveal:nth-child(4) { animation-delay: 0.6s; } +.role-reveal:nth-child(5) { animation-delay: 0.7s; } +.role-reveal:nth-child(6) { animation-delay: 0.8s; } +.role-reveal:nth-child(7) { animation-delay: 0.9s; } +.role-reveal:nth-child(8) { animation-delay: 1s; } +.role-reveal:nth-child(9) { animation-delay: 1.1s; } +.role-reveal:nth-child(10) { animation-delay: 1.2s; } + +@keyframes roleRevealSlide { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.role-reveal:hover { + transform: translateX(3px); +} + +.role-reveal.civil-reveal { + border-left-color: var(--accent-success); +} + +.role-reveal.impostor-reveal { + border-left-color: var(--accent-danger); +} + +.role-reveal.executed { + opacity: 0.5; + background: rgba(0, 0, 0, 0.2); + text-decoration: line-through; +} + +.tag { + display: inline-block; + padding: 6px 10px; + border-radius: 0; + background: var(--surface-hover); + margin: 4px 0; + font-weight: 800; + font-size: 0.75em; + border: 2px solid var(--border-medium); + letter-spacing: 1px; + text-transform: uppercase; +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + POOL SELECTION + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.pool-buttons { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px; + padding: 0; +} + +.pool-buttons-wrapper { + position: relative; + flex: 1 1 auto; + min-height: 0; + max-height: 320px; /* Ajustado para mostrar fila parcial - efecto peek */ + overflow-y: scroll; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE y Edge */ + /* Visual frame to indicate scrollable area */ + background: var(--surface-card); + border: 4px solid var(--border-heavy); + border-radius: 0; + padding: 12px; + margin: 12px 0; + box-shadow: inset 0 4px 12px rgba(0, 0, 0, 0.15), + inset 0 -4px 12px rgba(0, 0, 0, 0.15), + var(--shadow-md); + /* Gradiente para crear efecto peek - texto cortado visible */ + -webkit-mask-image: linear-gradient(to bottom, + black 0%, + black calc(100% - 50px), + transparent 100%); + mask-image: linear-gradient(to bottom, + black 0%, + black calc(100% - 50px), + transparent 100%); +} + +.pool-buttons-wrapper::-webkit-scrollbar { + display: none; /* Chrome, Safari, Opera */ +} + +.pool-btn { + padding: 12px 10px; + border-radius: 0; + border: 2px solid var(--border-medium); + background: var(--surface-card); + color: var(--text-primary); + font-weight: 700; + font-size: 0.8em; + cursor: pointer; + transition: all 0.18s ease; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-transform: uppercase; + letter-spacing: 0.5px; + box-shadow: var(--shadow-sm); +} + +.pool-btn:hover { + background: var(--surface-hover); + box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.15); + filter: brightness(1.05); +} + +.pool-btn.selected { + border-color: var(--text-primary); + background: var(--accent-warning); + color: var(--text-inverted); + box-shadow: 0 0 0 3px rgba(212, 165, 116, 0.3), 3px 3px 0px rgba(0, 0, 0, 0.2); +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + FIXED UI CONTROLS (Theme, Language, Exit) + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.theme-toggle { + position: fixed; + top: 20px; + right: 20px; + width: 56px; + height: 56px; + border-radius: 0; + border: 3px solid var(--border-heavy); + background: var(--surface-glass); + backdrop-filter: blur(20px); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.6em; + box-shadow: var(--shadow-harsh); + transition: all 0.2s ease; + z-index: 1000; + margin: 0; + padding: 0; +} + +.theme-toggle:hover { + box-shadow: 8px 8px 0px rgba(0, 0, 0, 0.2); + filter: brightness(1.1); +} + +.theme-toggle:active { + box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15); + filter: brightness(0.95); +} + +.theme-icon { + transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55); + display: inline-block; +} + +.theme-toggle:hover .theme-icon { + transform: rotate(180deg) scale(1.1); +} + +.language-toggle { + position: fixed; + top: 86px; + right: 20px; + width: auto; + min-width: 56px; + height: 56px; + padding: 0 16px; + border-radius: 0; + border: 3px solid var(--border-heavy); + background: var(--surface-glass); + backdrop-filter: blur(20px); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + font-size: 1em; + font-weight: 800; + box-shadow: var(--shadow-harsh); + transition: all 0.2s ease; + z-index: 1000; + color: var(--text-primary); + margin: 0; + text-transform: uppercase; + letter-spacing: 1px; +} + +.language-toggle:hover { + box-shadow: 8px 8px 0px rgba(0, 0, 0, 0.2); + filter: brightness(1.1); +} + +.language-toggle:active { + box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15); + filter: brightness(0.95); +} + +.language-icon { + font-size: 1.3em; + transition: transform 0.3s ease; + display: inline-block; +} + +.language-text { + font-size: 0.85em; + letter-spacing: 1.5px; + font-family: 'JetBrains Mono', monospace; +} + +.exit-game { + position: fixed; + top: 20px; + left: 20px; + width: auto; + height: 56px; + padding: 0 16px; + border-radius: 0; + border: 3px solid var(--border-heavy); + background: var(--surface-glass); + backdrop-filter: blur(20px); + cursor: pointer; + display: none; + align-items: center; + justify-content: center; + gap: 10px; + font-size: 0.85em; + font-weight: 800; + box-shadow: var(--shadow-harsh); + transition: all 0.2s ease; + z-index: 1000; + color: var(--text-primary); + text-transform: uppercase; + letter-spacing: 1px; +} + +.exit-game.visible { + display: inline-flex; +} + +.exit-game:hover { + box-shadow: 8px 8px 0px rgba(0, 0, 0, 0.2); + background: var(--accent-danger); + color: var(--text-inverted); +} + +.exit-game:active { + box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15); +} + +.exit-icon { + font-size: 1.3em; +} + +.exit-text { + font-size: 0.9em; + font-family: 'JetBrains Mono', monospace; +} + +.screen-lock-toggle { + position: fixed; + top: 152px; + right: 20px; + width: 56px; + height: 56px; + border-radius: 0; + border: 3px solid var(--border-heavy); + background: var(--surface-glass); + backdrop-filter: blur(20px); + cursor: pointer; + display: none; + align-items: center; + justify-content: center; + font-size: 1.6em; + box-shadow: var(--shadow-harsh); + transition: all 0.2s ease; + z-index: 1000; + margin: 0; + padding: 0; +} + +.screen-lock-toggle.visible { + display: inline-flex; +} + +.screen-lock-toggle:hover { + box-shadow: 8px 8px 0px rgba(0, 0, 0, 0.2); + filter: brightness(1.1); +} + +.screen-lock-toggle:active { + box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15); + filter: brightness(0.95); +} + +.screen-lock-toggle.active { + background: var(--accent-success); + color: var(--text-inverted); + border-color: var(--accent-success); +} + +.screen-lock-icon { + transition: transform 0.3s ease; + display: inline-block; +} + +.screen-lock-toggle:hover .screen-lock-icon { + transform: scale(1.1); +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + RESPONSIVE DESIGN + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +@media (max-width: 600px) { + body { + padding: 60px 10px 10px 10px; + font-size: 13px; + } + + h1 { + font-size: 1.7em; + margin-bottom: 14px; + } + + .container { + padding: 20px 16px; + } + + .theme-toggle, + .language-toggle, + .exit-game, + .screen-lock-toggle { + top: 8px; + width: 44px; + height: 44px; + min-width: 44px; + } + + .language-toggle { + top: 58px; + } + + .screen-lock-toggle { + top: 108px; + } + + .exit-game { + padding: 0 12px; + font-size: 0.75em; + height: 44px; + } + + /* Ocultar textos en móvil, solo emojis */ + .exit-text, + .language-text { + display: none; + } + + .exit-game { + padding: 0; + width: 44px; + min-width: 44px; + } + + .language-toggle { + padding: 0; + width: 44px; + min-width: 44px; + } + + .exit-icon, + .language-icon { + font-size: 1.4em; + } + + .timer { + font-size: 2.5em; + padding: 16px; + } + + .welcome-title { + font-size: 1.8em; + } + + .role { + font-size: 1.6em; + padding: 10px 18px; + } + + .word { + font-size: 1.3em; + padding: 12px 20px; + } + + .form-group { + margin-bottom: 10px; + } + + .form-group.compact { + margin-bottom: 8px; + } + + button { + padding: 12px 16px; + margin-top: 8px; + } + + .rule-section { + padding: 12px; + margin-bottom: 12px; + } + + .rule-section h3 { + font-size: 0.85em; + margin-bottom: 10px; + } + + .rule-section p { + font-size: 0.8em; + margin: 6px 0; + } + + .player-name-item { + padding: 10px; + margin-bottom: 6px; + } + + .player-name-item span { + font-size: 0.75em; + min-width: 70px; + } + + .player-item { + padding: 14px 10px; + min-height: 72px; /* Altura fija también en móvil */ + font-size: 0.8em; + } + + .pool-btn { + padding: 10px 8px; + font-size: 0.75em; + } + + .pool-buttons-wrapper { + max-height: 240px; /* Ajustado para mostrar fila parcial en móvil - efecto peek */ + padding: 10px; + margin: 10px 0; + } + + .player-names-list { + max-height: 280px; /* Ajustado para mostrar fila parcial en móvil - efecto peek */ + padding: 10px; + } + + .info-text { + padding: 12px 14px; + font-size: 0.8em; + margin: 10px 0; + } + + .curtain { + height: 240px; + margin: 10px 0; + } +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + UTILITY ANIMATIONS + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +@keyframes typewriter { + from { width: 0; } + to { width: 100%; } +} + +@keyframes glitch { + 0% { transform: translate(0); } + 20% { transform: translate(-2px, 2px); } + 40% { transform: translate(-2px, -2px); } + 60% { transform: translate(2px, 2px); } + 80% { transform: translate(2px, -2px); } + 100% { transform: translate(0); } +} + +/* Smooth scrolling */ +* { + scrollbar-width: thin; + scrollbar-color: var(--border-medium) transparent; +} + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--border-medium); + border-radius: 0; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--border-heavy); +} From 201c41e4f582de923f0d3b288bd384d896872053 Mon Sep 17 00:00:00 2001 From: CI Action Date: Wed, 14 Jan 2026 12:29:23 +0000 Subject: [PATCH 5/9] chore: update asset versions [skip ci] --- script.d5454706.js | 468 ----------- script.f88d8968.js | 1347 +++++++++++++++++++++++++++++++ styles.1a37b506.css | 1829 +++++++++++++++++++++++++++++++++++++++++++ styles.26a5b74f.css | 455 ----------- 4 files changed, 3176 insertions(+), 923 deletions(-) delete mode 100644 script.d5454706.js create mode 100644 script.f88d8968.js create mode 100644 styles.1a37b506.css delete mode 100644 styles.26a5b74f.css diff --git a/script.d5454706.js b/script.d5454706.js deleted file mode 100644 index 8e41f46..0000000 --- a/script.d5454706.js +++ /dev/null @@ -1,468 +0,0 @@ -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 EMBEDDED_POOLS = [ - { id: 'animales_naturaleza', name: 'Animales y naturaleza', emoji: '🌿', words: ['Perro','Gato','Lobo','Zorro','Oso','Tigre','León','Pantera','Jaguar','Puma','Guepardo','Elefante','Rinoceronte','Hipopótamo','Jirafa','Cebra','Camello','Dromedario','Canguro','Koala','Panda','Mapache','Nutria','Castor','Foca','Morsa','Delfín','Ballena','Tiburón','Orca','Pulpo','Calamar','Medusa','Tortuga','Lagarto','Cocodrilo','Serpiente','Anaconda','Iguana','Rana','Sapo','Búho','Halcón','Águila','Cóndor','Gaviota','Loro','Flamenco','Pingüino','Avestruz','Gallina','Pato','Ganso','Cisne','Abeja','Hormiga','Mariquita','Libélula','Mariposa','Escarabajo','Grillo','Saltamontes','Araña','Escorpión','Lombriz','Caracol','Estrella de mar','Coral','Musgo','Helecho','Pino','Roble','Encina','Palmera','Cactus','Bambú','Rosa','Tulipán','Girasol','Lavanda','Montaña','Río','Lago','Mar','Playa','Desierto','Selva','Bosque','Pradera','Glaciar','Volcán'] }, - { id: 'vida_cotidiana', name: 'Vida cotidiana', emoji: '🏠', words: ['Pan','Leche','Café','Té','Agua','Jugo','Refresco','Cerveza','Vino','Pizza','Hamburguesa','Sándwich','Taco','Burrito','Pasta','Arroz','Paella','Sushi','Ramen','Ensalada','Sopa','Croqueta','Tortilla','Empanada','Arepa','Queso','Jamón','Chorizo','Pollo','Carne','Cerdo','Pescado','Marisco','Patata','Tomate','Cebolla','Ajo','Pimiento','Zanahoria','Lechuga','Brócoli','Coliflor','Manzana','Plátano','Naranja','Pera','Uva','Fresa','Mango','Piña','Melón','Sandía','Yogur','Galletas','Chocolate','Helado','Cereales','Mantequilla','Aceite','Sal','Pimienta','Azúcar','Harina','Huevo','Cuchara','Tenedor','Cuchillo','Plato','Vaso','Taza','Olla','Sartén','Microondas','Horno','Nevera','Mesa','Silla','Sofá','Cama','Almohada','Sábana','Toalla','Ducha','Jabón','Champú','Cepillo','Pasta de dientes'] }, - { id: 'deportes', name: 'Deportes', emoji: '🏅', words: ['Fútbol','Baloncesto','Tenis','Pádel','Bádminton','Voleibol','Béisbol','Rugby','Hockey hielo','Hockey césped','Golf','Boxeo','MMA','Judo','Karate','Taekwondo','Esgrima','Tiro con arco','Halterofilia','Crossfit','Atletismo','Maratón','Triatlón','Ciclismo ruta','Ciclismo montaña','BMX','Natación','Waterpolo','Surf','Vela','Remo','Piragüismo','Esquí','Snowboard','Patinaje artístico','Patinaje velocidad','Curling','Escalada','Senderismo','Trail running','Parkour','Gimnasia artística','Gimnasia rítmica','Trampolín','Skate','Breakdance','Carreras coches','Fórmula 1','Rally','Karting','Motociclismo','Enduro','Motocross','Equitación','Polo','Críquet','Billar','Dardos','Petanca','Pickleball','Ultimate frisbee','Paintball','Airsoft','eSports'] }, - { id: 'marcas', name: 'Marcas', emoji: '🛍️', words: ['Apple','Samsung','Google','Microsoft','Amazon','Meta','Tesla','Toyota','Honda','Ford','BMW','Mercedes','Audi','Volkswagen','Porsche','Ferrari','Lamborghini','Maserati','McLaren','Chevrolet','Nissan','Kia','Hyundai','Peugeot','Renault','Volvo','Jaguar','Land Rover','Fiat','Alfa Romeo','Ducati','Yamaha','Canon','Nikon','Sony','Panasonic','LG','Philips','Siemens','Bosch','Whirlpool','Ikea','Zara','H&M','Uniqlo','Nike','Adidas','Puma','Reebok','New Balance','Under Armour','Converse','Vans','Patagonia','The North Face','Columbia','Levi’s','Calvin Klein','Gucci','Prada','Louis Vuitton','Chanel','Hermès','Dior','Rolex','Omega','Casio','Pepsi','Coca-Cola','Fanta','Red Bull','Monster','Starbucks','Nespresso','Nestlé','Danone','Kellogg’s','Oreo','Intel','AMD','Nvidia','Qualcomm','TikTok','Netflix','Disney','Warner Bros','HBO','Spotify','Airbnb','Uber','Booking'] }, - { id: 'musica', name: 'Música', emoji: '🎵', words: ['Guitarra','Piano','Violín','Batería','Bajo','Saxofón','Trompeta','Flauta','Clarinete','Acordeón','Ukelele','Arpa','Sintetizador','DJ','Micrófono','Altavoz','Concierto','Festival','Vinilo','Rock','Pop','Punk','Metal','Heavy','Thrash','Death metal','Jazz','Blues','Soul','Funk','R&B','Rap','Hip hop','Trap','Reggaetón','Salsa','Bachata','Merengue','Cumbia','Vallenato','Flamenco','Rumba','Bossa nova','Samba','Tango','Country','EDM','Techno','House','Trance','Dubstep','Drum and bass','Lo-fi','Reggae','Ska','K-pop','J-pop','Indie','Gospel','Ópera','Sinfonía','Orquesta','Coro','Cantautor','Balada','Bolero','Ranchera','Corrido','Mariachi'] }, - { id: 'personajes', name: 'Personajes', emoji: '🧙', words: ['Sherlock Holmes','Harry Potter','Hermione Granger','Ron Weasley','Albus Dumbledore','Voldemort','Frodo Bolsón','Sam Gamyi','Gandalf','Aragorn','Legolas','Gimli','Gollum','Bilbo Bolsón','Katniss Everdeen','Peeta Mellark','Batman','Bruce Wayne','Joker','Harley Quinn','Superman','Clark Kent','Lois Lane','Wonder Woman','Diana Prince','Flash','Barry Allen','Aquaman','Arthur Curry','Spider-Man','Peter Parker','Iron Man','Tony Stark','Capitán América','Steve Rogers','Black Widow','Natasha Romanoff','Hulk','Bruce Banner','Thor','Loki','Thanos','Doctor Strange','Wanda Maximoff','Vision','Star-Lord','Gamora','Groot','Rocket','Drax','Deadpool','Wolverine','Magneto','Professor X','Storm','Cyclops','Jean Grey','Mystique','Darth Vader','Luke Skywalker','Leia Organa','Han Solo','Chewbacca','Yoda','Obi-Wan Kenobi','Anakin Skywalker','Rey','Kylo Ren','R2-D2','C-3PO','Indiana Jones','Lara Croft','James Bond','Mario','Luigi','Princesa Peach','Bowser','Link','Zelda','Geralt de Rivia','Ciri','Yennefer','Kratos','Atreus','Ellie','Joel Miller','Nathan Drake','Master Chief','Cortana','Sonic','Tails','Ash Ketchum','Pikachu','Goku','Vegeta','Naruto','Sasuke','Luffy','Zoro','Nami','Tanjiro','Nezuko','Saitama','Light Yagami','L Lawliet'] } -]; - -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: [], - selectedPool: 'animales_naturaleza', - 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)); - -// ---------- Defaults ---------- -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)); -} - -// ---------- Pools ---------- -async function loadPoolsList() { - loadPoolsCache(); - let list = []; - try { - const res = await fetch(POOLS_MANIFEST_URL); - if (res.ok) list = await res.json(); - } catch (_) {} - if (!Array.isArray(list) || list.length === 0) { - list = EMBEDDED_POOLS.map(p => ({ id: p.id, name: p.name, emoji: p.emoji, count: p.words.length })); - } - availablePools = list; - renderPoolButtons(); -} - -function parseWordsFile(text) { - const lines = text.split(/\r?\n/).map(l => l.trim()).filter(Boolean); - if (!lines.length) return []; - if (lines[0].startsWith('#')) return lines.slice(1); - return lines; -} - -async function pickWords() { - const poolId = state.selectedPool || 'default'; - let words = []; - if (poolsCache[poolId]?.words) { - words = poolsCache[poolId].words; - } else if (poolId !== 'default') { - const res = await fetch(`word-pools/${poolId}.txt`); - if (!res.ok) throw new Error('No se pudo cargar el pool'); - const text = await res.text(); - words = parseWordsFile(text); - poolsCache[poolId] = { words, ts: Date.now() }; savePoolsCache(); - } else { - words = EMBEDDED_POOLS[0].words; - } - const shuffled = [...words].sort(() => Math.random() - 0.5); - return { civilian: shuffled[0], impostor: shuffled[1] }; -} - -function renderPoolButtons() { - const container = document.getElementById('pool-buttons'); - if (!container) return; - container.innerHTML = ''; - 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.selectedPool === pool.id) btn.classList.add('selected'); - btn.onclick = () => { state.selectedPool = pool.id; saveState(); renderPoolButtons(); }; - container.appendChild(btn); - }); -} - -// ---------- Configuración y nombres ---------- -function goToNames() { - 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('Impostores debe ser menor que jugadores'); return; } - state.numPlayers = nPlayers; state.numImpostors = nImpostors; state.gameTime = gTime; state.deliberationTime = dTime; - 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'; - div.innerHTML = `Jugador ${i+1}:`; - list.appendChild(div); - } -} - -// ---------- Inicio de partida ---------- -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); - state.civilianWord = shuffled[0]; - state.impostorWord = shuffled[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'); -} - -// Ajustar defaults cuando se edita el nº de jugadores -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] || `Jugador ${state.startPlayer+1}`; - const poolMeta = availablePools.find(p => p.id === state.selectedPool) || EMBEDDED_POOLS[0]; - el.innerHTML = ` -

Jugadores: ${state.numPlayers}

-

Impostores: ${state.numImpostors}

-

Tiempo de partida: ${fmt(state.gameTime)}

-

Tiempo de deliberación: ${fmt(state.deliberationTime)}

-

Pool: ${poolMeta.emoji || '🎲'} ${poolMeta.name || poolMeta.id}

-

Empieza: ${startName} · Orden: ${state.turnDirection === 'horario' ? 'Horario' : 'Antihorario'}

- `; -} - -// ---------- Revelación ---------- -function loadCurrentReveal() { - state.phase = 'reveal'; saveState(); - 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; - document.getElementById('curtain-cover').classList.remove('lifted'); - document.getElementById('next-player-btn').style.display = 'none'; - document.getElementById('start-game-btn').style.display = 'none'; -} - -function liftCurtain() { - const cover = document.getElementById('curtain-cover'); - if (cover.classList.contains('lifted')) return; - cover.classList.add('lifted'); - const idx = state.revealOrder[state.currentReveal]; - const role = state.roles[idx]; - const word = role === 'CIVIL' ? state.civilianWord : state.impostorWord; - document.getElementById('role-text').textContent = role; - document.getElementById('role-text').className = 'role ' + (role === 'CIVIL' ? 'civil' : 'impostor'); - document.getElementById('word-text').textContent = word; - setTimeout(() => { - if (state.currentReveal + 1 < state.numPlayers) document.getElementById('next-player-btn').style.display = 'block'; - else document.getElementById('start-game-btn').style.display = 'block'; - }, 700); -} - -function nextReveal() { state.currentReveal++; saveState(); loadCurrentReveal(); } - -// swipe support -(() => { - const curtain = document.getElementById('curtain'); - let startY = null; - curtain.addEventListener('touchstart', e => { startY = e.touches[0].clientY; }, {passive:true}); - curtain.addEventListener('touchmove', e => { if (startY === null) return; const dy = e.touches[0].clientY - startY; if (dy < -40) { liftCurtain(); startY = null; } }, {passive:true}); - curtain.addEventListener('click', liftCurtain); -})(); - -// ---------- Timers ---------- -let timerInterval = null; -function startPhaseTimer(phase, seconds, elementId, onEnd) { - if (timerInterval) clearInterval(timerInterval); - const now = Date.now(); - state.timerPhase = phase; - state.timerEndAt = now + seconds*1000; - saveState(); - const el = document.getElementById(elementId); - const tick = () => { - const remaining = Math.max(0, Math.round((state.timerEndAt - Date.now())/1000)); - updateTimerDisplay(el, remaining); - if (remaining <= 0) { clearInterval(timerInterval); playBeep(); onEnd(); } - }; - tick(); - timerInterval = setInterval(tick, 1000); -} - -function resumeTimerIfNeeded() { - if (!state.timerEndAt || !state.timerPhase) return; - const remaining = Math.round((state.timerEndAt - Date.now())/1000); - if (remaining <= 0) { state.timerEndAt = null; saveState(); return; } - if (state.timerPhase === 'game') { showScreen('game-screen'); startPhaseTimer('game', remaining, 'game-timer', startDeliberationPhase); } - else if (state.timerPhase === 'deliberation') { showScreen('deliberation-screen'); startPhaseTimer('deliberation', remaining, 'deliberation-timer', startVotingPhase); } -} - -function updateTimerDisplay(el, remaining) { - const minutes = Math.floor(remaining/60); const secs = remaining%60; - el.textContent = `${minutes}:${secs.toString().padStart(2,'0')}`; - el.className = 'timer'; - if (remaining <= 10) el.classList.add('danger'); else if (remaining <= 30) el.classList.add('warning'); -} - -function playBeep() { - const ctx = new (window.AudioContext || window.webkitAudioContext)(); - const osc = ctx.createOscillator(); const gain = ctx.createGain(); - osc.connect(gain); gain.connect(ctx.destination); osc.frequency.value = 820; osc.type = 'sine'; - gain.gain.setValueAtTime(0.3, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.45); - osc.start(); osc.stop(ctx.currentTime + 0.45); -} - -// ---------- Fases ---------- -function startGamePhase() { state.phase = 'game'; saveState(); showScreen('game-screen'); startPhaseTimer('game', state.gameTime, 'game-timer', startDeliberationPhase); } -function startDeliberationPhase() { state.phase = 'deliberation'; saveState(); showScreen('deliberation-screen'); startPhaseTimer('deliberation', state.deliberationTime, 'deliberation-timer', startVotingPhase); } -function startVotingPhase(candidates = null, isTiebreak = false) { - state.phase = 'voting'; - state.votingPlayer = 0; - state.votes = {}; - state.selections = []; - state.votingPool = candidates; - state.isTiebreak = isTiebreak; - saveState(); - renderVoting(); - showScreen('voting-screen'); -} -function skipToDeliberation() { if (timerInterval) clearInterval(timerInterval); startDeliberationPhase(); } -function skipToVoting() { if (timerInterval) clearInterval(timerInterval); startVotingPhase(); } -function startTiebreakDeliberation(candidates) { - state.phase = 'deliberation'; - state.tiebreakCandidates = candidates; - saveState(); - showScreen('deliberation-screen'); - startPhaseTimer('deliberation', 60, 'deliberation-timer', () => startVotingPhase(candidates, true)); -} - -// ---------- Votación secreta ---------- -function renderVoting() { - const pool = state.votingPool || Array.from({length: state.numPlayers}, (_, i) => i); - const voter = state.playerNames[state.votingPlayer]; - document.getElementById('voter-name').textContent = voter; - document.getElementById('votes-needed').textContent = state.numImpostors; - state.selections = state.selections || []; - const list = document.getElementById('vote-list'); list.innerHTML = ''; - pool.forEach(i => { - const item = document.createElement('div'); - item.className = 'player-item'; - item.textContent = state.playerNames[i]; - if (state.votes[i]) item.innerHTML += `Votos: ${state.votes[i]}`; - if (state.selections.includes(i)) item.classList.add('selected'); - if (i === state.votingPlayer) { - item.classList.add('disabled'); - item.style.opacity = '0.5'; - item.style.pointerEvents = 'none'; - } else { - item.onclick = () => toggleSelection(i, item); - } - list.appendChild(item); - }); - updateConfirmButton(); -} - -function toggleSelection(idx, el) { - if (idx === state.votingPlayer) return; - if (state.selections.includes(idx)) state.selections = state.selections.filter(x => x !== idx); - else { - if (state.selections.length >= state.numImpostors) return; - state.selections.push(idx); - } - saveState(); - renderVoting(); -} - -function updateConfirmButton() { - const btn = document.getElementById('confirm-vote-btn'); - btn.disabled = state.selections.length !== state.numImpostors; -} - -function confirmCurrentVote() { - state.selections.forEach(t => { state.votes[t] = (state.votes[t] || 0) + 1; }); - state.votingPlayer++; - state.selections = []; - saveState(); - if (state.votingPlayer >= state.numPlayers) { handleVoteOutcome(); return; } - renderVoting(); -} - -// ---------- Resolución de voto ---------- -function handleVoteOutcome() { - const pool = state.votingPool || Array.from({length: state.numPlayers}, (_, i) => i); - const counts = pool.map(idx => ({ idx, votes: state.votes[idx] || 0 })); - counts.sort((a, b) => b.votes - a.votes); - - let slots = state.numImpostors; - const executed = []; - for (let i = 0; i < counts.length && slots > 0; ) { - const currentVotes = counts[i].votes; - const group = []; - let j = i; - while (j < counts.length && counts[j].votes === currentVotes) { group.push(counts[j].idx); j++; } - if (group.length <= slots) { - executed.push(...group); - slots -= group.length; - i = j; - } else { - // Tie for remaining slots - if (state.isTiebreak) { - // segunda vez empatados: ganan impostores - state.executed = []; - showResults(true); - return; - } - startTiebreakDeliberation(group); - return; - } - } - - state.executed = executed; - showResults(); -} - -// ---------- Resultados ---------- -function showResults(isTiebreak = false) { - state.phase = 'results'; saveState(); - const executed = state.executed || []; - let impostorsAlive = 0; - state.roles.forEach((r,i) => { if (r === 'IMPOSTOR' && !executed.includes(i)) impostorsAlive++; }); - const winner = impostorsAlive > 0 ? 'IMPOSTORES' : 'CIVILES'; - const results = document.getElementById('results-content'); - results.innerHTML = ` -

${winner === 'CIVILES' ? '✅ ¡GANAN LOS CIVILES!' : '❌ ¡GANAN LOS IMPOSTORES!'}

-

Ejecutados: ${executed.length ? executed.map(i => state.playerNames[i]).join(', ') : 'Nadie'}

-

Votos: ${Object.keys(state.votes).length ? '' : 'Sin votos'}

-

Roles revelados

- ${state.roles.map((role,i) => { - const word = role === 'CIVIL' ? state.civilianWord : state.impostorWord; - const killed = executed.includes(i) ? 'executed' : ''; - return `
${state.playerNames[i]}: ${role} — "${word}" ${killed ? '☠️' : ''}
`; - }).join('')} - `; - showScreen('results-screen'); -} - -// ---------- Utilidades ---------- -function showScreen(id) { - document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); - document.getElementById(id).classList.add('active'); - state.phase = id.replace('-screen',''); - saveState(); -} - -function newMatch() { clearState(); state = { ...state, phase:'setup', timerEndAt:null, timerPhase:null, votingPool:null, isTiebreak:false, tiebreakCandidates:[] }; location.reload(); } - -// ---------- Rehidratación ---------- -(function init() { - const restored = loadState(); - showScreen('setup-screen'); - loadPoolsList(); - if (!state.turnDirection) state.turnDirection = 'horario'; - if (typeof state.startPlayer !== 'number') state.startPlayer = 0; - switch (state.phase) { - case 'setup': showScreen('setup-screen'); break; - case 'names': buildNameInputs(); showScreen('names-screen'); break; - case 'pre-reveal': renderSummary(); showScreen('pre-reveal-screen'); break; - case 'reveal': showScreen('reveal-screen'); loadCurrentReveal(); break; - case 'game': showScreen('game-screen'); resumeTimerIfNeeded(); break; - case 'deliberation': showScreen('deliberation-screen'); resumeTimerIfNeeded(); break; - case 'voting': showScreen('voting-screen'); renderVoting(); break; - case 'results': showResults(); break; - default: showScreen('setup-screen'); - } -})(); diff --git a/script.f88d8968.js b/script.f88d8968.js new file mode 100644 index 0000000..05c0b74 --- /dev/null +++ b/script.f88d8968.js @@ -0,0 +1,1347 @@ +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('othersLookAway')}`; + } + + const curtainText = document.querySelector('.curtain-cover div:last-child'); + if (curtainText) { + curtainText.textContent = t('liftCurtain'); + } + + // Reset curtain state + curtainState.isRevealed = false; + const coverEl = document.getElementById('curtain-cover'); + coverEl.style.transform = 'translateY(0)'; + coverEl.style.transition = ''; + + document.getElementById('next-player-btn').style.display = 'none'; + document.getElementById('start-game-btn').style.display = 'none'; +} + +function liftCurtain() { + const cover = document.getElementById('curtain-cover'); + if (cover.classList.contains('lifted')) return; + + // Restore CSS transition and use the class + cover.style.transition = ''; + cover.style.transform = ''; + cover.classList.add('lifted'); + + const idx = state.revealOrder[state.currentReveal]; + const role = state.roles[idx]; + const word = role === 'CIVIL' ? state.civilianWord : state.impostorWord; + document.getElementById('role-text').textContent = role; + document.getElementById('role-text').className = 'role ' + (role === 'CIVIL' ? 'civil' : 'impostor'); + document.getElementById('word-text').textContent = word; + setTimeout(() => { + if (state.currentReveal + 1 < state.numPlayers) document.getElementById('next-player-btn').style.display = 'block'; + else document.getElementById('start-game-btn').style.display = 'block'; + }, 700); +} + +function nextReveal() { state.currentReveal++; saveState(); loadCurrentReveal(); } + +// Curtain system with GRAVITY - The curtain always tends to fall +// Supports both touch (mobile) and mouse (desktop) +// On desktop: curtain stays up while mouse button is held, even if cursor leaves the area +let curtainState = { isRevealed: false }; +let curtainDragState = { + startY: null, + isDragging: false, + currentTranslateY: 0 +}; + +function initCurtainHandlers() { + const curtain = document.getElementById('curtain'); + if (!curtain) return; + + // Function to get Y position from event (touch or mouse) + const getY = (e) => { + return e.touches ? e.touches[0].clientY : e.clientY; + }; + + // Start function (touch and mouse) + const handleStart = (e) => { + curtainDragState.startY = getY(e); + curtainDragState.isDragging = true; + curtainDragState.currentTranslateY = 0; + if (e.type === 'mousedown') { + e.preventDefault(); // Prevent text selection on desktop + } + }; + + // Move function (touch and mouse) + const handleMove = (e) => { + if (curtainDragState.startY === null || !curtainDragState.isDragging) return; + const currentY = getY(e); + const dy = currentY - curtainDragState.startY; + const coverEl = document.getElementById('curtain-cover'); + if (!coverEl) return; + + // Calculate displacement: negative = up, positive = down + // Allow going further up than the curtain height (user can keep dragging up) + // but don't allow going below initial position (0) + curtainDragState.currentTranslateY = Math.min(dy, 0); + + coverEl.style.transform = `translateY(${curtainDragState.currentTranslateY}px)`; + coverEl.style.transition = 'none'; + + // If lifted enough, show content + if (curtainDragState.currentTranslateY < -80 && !curtainState.isRevealed) { + curtainState.isRevealed = true; + const idx = state.revealOrder[state.currentReveal]; + const role = state.roles[idx]; + const word = role === 'CIVIL' ? state.civilianWord : state.impostorWord; + const roleText = t(role.toLowerCase()); + document.getElementById('role-text').textContent = roleText; + document.getElementById('role-text').className = 'role ' + (role === 'CIVIL' ? 'civil' : 'impostor'); + document.getElementById('word-text').textContent = word; + } + + e.preventDefault(); // Prevent selection + }; + + // End function (touch and mouse) + const handleEnd = (e) => { + if (!curtainDragState.isDragging || curtainDragState.startY === null) return; + const coverEl = document.getElementById('curtain-cover'); + if (!coverEl) return; + + // ALWAYS bring the curtain down when released (GRAVITY) + coverEl.style.transition = 'transform 0.4s ease'; + coverEl.style.transform = 'translateY(0)'; + + // If content was revealed, show button after it falls + if (curtainState.isRevealed) { + setTimeout(() => { + if (state.currentReveal + 1 < state.numPlayers) { + document.getElementById('next-player-btn').style.display = 'block'; + } else { + document.getElementById('start-game-btn').style.display = 'block'; + } + }, 400); + } + + curtainDragState.startY = null; + curtainDragState.isDragging = false; + curtainDragState.currentTranslateY = 0; + }; + + // Touch events (mobile) + curtain.addEventListener('touchstart', handleStart, {passive: false}); + curtain.addEventListener('touchmove', handleMove, {passive: false}); + curtain.addEventListener('touchend', handleEnd, {passive: true}); + curtain.addEventListener('touchcancel', handleEnd, {passive: true}); + + // Mouse events (desktop) - start on curtain only + curtain.addEventListener('mousedown', handleStart); + + // Mouse move and up events on WINDOW so we can track even when cursor leaves everything + window.addEventListener('mousemove', handleMove); + window.addEventListener('mouseup', handleEnd); +} + +// Initialize curtain handlers when DOM is ready +if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', initCurtainHandlers); +} else { + initCurtainHandlers(); +} + +// ---------- Screen Wake Lock (prevent screen from sleeping during timers) ---------- +let wakeLock = null; +let wakeLockVideo = null; // For iOS workaround + +// Detect if device is iOS +function isIOS() { + return /iPad|iPhone|iPod/.test(navigator.userAgent) || + (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1); +} + +// Check if screen lock is enabled in settings +function isScreenLockEnabled() { + const saved = localStorage.getItem(SCREEN_LOCK_STORAGE_KEY); + return saved === null ? true : saved === 'true'; // Default enabled +} + +// Save screen lock preference +function setScreenLockEnabled(enabled) { + localStorage.setItem(SCREEN_LOCK_STORAGE_KEY, enabled.toString()); + updateScreenLockButton(); +} + +async function requestWakeLock() { + if (!isScreenLockEnabled()) return; + + // Try native Wake Lock API first (works on Android Chrome, etc.) + if ('wakeLock' in navigator) { + try { + wakeLock = await navigator.wakeLock.request('screen'); + wakeLock.addEventListener('release', () => { + wakeLock = null; + }); + console.log('Wake Lock activated (native API)'); + return; + } catch (err) { + console.log('Wake lock request failed:', err); + } + } + + // Fallback for iOS - use hidden video loop + if (isIOS() && !wakeLockVideo) { + try { + wakeLockVideo = document.createElement('video'); + wakeLockVideo.setAttribute('playsinline', ''); + wakeLockVideo.setAttribute('muted', ''); + wakeLockVideo.style.position = 'fixed'; + wakeLockVideo.style.opacity = '0'; + wakeLockVideo.style.pointerEvents = 'none'; + wakeLockVideo.style.width = '1px'; + wakeLockVideo.style.height = '1px'; + + // Minimal base64 encoded video (1 frame, silent) + wakeLockVideo.src = 'data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAAAu1tZGF0AAACrQYF//+p3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE1NSByMjkwMSA3ZDBmZjIyIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAxOCAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTMgZGVibG9jaz0xOjA6MCBhbmFseXNlPTB4MzoweDExMyBtZT1oZXggc3VibWU9NyBwc3k9MSBwc3lfcmQ9MS4wMDowLjAwIG1peGVkX3JlZj0xIG1lX3JhbmdlPTE2IGNocm9tYV9tZT0xIHRyZWxsaXM9MSA4eDhkY3Q9MSBjcW09MCBkZWFkem9uZT0yMSwxMSBmYXN0X3Bza2lwPTEgY2hyb21hX3FwX29mZnNldD0tMiB0aHJlYWRzPTMgbG9va2FoZWFkX3RocmVhZHM9MSBzbGljZWRfdGhyZWFkcz0wIG5yPTAgZGVjaW1hdGU9MSBpbnRlcmxhY2VkPTAgYmx1cmF5X2NvbXBhdD0wIGNvbnN0cmFpbmVkX2ludHJhPTAgYmZyYW1lcz0zIGJfcHlyYW1pZD0yIGJfYWRhcHQ9MSBiX2JpYXM9MCBkaXJlY3Q9MSB3ZWlnaHRiPTEgb3Blbl9nb3A9MCB3ZWlnaHRwPTIga2V5aW50PTI1MCBrZXlpbnRfbWluPTI1IHNjZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NDAgcmM9Y3JmIG1idHJlZT0xIGNyZj0yMy4wIHFjb21wPTAuNjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFxPTE6MS4wMACAAAAAwWWIhAAz//727L4FNf2f0JcRLMXaSnA+KqSAgHc0wAAAAwAAAwAAJuKiZ0WFMeJsgAAAHGAFBCwCPCVC'; + wakeLockVideo.loop = true; + + document.body.appendChild(wakeLockVideo); + await wakeLockVideo.play(); + console.log('Wake Lock activated (iOS video workaround)'); + } catch (err) { + console.log('iOS wake lock workaround failed:', err); + } + } +} + +function releaseWakeLock() { + // Release native Wake Lock + if (wakeLock) { + wakeLock.release(); + wakeLock = null; + } + + // Stop iOS video workaround + if (wakeLockVideo) { + wakeLockVideo.pause(); + wakeLockVideo.remove(); + wakeLockVideo = null; + } +} + +// Re-request wake lock when page becomes visible again +document.addEventListener('visibilitychange', async () => { + if (document.visibilityState === 'visible' && (wakeLock !== null || wakeLockVideo !== null)) { + await requestWakeLock(); + } +}); + +// ---------- Timers ---------- +let timerInterval = null; +async function startPhaseTimer(phase, seconds, elementId, onEnd) { + if (timerInterval) clearInterval(timerInterval); + + // Request wake lock to keep screen on during timer + await requestWakeLock(); + + const now = Date.now(); + state.timerPhase = phase; + state.timerEndAt = now + seconds*1000; + saveState(); + const el = document.getElementById(elementId); + const tick = () => { + const remaining = Math.max(0, Math.round((state.timerEndAt - Date.now())/1000)); + updateTimerDisplay(el, remaining); + if (remaining <= 0) { + clearInterval(timerInterval); + releaseWakeLock(); // Release wake lock when timer ends + playBeep(); + onEnd(); + } + }; + tick(); + timerInterval = setInterval(tick, 1000); +} + +function resumeTimerIfNeeded() { + if (!state.timerEndAt || !state.timerPhase) return; + const remaining = Math.round((state.timerEndAt - Date.now())/1000); + if (remaining <= 0) { state.timerEndAt = null; saveState(); return; } + if (state.timerPhase === 'game') { showScreen('game-screen'); startPhaseTimer('game', remaining, 'game-timer', startDeliberationPhase); } + else if (state.timerPhase === 'deliberation') { showScreen('deliberation-screen'); startPhaseTimer('deliberation', remaining, 'deliberation-timer', startVotingPhase); } +} + +function updateTimerDisplay(el, remaining) { + const minutes = Math.floor(remaining/60); const secs = remaining%60; + el.textContent = `${minutes}:${secs.toString().padStart(2,'0')}`; + el.className = 'timer'; + if (remaining <= 10) el.classList.add('danger'); else if (remaining <= 30) el.classList.add('warning'); +} + +function playBeep() { + // Play alarm sound - 3 ascending beeps pattern repeated twice + const ctx = new (window.AudioContext || window.webkitAudioContext)(); + const now = ctx.currentTime; + + // Frequencies for alarm pattern (ascending) + const frequencies = [523, 659, 784]; // C5, E5, G5 + const beepDuration = 0.15; + const gapDuration = 0.08; + const patternGap = 0.3; + + let time = now; + + // Play pattern twice + for (let pattern = 0; pattern < 2; pattern++) { + for (let i = 0; i < frequencies.length; i++) { + const osc = ctx.createOscillator(); + const gain = ctx.createGain(); + + osc.connect(gain); + gain.connect(ctx.destination); + osc.frequency.value = frequencies[i]; + osc.type = 'square'; // More alarm-like sound + + gain.gain.setValueAtTime(0, time); + gain.gain.linearRampToValueAtTime(0.25, time + 0.02); + gain.gain.setValueAtTime(0.25, time + beepDuration - 0.02); + gain.gain.linearRampToValueAtTime(0, time + beepDuration); + + osc.start(time); + osc.stop(time + beepDuration); + + time += beepDuration + gapDuration; + } + time += patternGap; + } +} + +// ---------- Game phases ---------- +function startGamePhase() { state.phase = 'game'; saveState(); showScreen('game-screen'); startPhaseTimer('game', state.gameTime, 'game-timer', startDeliberationPhase); } +function startDeliberationPhase() { state.phase = 'deliberation'; saveState(); showScreen('deliberation-screen'); startPhaseTimer('deliberation', state.deliberationTime, 'deliberation-timer', startVotingPhase); } +function startVotingPhase(candidates = null, isTiebreak = false) { + releaseWakeLock(); // Release wake lock when voting starts (no timer) + state.phase = 'voting'; + state.votingPlayer = 0; + state.votes = {}; + state.selections = []; + state.votingPool = candidates; + state.isTiebreak = isTiebreak; + saveState(); + renderVoting(); + showScreen('voting-screen'); +} +function skipToDeliberation() { if (timerInterval) clearInterval(timerInterval); releaseWakeLock(); startDeliberationPhase(); } +function skipToVoting() { if (timerInterval) clearInterval(timerInterval); releaseWakeLock(); startVotingPhase(); } +function startTiebreakDeliberation(candidates) { + state.phase = 'deliberation'; + state.tiebreakCandidates = candidates; + saveState(); + showScreen('deliberation-screen'); + startPhaseTimer('deliberation', 60, 'deliberation-timer', () => startVotingPhase(candidates, true)); +} + +// ---------- Secret voting ---------- +function renderVoting() { + const pool = state.votingPool || Array.from({length: state.numPlayers}, (_, i) => i); + const voter = state.playerNames[state.votingPlayer]; + document.getElementById('voter-name').textContent = voter; + document.getElementById('votes-needed').textContent = state.numImpostors; + + // Update voting instruction text + const votingInfo = document.querySelector('#voting-screen .info-text'); + if (votingInfo) { + votingInfo.innerHTML = `${t('passMobileTo')} ${voter}. ${t('chooseSuspects')} ${state.numImpostors} ${t('suspect')}.`; + } + + state.selections = state.selections || []; + const list = document.getElementById('vote-list'); list.innerHTML = ''; + pool.forEach(i => { + const item = document.createElement('div'); + item.className = 'player-item'; + + // Marcar como disabled ANTES de añadir al DOM para que la animación correcta se aplique + if (i === state.votingPlayer) { + item.classList.add('disabled'); + // NO aplicar opacity inline - dejamos que CSS lo maneje con la animación + item.style.pointerEvents = 'none'; + } + + item.textContent = state.playerNames[i]; + if (state.votes[i]) item.innerHTML += `${t('votes')}: ${state.votes[i]}`; + if (state.selections.includes(i)) item.classList.add('selected'); + + if (i !== state.votingPlayer) { + item.onclick = () => toggleSelection(i, item); + } + + list.appendChild(item); + }); + updateConfirmButton(); +} + +function toggleSelection(idx, el) { + if (idx === state.votingPlayer) return; + if (state.selections.includes(idx)) state.selections = state.selections.filter(x => x !== idx); + else { + if (state.selections.length >= state.numImpostors) return; + state.selections.push(idx); + } + saveState(); + renderVoting(); +} + +function updateConfirmButton() { + const btn = document.getElementById('confirm-vote-btn'); + btn.disabled = state.selections.length !== state.numImpostors; +} + +function confirmCurrentVote() { + state.selections.forEach(t => { state.votes[t] = (state.votes[t] || 0) + 1; }); + state.votingPlayer++; + state.selections = []; + saveState(); + if (state.votingPlayer >= state.numPlayers) { handleVoteOutcome(); return; } + renderVoting(); +} + +// ---------- Vote resolution ---------- +function handleVoteOutcome() { + const pool = state.votingPool || Array.from({length: state.numPlayers}, (_, i) => i); + const counts = pool.map(idx => ({ idx, votes: state.votes[idx] || 0 })); + counts.sort((a, b) => b.votes - a.votes); + + let slots = state.numImpostors; + const executed = []; + for (let i = 0; i < counts.length && slots > 0; ) { + const currentVotes = counts[i].votes; + const group = []; + let j = i; + while (j < counts.length && counts[j].votes === currentVotes) { group.push(counts[j].idx); j++; } + if (group.length <= slots) { + executed.push(...group); + slots -= group.length; + i = j; + } else { + // Tie for remaining slots + if (state.isTiebreak) { + // Second tie: impostors win + state.executed = []; + showResults(true); + return; + } + startTiebreakDeliberation(group); + return; + } + } + + state.executed = executed; + showResults(); +} + +// ---------- Results ---------- +function showResults(isTiebreak = false) { + state.phase = 'results'; saveState(); + + // Liberar Wake Lock cuando termina la partida + releaseWakeLock(); + + const executed = state.executed || []; + let impostorsAlive = 0; + state.roles.forEach((r,i) => { if (r === 'IMPOSTOR' && !executed.includes(i)) impostorsAlive++; }); + const winner = impostorsAlive > 0 ? 'IMPOSTORES' : 'CIVILES'; + const results = document.getElementById('results-content'); + const winText = winner === 'CIVILES' ? `✅ ${t('civiliansWin')}` : `❌ ${t('impostorsWin')}`; + results.innerHTML = ` +

${winText}

+

${t('executed')}: ${executed.length ? executed.map(i => state.playerNames[i]).join(', ') : t('nobody')}

+

${t('votes')}: ${Object.keys(state.votes).length ? '' : t('noVotes')}

+

${t('revealedRoles')}

+ ${state.roles.map((role,i) => { + const word = role === 'CIVIL' ? state.civilianWord : state.impostorWord; + const killed = executed.includes(i) ? 'executed' : ''; + const roleText = t(role.toLowerCase()); + return `
${state.playerNames[i]}: ${roleText} — "${word}" ${killed ? '☠️' : ''}
`; + }).join('')} + `; + showScreen('results-screen'); +} + +// ---------- Utilities ---------- +function showScreen(id) { + document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); + document.getElementById(id).classList.add('active'); + state.phase = id.replace('-screen',''); + saveState(); + updateExitButtonVisibility(); +} + +function newMatch() { + clearState(); + releaseWakeLock(); // Make sure wake lock is released when exiting game + if (timerInterval) clearInterval(timerInterval); + state = { ...state, phase:'welcome', timerEndAt:null, timerPhase:null, votingPool:null, isTiebreak:false, tiebreakCandidates:[] }; + saveState(); + showScreen('welcome-screen'); +} + +function confirmExitGame() { + const confirmMessage = currentLanguage === 'es' + ? '¿Estás seguro de que quieres salir de la partida? Se perderá todo el progreso actual.' + : 'Are you sure you want to exit the game? All current progress will be lost.'; + + if (confirm(confirmMessage)) { + newMatch(); + } +} + +function updateExitButtonVisibility() { + const exitBtn = document.getElementById('exit-game'); + const langBtn = document.getElementById('language-toggle'); + const screenLockBtn = document.getElementById('screen-lock-toggle'); + + // Show exit button and hide language/screen-lock toggles in all phases except welcome and setup + if (state.phase !== 'welcome' && state.phase !== 'setup') { + exitBtn.classList.add('visible'); + if (langBtn) langBtn.style.display = 'none'; + if (screenLockBtn) screenLockBtn.classList.remove('visible'); + } else { + exitBtn.classList.remove('visible'); + if (langBtn) langBtn.style.display = 'inline-flex'; + // Only show screen lock button on iOS + if (screenLockBtn && isIOS()) { + screenLockBtn.classList.add('visible'); + } + } +} + +// ---------- Theme system ---------- +function getSystemTheme() { + return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'; +} + +function loadTheme() { + const savedTheme = localStorage.getItem(THEME_STORAGE_KEY); + return savedTheme || getSystemTheme(); +} + +function saveTheme(theme) { + localStorage.setItem(THEME_STORAGE_KEY, theme); +} + +function applyTheme(theme) { + document.documentElement.setAttribute('data-theme', theme); + const themeIcon = document.querySelector('.theme-icon'); + if (themeIcon) { + themeIcon.textContent = theme === 'dark' ? '☀️' : '🌙'; + } +} + +function toggleTheme() { + const currentTheme = document.documentElement.getAttribute('data-theme') || 'light'; + const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; + applyTheme(newTheme); + saveTheme(newTheme); +} + +// Initialize theme +const initialTheme = loadTheme(); +applyTheme(initialTheme); + +// ---------- Screen Lock Button ---------- +function updateScreenLockButton() { + const btn = document.getElementById('screen-lock-toggle'); + if (!btn) return; + + const enabled = isScreenLockEnabled(); + const icon = btn.querySelector('.screen-lock-icon'); + + if (enabled) { + btn.classList.add('active'); + btn.setAttribute('title', 'Bloqueo de pantalla activado'); + if (icon) icon.textContent = '🔒'; + } else { + btn.classList.remove('active'); + btn.setAttribute('title', 'Bloqueo de pantalla desactivado'); + if (icon) icon.textContent = '🔓'; + } +} + +function toggleScreenLock() { + const currentState = isScreenLockEnabled(); + setScreenLockEnabled(!currentState); + + // If disabling, release any active wake lock + if (currentState) { + releaseWakeLock(); + } +} + +// Event listener for theme and language buttons +document.addEventListener('DOMContentLoaded', () => { + const themeToggle = document.getElementById('theme-toggle'); + if (themeToggle) { + themeToggle.addEventListener('click', toggleTheme); + } + + const languageToggle = document.getElementById('language-toggle'); + if (languageToggle) { + languageToggle.addEventListener('click', toggleLanguage); + } + + const screenLockToggle = document.getElementById('screen-lock-toggle'); + if (screenLockToggle) { + screenLockToggle.addEventListener('click', toggleScreenLock); + updateScreenLockButton(); + } + + // Initialize language + currentLanguage = loadLanguage(); + setLanguage(currentLanguage); + + // Detect system theme changes + window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { + // Only apply automatically if user hasn't manually selected a theme + if (!localStorage.getItem(THEME_STORAGE_KEY)) { + applyTheme(e.matches ? 'dark' : 'light'); + } + }); +}); + +// ---------- State rehydration ---------- +(function init() { + const restored = loadState(); + loadPoolsList(); + if (!state.turnDirection) state.turnDirection = 'horario'; + if (typeof state.startPlayer !== 'number') state.startPlayer = 0; + + // Set default values in inputs if we're in setup + if (state.phase === 'setup' || !restored) { + const defaultPlayers = 6; + const defaultImp = defaultImpostors(defaultPlayers); + const defaultGTime = defaultGameTime(defaultPlayers); + const defaultDTime = defaultDeliberation(defaultGTime); + + document.getElementById('num-players').value = defaultPlayers; + document.getElementById('num-impostors').value = defaultImp; + document.getElementById('num-impostors').max = Math.max(1, Math.floor(defaultPlayers / 2)); + document.getElementById('game-time').value = defaultGTime; + document.getElementById('deliberation-time').value = defaultDTime; + } + + // Determine initial screen + if (!restored || state.phase === 'setup' || state.phase === 'welcome') { + // If no saved state or we're in setup/welcome, show welcome + showScreen('welcome-screen'); + } else { + // If there's a game in progress, restore it + switch (state.phase) { + case 'names': buildNameInputs(); showScreen('names-screen'); break; + case 'pre-reveal': renderSummary(); showScreen('pre-reveal-screen'); break; + case 'reveal': showScreen('reveal-screen'); loadCurrentReveal(); break; + case 'game': showScreen('game-screen'); resumeTimerIfNeeded(); break; + case 'deliberation': showScreen('deliberation-screen'); resumeTimerIfNeeded(); break; + case 'voting': showScreen('voting-screen'); renderVoting(); break; + case 'results': showResults(); break; + default: showScreen('welcome-screen'); + } + } + + // Initialize exit button visibility + updateExitButtonVisibility(); + + // Initialize screen lock button for iOS + initScreenLockButton(); +})(); diff --git a/styles.1a37b506.css b/styles.1a37b506.css new file mode 100644 index 0000000..8e4ebaf --- /dev/null +++ b/styles.1a37b506.css @@ -0,0 +1,1829 @@ +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + EXPEDIENTE CLASIFICADO - IMPOSTOR GAME + Noir Cyberpunk Interrogation Aesthetic + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Special+Elite&display=swap'); + +:root { + /* LIGHT THEME: Interrogation Room */ + --bg-primary: #dcd9d2; + --bg-secondary: #c8c3b8; + --bg-overlay: rgba(0, 0, 0, 0.05); + + --surface-glass: rgba(255, 255, 255, 0.85); + --surface-card: rgba(255, 255, 255, 0.95); + --surface-hover: rgba(255, 255, 255, 1); + + --text-primary: #0a0a0a; + --text-secondary: #2a2a2a; + --text-tertiary: #5a5a5a; + --text-inverted: #ffffff; + + --accent-warning: #e6a73c; + --accent-danger: #d93626; + --accent-success: #2d8b3d; + --accent-info: #2e4e7a; + + --border-light: rgba(0, 0, 0, 0.18); + --border-medium: rgba(0, 0, 0, 0.35); + --border-heavy: rgba(0, 0, 0, 0.55); + + --shadow-sm: 0 3px 12px rgba(0, 0, 0, 0.15); + --shadow-md: 0 6px 24px rgba(0, 0, 0, 0.22); + --shadow-lg: 0 12px 48px rgba(0, 0, 0, 0.28); + --shadow-harsh: 6px 6px 0px rgba(0, 0, 0, 0.25); + + --grain-opacity: 0.05; + --scanline-opacity: 0.025; + + /* Spotlight effect */ + --spotlight-color: rgba(255, 235, 180, 0.08); + --vignette-intensity: 0.4; +} + +[data-theme="dark"] { + /* DARK THEME: Night Investigation */ + --bg-primary: #050505; + --bg-secondary: #0f0f0f; + --bg-overlay: rgba(255, 255, 255, 0.03); + + --surface-glass: rgba(25, 25, 25, 0.9); + --surface-card: rgba(35, 35, 35, 0.95); + --surface-hover: rgba(45, 45, 45, 1); + + --text-primary: #f5f5f5; + --text-secondary: #d0d0d0; + --text-tertiary: #909090; + --text-inverted: #0a0a0a; + + --accent-warning: #ffb84d; + --accent-danger: #ff3d2e; + --accent-success: #3dd46b; + --accent-info: #4d8ce0; + + --border-light: rgba(255, 255, 255, 0.12); + --border-medium: rgba(255, 255, 255, 0.22); + --border-heavy: rgba(255, 255, 255, 0.35); + + --shadow-sm: 0 3px 12px rgba(0, 0, 0, 0.6); + --shadow-md: 0 6px 24px rgba(0, 0, 0, 0.8); + --shadow-lg: 0 12px 48px rgba(0, 0, 0, 0.95); + --shadow-harsh: 6px 6px 0px rgba(0, 0, 0, 0.7); + + --grain-opacity: 0.07; + --scanline-opacity: 0.035; + + /* Spotlight effect */ + --spotlight-color: rgba(255, 200, 100, 0.04); + --vignette-intensity: 0.6; +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + BASE STYLES & TYPOGRAPHY + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + -webkit-tap-highlight-color: transparent; +} + +body { + font-family: 'JetBrains Mono', 'Courier Prime', 'Courier New', monospace; + background: + radial-gradient(ellipse 80% 50% at 50% 20%, var(--spotlight-color) 0%, transparent 50%), + radial-gradient(circle at 20% 30%, rgba(230, 167, 60, 0.08) 0%, transparent 40%), + radial-gradient(circle at 80% 70%, rgba(217, 54, 38, 0.06) 0%, transparent 40%), + var(--bg-primary); + min-height: 100vh; + min-height: 100dvh; + display: flex; + justify-content: center; + align-items: center; + padding: 70px 16px 16px; + color: var(--text-primary); + position: relative; + overflow: hidden; + font-size: 14px; + letter-spacing: 0px; + transition: background 0.5s ease, color 0.3s ease; +} + +/* Film grain texture overlay */ +body::before { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.5'/%3E%3C/svg%3E"); + opacity: var(--grain-opacity); + pointer-events: none; + z-index: 9999; + mix-blend-mode: overlay; + animation: grain 8s steps(10) infinite; +} + +@keyframes grain { + 0%, 100% { transform: translate(0, 0); } + 10% { transform: translate(-5%, -10%); } + 20% { transform: translate(-15%, 5%); } + 30% { transform: translate(7%, -25%); } + 40% { transform: translate(-5%, 25%); } + 50% { transform: translate(-15%, 10%); } + 60% { transform: translate(15%, 0%); } + 70% { transform: translate(0%, 15%); } + 80% { transform: translate(3%, 35%); } + 90% { transform: translate(-10%, 10%); } +} + +/* Scanlines */ +body::after { + content: ''; + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: repeating-linear-gradient( + 0deg, + transparent, + transparent 2px, + rgba(0, 0, 0, 0.1) 2px, + rgba(0, 0, 0, 0.1) 4px + ); + opacity: var(--scanline-opacity); + pointer-events: none; + z-index: 9998; +} + +/* Dramatic vignette overlay */ +.vignette-overlay { + position: fixed; + inset: 0; + background: radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,var(--vignette-intensity)) 100%); + pointer-events: none; + z-index: 9997; +} + +/* VHS interference effect */ +@keyframes vhsInterference { + 0%, 100% { opacity: 0; } + 5% { opacity: 0.03; transform: translateX(-2px); } + 10% { opacity: 0; } + 15% { opacity: 0.02; transform: translateX(1px); } + 20% { opacity: 0; } +} + +.vhs-line { + position: fixed; + left: 0; + width: 100%; + height: 3px; + background: linear-gradient(90deg, transparent, rgba(255,255,255,0.8), transparent); + pointer-events: none; + z-index: 9996; + animation: vhsScan 8s linear infinite; + opacity: 0.04; +} + +@keyframes vhsScan { + 0% { top: -10px; } + 100% { top: 110%; } +} + +h1 { + font-family: 'Bebas Neue', 'Crimson Text', Georgia, serif; + text-align: center; + margin-bottom: 20px; + font-size: 2.6em; + font-weight: 400; + letter-spacing: 4px; + text-transform: uppercase; + position: relative; + text-shadow: 3px 3px 0px var(--bg-secondary), 0 0 30px rgba(230, 167, 60, 0.2); + line-height: 1.1; + animation: titleReveal 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards; +} + +@keyframes titleReveal { + from { + opacity: 0; + letter-spacing: 20px; + filter: blur(8px); + } + to { + opacity: 1; + letter-spacing: 4px; + filter: blur(0); + } +} + +h1::after { + content: ''; + display: block; + width: 80px; + height: 4px; + background: linear-gradient(90deg, var(--accent-danger) 0%, var(--accent-warning) 50%, var(--accent-danger) 100%); + background-size: 200% 100%; + margin: 14px auto 0; + box-shadow: 0 0 15px rgba(230, 167, 60, 0.5); + animation: shimmer 3s ease-in-out infinite; +} + +@keyframes shimmer { + 0%, 100% { background-position: 0% 50%; } + 50% { background-position: 100% 50%; } +} + +h2 { + font-family: 'Crimson Text', Georgia, serif; + text-align: center; + margin: 16px 0; + font-size: 1.4em; + font-weight: 700; + letter-spacing: 0.5px; +} + +h3 { + font-family: 'JetBrains Mono', monospace; + font-size: 1em; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 1.5px; + margin-bottom: 12px; +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + CONTAINER & SCREENS + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.container { + width: 100%; + max-width: 480px; + background: var(--surface-glass); + backdrop-filter: blur(20px) saturate(150%); + border-radius: 0; + padding: 28px 22px; + box-shadow: var(--shadow-harsh), var(--shadow-lg); + border: 4px solid var(--border-heavy); + display: flex; + flex-direction: column; + transition: all 0.4s ease; + margin-bottom: 20px; + position: relative; + overflow: hidden; + clip-path: polygon( + 0 20px, + 20px 0, + 100% 0, + 100% calc(100% - 20px), + calc(100% - 20px) 100%, + 0 100% + ); +} + +.container::before { + content: '⬢ CLASSIFIED ⬢'; + position: absolute; + top: 8px; + left: 50%; + transform: translateX(-50%); + font-size: 0.65em; + letter-spacing: 3px; + opacity: 0.4; + font-weight: 800; + color: var(--accent-danger); + text-shadow: 0 0 10px rgba(217, 54, 38, 0.3); + animation: classifiedPulse 4s ease-in-out infinite; +} + +@keyframes classifiedPulse { + 0%, 100% { opacity: 0.4; text-shadow: 0 0 10px rgba(217, 54, 38, 0.3); } + 50% { opacity: 0.6; text-shadow: 0 0 20px rgba(217, 54, 38, 0.6); } +} + +/* Diagonal classified stamp */ +.container::after { + content: 'EXPEDIENTE'; + position: absolute; + bottom: 15px; + right: -30px; + font-family: 'Special Elite', 'Courier Prime', monospace; + font-size: 0.7em; + letter-spacing: 3px; + color: var(--accent-danger); + opacity: 0.12; + transform: rotate(-45deg); + font-weight: 400; + white-space: nowrap; + pointer-events: none; +} + +.screen { + display: none; + animation: screenEnter 0.35s cubic-bezier(0.22, 1, 0.36, 1); + flex: 1; + overflow: hidden; + min-height: 0; +} + +.screen.active { + display: flex; + flex-direction: column; +} + +@keyframes screenEnter { + 0% { + opacity: 0; + transform: translateY(30px) scale(0.95); + filter: blur(4px); + } + 60% { + opacity: 1; + filter: blur(0); + } + 100% { + opacity: 1; + transform: translateY(0) scale(1); + filter: blur(0); + } +} + +/* Staggered children animation */ +.screen.active > * { + animation: fadeSlideUp 0.5s cubic-bezier(0.22, 1, 0.36, 1) backwards; +} + +.screen.active > *:nth-child(1) { animation-delay: 0.05s; } +.screen.active > *:nth-child(2) { animation-delay: 0.1s; } +.screen.active > *:nth-child(3) { animation-delay: 0.15s; } +.screen.active > *:nth-child(4) { animation-delay: 0.2s; } +.screen.active > *:nth-child(5) { animation-delay: 0.25s; } +.screen.active > *:nth-child(6) { animation-delay: 0.3s; } +.screen.active > *:nth-child(7) { animation-delay: 0.35s; } +.screen.active > *:nth-child(8) { animation-delay: 0.4s; } + +@keyframes fadeSlideUp { + from { + opacity: 0; + transform: translateY(15px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + FORM CONTROLS + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.form-group { + margin-bottom: 16px; +} + +.form-group.compact { + margin-bottom: 12px; +} + +label { + display: block; + margin-bottom: 8px; + font-weight: 700; + font-size: 0.8em; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 1.2px; +} + +input { + width: 100%; + padding: 12px 14px; + border: 2px solid var(--border-medium); + border-radius: 0; + font-size: 0.95em; + font-family: 'JetBrains Mono', monospace; + background: var(--surface-card); + color: var(--text-primary); + transition: all 0.2s ease; + box-shadow: inset 2px 2px 4px rgba(0, 0, 0, 0.1); +} + +input:focus { + outline: none; + border-color: var(--accent-warning); + box-shadow: inset 2px 2px 4px rgba(0, 0, 0, 0.1), 0 0 0 3px rgba(212, 165, 116, 0.2); + transform: translateY(-1px); +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + BUTTONS + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +button { + width: 100%; + padding: 16px 20px; + border: 3px solid var(--text-primary); + border-radius: 0; + font-size: 0.9em; + font-weight: 800; + font-family: 'JetBrains Mono', monospace; + cursor: pointer; + background: var(--text-primary); + color: var(--text-inverted); + box-shadow: var(--shadow-harsh); + transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1); + margin-top: 12px; + text-transform: uppercase; + letter-spacing: 0.8px; + position: relative; + overflow: hidden; + clip-path: polygon( + 0 0, + calc(100% - 12px) 0, + 100% 12px, + 100% 100%, + 12px 100%, + 0 calc(100% - 12px) + ); +} + +button::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 0; + height: 0; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + transform: translate(-50%, -50%); + transition: width 0.5s ease, height 0.5s ease; +} + +button:hover::before { + width: 300px; + height: 300px; +} + +button:hover { + box-shadow: 8px 8px 0px rgba(0, 0, 0, 0.3); + filter: brightness(1.1); +} + +button:active { + box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15); + filter: brightness(0.95); +} + +button.secondary { + background: linear-gradient(135deg, var(--accent-warning) 0%, #c48a2e 100%); + border-color: var(--accent-warning); + color: var(--text-inverted); + box-shadow: var(--shadow-harsh), 0 0 15px rgba(230, 167, 60, 0.25); +} + +button.ghost { + background: transparent; + color: var(--text-primary); + border-color: var(--border-medium); + box-shadow: none; +} + +button.ghost:hover { + background: var(--surface-hover); + box-shadow: var(--shadow-harsh); +} + +button:disabled { + opacity: 0.4; + cursor: not-allowed; + pointer-events: none; +} + +.btn-primary { + background: linear-gradient(135deg, var(--accent-danger) 0%, #b8301e 100%); + border-color: var(--accent-danger); + box-shadow: var(--shadow-harsh), 0 0 20px rgba(217, 54, 38, 0.3); +} + +.btn-secondary { + background: linear-gradient(135deg, var(--accent-info) 0%, #1e3a5f 100%); + border-color: var(--accent-info); + box-shadow: var(--shadow-harsh), 0 0 20px rgba(46, 78, 122, 0.3); +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + WELCOME SCREEN + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.welcome-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + height: 100%; + gap: 24px; + padding: 20px 0; +} + +.welcome-logo { + width: 140px; + height: 140px; + object-fit: contain; + filter: drop-shadow(5px 5px 0px var(--bg-secondary)) + drop-shadow(0 0 30px rgba(230, 167, 60, 0.3)) + grayscale(0.2) contrast(1.15); + animation: logoFloat 4s ease-in-out infinite, logoGlitch 8s step-end infinite; + position: relative; +} + +@keyframes logoFloat { + 0%, 100% { transform: translateY(0) rotate(0deg); } + 25% { transform: translateY(-8px) rotate(-2deg); } + 75% { transform: translateY(-8px) rotate(2deg); } +} + +@keyframes logoGlitch { + 0%, 90%, 100% { + filter: drop-shadow(4px 4px 0px var(--bg-secondary)) + drop-shadow(0 0 20px var(--border-heavy)) + grayscale(0.3) contrast(1.1); + } + 91% { + filter: drop-shadow(6px 4px 0px var(--accent-danger)) + drop-shadow(0 0 20px var(--accent-danger)) + grayscale(0) contrast(1.3); + } + 92% { + filter: drop-shadow(4px 6px 0px var(--accent-info)) + drop-shadow(0 0 20px var(--accent-info)) + grayscale(0) contrast(1.3); + } + 93% { + filter: drop-shadow(4px 4px 0px var(--bg-secondary)) + drop-shadow(0 0 20px var(--border-heavy)) + grayscale(0.3) contrast(1.1); + } +} + +.welcome-title { + font-size: 2.8em; + margin: 0; + font-family: 'Bebas Neue', 'Crimson Text', Georgia, serif; + font-weight: 400; + text-shadow: 4px 4px 0px var(--bg-secondary), 0 0 40px rgba(230, 167, 60, 0.25); + letter-spacing: 6px; + line-height: 1; + position: relative; + animation: welcomeTitleReveal 0.8s cubic-bezier(0.22, 1, 0.36, 1) forwards; +} + +@keyframes welcomeTitleReveal { + 0% { + opacity: 0; + letter-spacing: 30px; + filter: blur(10px); + transform: scale(0.9); + } + 100% { + opacity: 1; + letter-spacing: 6px; + filter: blur(0); + transform: scale(1); + } +} + +.welcome-subtitle { + font-size: 0.95em; + color: var(--text-secondary); + margin: -10px 0 0 0; + font-weight: 400; + letter-spacing: 0.5px; + font-family: 'JetBrains Mono', monospace; +} + +.welcome-buttons { + display: flex; + flex-direction: column; + gap: 12px; + width: 100%; + max-width: 320px; + margin-top: 10px; +} + +.welcome-credits { + color: var(--text-tertiary); + font-size: 0.75em; + margin-top: auto; + font-weight: 400; + letter-spacing: 1px; + text-transform: uppercase; +} + +.welcome-credits::before { + content: '───── '; +} + +.welcome-credits::after { + content: ' ─────'; +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + RULES SCREEN + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.rules-content { + flex: 1; + overflow-y: auto; + overflow-x: hidden; + padding: 10px 0; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + -ms-overflow-style: none; +} + +.rules-content::-webkit-scrollbar { + display: none; +} + +.rule-section { + background: var(--surface-card); + border: 3px solid var(--border-medium); + border-left: 8px solid var(--accent-warning); + border-radius: 0; + padding: 18px; + margin-bottom: 16px; + transition: all 0.3s cubic-bezier(0.22, 1, 0.36, 1); + position: relative; + box-shadow: var(--shadow-md); + clip-path: polygon( + 0 0, + 100% 0, + 100% calc(100% - 10px), + calc(100% - 10px) 100%, + 0 100% + ); + animation: ruleSlideIn 0.4s cubic-bezier(0.22, 1, 0.36, 1) backwards; +} + +.rule-section:nth-child(1) { animation-delay: 0.1s; } +.rule-section:nth-child(2) { animation-delay: 0.2s; } +.rule-section:nth-child(3) { animation-delay: 0.3s; } +.rule-section:nth-child(4) { animation-delay: 0.4s; } + +@keyframes ruleSlideIn { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.rule-section::before { + content: '▸'; + position: absolute; + left: -3px; + top: 18px; + font-size: 1.5em; + color: var(--accent-warning); + animation: blink 2s ease-in-out infinite; +} + +@keyframes blink { + 0%, 49%, 100% { opacity: 1; } + 50%, 99% { opacity: 0; } +} + +.rule-section:hover { + background: var(--surface-hover); + border-left-color: var(--accent-danger); + box-shadow: var(--shadow-lg); +} + +.rule-section h3 { + margin: 0 0 14px 0; + color: var(--text-primary); + font-size: 0.95em; +} + +.rule-section p { + margin: 8px 0; + color: var(--text-secondary); + line-height: 1.7; + font-size: 0.85em; + letter-spacing: 0.3px; +} + +.rule-section strong { + color: var(--accent-danger); + font-weight: 800; + text-transform: uppercase; + font-size: 0.9em; + letter-spacing: 0.5px; +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + PLAYER MANAGEMENT + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.player-names-list { + flex: 1 1 auto; + min-height: 0; + max-height: 360px; /* Altura máxima para activar scroll y mostrar fila parcial - efecto peek */ + overflow-y: scroll; + overflow-x: hidden; + margin-bottom: 12px; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE y Edge */ + /* Visual frame to indicate scrollable area */ + background: var(--surface-card); + border: 4px solid var(--border-heavy); + border-radius: 0; + padding: 12px; + box-shadow: inset 0 4px 12px rgba(0, 0, 0, 0.15), + inset 0 -4px 12px rgba(0, 0, 0, 0.15), + var(--shadow-md); + /* Gradiente para crear efecto peek - texto cortado visible */ + -webkit-mask-image: linear-gradient(to bottom, + black 0%, + black calc(100% - 40px), + transparent 100%); + mask-image: linear-gradient(to bottom, + black 0%, + black calc(100% - 40px), + transparent 100%); +} + +.player-names-list::-webkit-scrollbar { + display: none; /* Chrome, Safari, Opera */ +} + +.player-name-item { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 8px; + background: var(--bg-secondary); + padding: 12px; + border-radius: 0; + border: 2px solid var(--border-light); + border-left: 4px solid var(--accent-info); + transition: all 0.2s ease; + box-shadow: var(--shadow-sm); +} + +.player-name-item:last-child { + margin-bottom: 0; +} + +.player-name-item:hover { + background: var(--surface-hover); + border-left-color: var(--accent-warning); + transform: translateX(2px); +} + +.player-name-item span { + font-weight: 800; + min-width: 80px; + font-size: 0.8em; + color: var(--text-secondary); + text-transform: uppercase; + letter-spacing: 1px; +} + +.player-name-item input { + flex: 1; + padding: 10px; + margin: 0; + font-size: 0.85em; + border-width: 2px; +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + CURTAIN REVEAL MECHANISM + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.curtain { + position: relative; + width: 100%; + height: 280px; + background: var(--bg-secondary); + border-radius: 0; + overflow: hidden; + margin: 12px 0; + box-shadow: inset 0 0 40px rgba(0, 0, 0, 0.3), var(--shadow-harsh); + cursor: grab; + user-select: none; + border: 3px solid var(--border-heavy); + flex-shrink: 0; +} + +.curtain:active { + cursor: grabbing; +} + +.curtain-cover { + position: absolute; + inset: 0; + background: + repeating-linear-gradient( + 0deg, + #2a2a2a 0px, + #2a2a2a 8px, + #1a1a1a 8px, + #1a1a1a 12px + ), + linear-gradient(180deg, rgba(255,200,100,0.03) 0%, transparent 50%); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 16px; + font-size: 1.1em; + font-weight: 800; + color: #888; + transition: transform 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); + z-index: 10; + user-select: none; + box-shadow: inset 0 -20px 40px rgba(0, 0, 0, 0.5), inset 0 0 60px rgba(0,0,0,0.3); + letter-spacing: 2px; + font-family: 'Bebas Neue', 'JetBrains Mono', monospace; +} + +.curtain-cover::after { + content: ''; + position: absolute; + bottom: -8px; + left: 0; + right: 0; + height: 8px; + background: linear-gradient(90deg, + transparent 0%, + rgba(0,0,0,0.3) 25%, + rgba(0,0,0,0.5) 50%, + rgba(0,0,0,0.3) 75%, + transparent 100%); +} + +.curtain-cover.lifted { + transform: translateY(-100%); +} + +.curtain-icon { + font-size: 2.5em; + animation: bounce 2s ease-in-out infinite; +} + +@keyframes bounce { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-12px); } +} + +.curtain-content { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 20px; + padding: 20px; + text-align: center; +} + +.role { + font-size: 2.4em; + font-weight: 400; + padding: 16px 32px; + border-radius: 0; + text-transform: uppercase; + border: 4px solid; + font-family: 'Bebas Neue', 'JetBrains Mono', monospace; + letter-spacing: 6px; + box-shadow: var(--shadow-harsh); + position: relative; + animation: roleReveal 0.4s cubic-bezier(0.22, 1, 0.36, 1) forwards; +} + +@keyframes roleReveal { + 0% { + opacity: 0; + transform: scale(0.5) rotate(-5deg); + filter: blur(10px); + } + 50% { + transform: scale(1.1) rotate(2deg); + } + 100% { + opacity: 1; + transform: scale(1) rotate(0); + filter: blur(0); + } +} + +.role.civil { + background: var(--accent-success); + color: var(--text-inverted); + border-color: #3d5a40; + animation: civilPulse 2s ease-in-out infinite; +} + +.role.impostor { + background: var(--accent-danger); + color: var(--text-inverted); + border-color: #8a2e26; + animation: impostorPulse 1.5s ease-in-out infinite; +} + +@keyframes civilPulse { + 0%, 100% { box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.3), 0 0 0px rgba(90, 125, 95, 0.5); } + 50% { box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.3), 0 0 25px rgba(90, 125, 95, 0.8); } +} + +@keyframes impostorPulse { + 0%, 100% { box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.3), 0 0 0px rgba(196, 69, 54, 0.5); } + 50% { box-shadow: 4px 4px 0px rgba(0, 0, 0, 0.3), 0 0 30px rgba(196, 69, 54, 0.9); } +} + +.word { + font-size: 2em; + font-weight: 400; + background: var(--surface-card); + padding: 20px 36px; + border-radius: 0; + border: 3px solid var(--border-heavy); + font-family: 'Special Elite', 'Crimson Text', serif; + letter-spacing: 2px; + box-shadow: var(--shadow-harsh); + color: var(--text-primary); + text-transform: uppercase; + animation: wordReveal 0.5s cubic-bezier(0.22, 1, 0.36, 1) 0.2s forwards; + opacity: 0; +} + +@keyframes wordReveal { + 0% { + opacity: 0; + transform: translateY(20px); + filter: blur(5px); + } + 100% { + opacity: 1; + transform: translateY(0); + filter: blur(0); + } +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + TIMER + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.timer { + font-size: 4em; + font-weight: 800; + text-align: center; + margin: 20px 0; + padding: 24px; + background: var(--surface-card); + border-radius: 0; + border: 5px solid var(--border-heavy); + font-family: 'Bebas Neue', 'JetBrains Mono', monospace; + letter-spacing: 8px; + box-shadow: var(--shadow-harsh), inset 0 0 30px rgba(0, 0, 0, 0.2); + position: relative; + clip-path: polygon( + 16px 0, + calc(100% - 16px) 0, + 100% 16px, + 100% calc(100% - 16px), + calc(100% - 16px) 100%, + 16px 100%, + 0 calc(100% - 16px), + 0 16px + ); + animation: timerAppear 0.5s cubic-bezier(0.22, 1, 0.36, 1) forwards; +} + +@keyframes timerAppear { + from { + opacity: 0; + transform: scale(0.8); + filter: blur(5px); + } + to { + opacity: 1; + transform: scale(1); + filter: blur(0); + } +} + +.timer::before { + content: ''; + position: absolute; + top: 8px; + right: 8px; + width: 12px; + height: 12px; + background: var(--accent-success); + border-radius: 50%; + box-shadow: 0 0 10px var(--accent-success); + animation: statusBlink 2s ease-in-out infinite; +} + +@keyframes statusBlink { + 0%, 49%, 100% { opacity: 1; } + 50%, 99% { opacity: 0.3; } +} + +.timer.warning { + color: var(--accent-warning); + border-color: var(--accent-warning); + animation: timerShake 0.5s ease-in-out infinite; +} + +.timer.warning::before { + background: var(--accent-warning); + box-shadow: 0 0 10px var(--accent-warning); +} + +.timer.danger { + color: var(--accent-danger); + border-color: var(--accent-danger); + animation: timerShake 0.25s ease-in-out infinite, dangerFlash 1s ease-in-out infinite; +} + +.timer.danger::before { + background: var(--accent-danger); + box-shadow: 0 0 15px var(--accent-danger); + animation: statusBlink 0.5s ease-in-out infinite; +} + +@keyframes timerShake { + 0%, 100% { transform: translateX(0); } + 25% { transform: translateX(-4px); } + 75% { transform: translateX(4px); } +} + +@keyframes dangerFlash { + 0%, 100% { background: var(--surface-card); } + 50% { background: rgba(196, 69, 54, 0.15); } +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + INFO BOXES & CONTENT + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.info-text { + text-align: center; + margin: 14px 0; + font-size: 0.85em; + line-height: 1.7; + background: var(--surface-card); + padding: 14px 16px; + border-radius: 0; + color: var(--text-secondary); + border: 2px solid var(--border-light); + border-left: 5px solid var(--accent-info); + box-shadow: var(--shadow-sm), inset 4px 0 8px rgba(46, 78, 122, 0.1); + letter-spacing: 0.3px; +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + PLAYER SELECTION GRID + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.player-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 10px; + margin: 12px 0; + flex: 1; + overflow-y: auto; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; + padding: 4px; + scrollbar-width: none; + -ms-overflow-style: none; +} + +.player-list::-webkit-scrollbar { + display: none; +} + +.player-item { + padding: 18px 14px; + min-height: 80px; /* Altura fija para evitar cambios de tamaño con vote-count */ + background: var(--surface-card); + border-radius: 0; + text-align: center; + cursor: pointer; + transition: all 0.25s cubic-bezier(0.22, 1, 0.36, 1); + font-weight: 800; + font-size: 0.85em; + border: 3px solid var(--border-medium); + box-shadow: var(--shadow-sm); + letter-spacing: 0.5px; + text-transform: uppercase; + position: relative; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + clip-path: polygon( + 8px 0, + 100% 0, + 100% calc(100% - 8px), + calc(100% - 8px) 100%, + 0 100%, + 0 8px + ); + animation: playerItemAppear 0.3s cubic-bezier(0.22, 1, 0.36, 1) backwards; +} + +.player-item:nth-child(1) { animation-delay: 0.05s; } +.player-item:nth-child(2) { animation-delay: 0.1s; } +.player-item:nth-child(3) { animation-delay: 0.15s; } +.player-item:nth-child(4) { animation-delay: 0.2s; } +.player-item:nth-child(5) { animation-delay: 0.25s; } +.player-item:nth-child(6) { animation-delay: 0.3s; } +.player-item:nth-child(7) { animation-delay: 0.35s; } +.player-item:nth-child(8) { animation-delay: 0.4s; } +.player-item:nth-child(9) { animation-delay: 0.45s; } +.player-item:nth-child(10) { animation-delay: 0.5s; } + +@keyframes playerItemAppear { + from { + opacity: 0; + transform: scale(0.8); + } + to { + opacity: 1; + transform: scale(1); + } +} + +.player-item::before { + content: '□'; + position: absolute; + top: 6px; + right: 6px; + font-size: 1.2em; + transition: all 0.2s ease; +} + +.player-item:hover { + background: var(--surface-hover); + box-shadow: 6px 6px 0px rgba(0, 0, 0, 0.25); + filter: brightness(1.05); +} + +.player-item:active { + box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15); + filter: brightness(0.95); +} + +.player-item.selected { + background: var(--accent-danger); + border-color: var(--text-primary); + color: var(--text-inverted); + box-shadow: 0 0 0 4px rgba(217, 54, 38, 0.5), 6px 6px 0px rgba(0, 0, 0, 0.4); + animation: selectPulse 0.3s ease-out; +} + +@keyframes selectPulse { + 0% { transform: scale(1); } + 50% { transform: scale(1.08); } + 100% { transform: scale(1); } +} + +.player-item.selected::before { + content: '☑'; + animation: checkAppear 0.2s ease-out; +} + +@keyframes checkAppear { + from { transform: scale(0) rotate(-180deg); } + to { transform: scale(1) rotate(0); } +} + +.player-item.disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; + background: var(--bg-secondary); + border-color: var(--border-light); + filter: grayscale(0.6); + animation: playerItemAppearDisabled 0.3s cubic-bezier(0.22, 1, 0.36, 1) backwards !important; +} + +.player-item.disabled::before { + content: '✕'; + color: var(--text-tertiary); +} + +.player-item .vote-count { + display: block; + font-size: 0.7em; + margin-top: 4px; + opacity: 0.75; + font-weight: 600; + letter-spacing: 0.3px; + min-height: 1em; /* Reservar espacio siempre */ +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + RESULTS + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.results { + background: var(--surface-card); + border-radius: 0; + padding: 14px; + margin: 8px 0; + flex: 1; + overflow: visible; + border: 2px solid var(--border-medium); + box-shadow: var(--shadow-md); + animation: resultsReveal 0.5s cubic-bezier(0.22, 1, 0.36, 1) forwards; +} + +@keyframes resultsReveal { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.results h2 { + font-family: 'Bebas Neue', 'Crimson Text', Georgia, serif; + font-size: 1.6em; + letter-spacing: 3px; + margin-bottom: 10px; + animation: winnerReveal 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards; +} + +@keyframes winnerReveal { + 0% { + opacity: 0; + transform: scale(0.5); + filter: blur(10px); + } + 60% { + transform: scale(1.1); + } + 100% { + opacity: 1; + transform: scale(1); + filter: blur(0); + } +} + +.role-reveal { + background: var(--bg-secondary); + padding: 8px 10px; + border-radius: 0; + margin: 5px 0; + border-left: 4px solid; + font-size: 0.8em; + letter-spacing: 0.2px; + box-shadow: var(--shadow-sm); + transition: all 0.2s ease; + animation: roleRevealSlide 0.4s cubic-bezier(0.22, 1, 0.36, 1) backwards; +} + +.role-reveal:nth-child(1) { animation-delay: 0.3s; } +.role-reveal:nth-child(2) { animation-delay: 0.4s; } +.role-reveal:nth-child(3) { animation-delay: 0.5s; } +.role-reveal:nth-child(4) { animation-delay: 0.6s; } +.role-reveal:nth-child(5) { animation-delay: 0.7s; } +.role-reveal:nth-child(6) { animation-delay: 0.8s; } +.role-reveal:nth-child(7) { animation-delay: 0.9s; } +.role-reveal:nth-child(8) { animation-delay: 1s; } +.role-reveal:nth-child(9) { animation-delay: 1.1s; } +.role-reveal:nth-child(10) { animation-delay: 1.2s; } + +@keyframes roleRevealSlide { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.role-reveal:hover { + transform: translateX(3px); +} + +.role-reveal.civil-reveal { + border-left-color: var(--accent-success); +} + +.role-reveal.impostor-reveal { + border-left-color: var(--accent-danger); +} + +.role-reveal.executed { + opacity: 0.5; + background: rgba(0, 0, 0, 0.2); + text-decoration: line-through; +} + +.tag { + display: inline-block; + padding: 6px 10px; + border-radius: 0; + background: var(--surface-hover); + margin: 4px 0; + font-weight: 800; + font-size: 0.75em; + border: 2px solid var(--border-medium); + letter-spacing: 1px; + text-transform: uppercase; +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + POOL SELECTION + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.pool-buttons { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 8px; + padding: 0; +} + +.pool-buttons-wrapper { + position: relative; + flex: 1 1 auto; + min-height: 0; + max-height: 320px; /* Ajustado para mostrar fila parcial - efecto peek */ + overflow-y: scroll; + overflow-x: hidden; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; /* Firefox */ + -ms-overflow-style: none; /* IE y Edge */ + /* Visual frame to indicate scrollable area */ + background: var(--surface-card); + border: 4px solid var(--border-heavy); + border-radius: 0; + padding: 12px; + margin: 12px 0; + box-shadow: inset 0 4px 12px rgba(0, 0, 0, 0.15), + inset 0 -4px 12px rgba(0, 0, 0, 0.15), + var(--shadow-md); + /* Gradiente para crear efecto peek - texto cortado visible */ + -webkit-mask-image: linear-gradient(to bottom, + black 0%, + black calc(100% - 50px), + transparent 100%); + mask-image: linear-gradient(to bottom, + black 0%, + black calc(100% - 50px), + transparent 100%); +} + +.pool-buttons-wrapper::-webkit-scrollbar { + display: none; /* Chrome, Safari, Opera */ +} + +.pool-btn { + padding: 12px 10px; + border-radius: 0; + border: 2px solid var(--border-medium); + background: var(--surface-card); + color: var(--text-primary); + font-weight: 700; + font-size: 0.8em; + cursor: pointer; + transition: all 0.18s ease; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-transform: uppercase; + letter-spacing: 0.5px; + box-shadow: var(--shadow-sm); +} + +.pool-btn:hover { + background: var(--surface-hover); + box-shadow: 3px 3px 0px rgba(0, 0, 0, 0.15); + filter: brightness(1.05); +} + +.pool-btn.selected { + border-color: var(--text-primary); + background: var(--accent-warning); + color: var(--text-inverted); + box-shadow: 0 0 0 3px rgba(212, 165, 116, 0.3), 3px 3px 0px rgba(0, 0, 0, 0.2); +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + FIXED UI CONTROLS (Theme, Language, Exit) + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.theme-toggle { + position: fixed; + top: 20px; + right: 20px; + width: 56px; + height: 56px; + border-radius: 0; + border: 3px solid var(--border-heavy); + background: var(--surface-glass); + backdrop-filter: blur(20px); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.6em; + box-shadow: var(--shadow-harsh); + transition: all 0.2s ease; + z-index: 1000; + margin: 0; + padding: 0; +} + +.theme-toggle:hover { + box-shadow: 8px 8px 0px rgba(0, 0, 0, 0.2); + filter: brightness(1.1); +} + +.theme-toggle:active { + box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15); + filter: brightness(0.95); +} + +.theme-icon { + transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55); + display: inline-block; +} + +.theme-toggle:hover .theme-icon { + transform: rotate(180deg) scale(1.1); +} + +.language-toggle { + position: fixed; + top: 86px; + right: 20px; + width: auto; + min-width: 56px; + height: 56px; + padding: 0 16px; + border-radius: 0; + border: 3px solid var(--border-heavy); + background: var(--surface-glass); + backdrop-filter: blur(20px); + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + font-size: 1em; + font-weight: 800; + box-shadow: var(--shadow-harsh); + transition: all 0.2s ease; + z-index: 1000; + color: var(--text-primary); + margin: 0; + text-transform: uppercase; + letter-spacing: 1px; +} + +.language-toggle:hover { + box-shadow: 8px 8px 0px rgba(0, 0, 0, 0.2); + filter: brightness(1.1); +} + +.language-toggle:active { + box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15); + filter: brightness(0.95); +} + +.language-icon { + font-size: 1.3em; + transition: transform 0.3s ease; + display: inline-block; +} + +.language-text { + font-size: 0.85em; + letter-spacing: 1.5px; + font-family: 'JetBrains Mono', monospace; +} + +.exit-game { + position: fixed; + top: 20px; + left: 20px; + width: auto; + height: 56px; + padding: 0 16px; + border-radius: 0; + border: 3px solid var(--border-heavy); + background: var(--surface-glass); + backdrop-filter: blur(20px); + cursor: pointer; + display: none; + align-items: center; + justify-content: center; + gap: 10px; + font-size: 0.85em; + font-weight: 800; + box-shadow: var(--shadow-harsh); + transition: all 0.2s ease; + z-index: 1000; + color: var(--text-primary); + text-transform: uppercase; + letter-spacing: 1px; +} + +.exit-game.visible { + display: inline-flex; +} + +.exit-game:hover { + box-shadow: 8px 8px 0px rgba(0, 0, 0, 0.2); + background: var(--accent-danger); + color: var(--text-inverted); +} + +.exit-game:active { + box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15); +} + +.exit-icon { + font-size: 1.3em; +} + +.exit-text { + font-size: 0.9em; + font-family: 'JetBrains Mono', monospace; +} + +.screen-lock-toggle { + position: fixed; + top: 152px; + right: 20px; + width: 56px; + height: 56px; + border-radius: 0; + border: 3px solid var(--border-heavy); + background: var(--surface-glass); + backdrop-filter: blur(20px); + cursor: pointer; + display: none; + align-items: center; + justify-content: center; + font-size: 1.6em; + box-shadow: var(--shadow-harsh); + transition: all 0.2s ease; + z-index: 1000; + margin: 0; + padding: 0; +} + +.screen-lock-toggle.visible { + display: inline-flex; +} + +.screen-lock-toggle:hover { + box-shadow: 8px 8px 0px rgba(0, 0, 0, 0.2); + filter: brightness(1.1); +} + +.screen-lock-toggle:active { + box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.15); + filter: brightness(0.95); +} + +.screen-lock-toggle.active { + background: var(--accent-success); + color: var(--text-inverted); + border-color: var(--accent-success); +} + +.screen-lock-icon { + transition: transform 0.3s ease; + display: inline-block; +} + +.screen-lock-toggle:hover .screen-lock-icon { + transform: scale(1.1); +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + RESPONSIVE DESIGN + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +@media (max-width: 600px) { + body { + padding: 60px 10px 10px 10px; + font-size: 13px; + } + + h1 { + font-size: 1.7em; + margin-bottom: 14px; + } + + .container { + padding: 20px 16px; + } + + .theme-toggle, + .language-toggle, + .exit-game, + .screen-lock-toggle { + top: 8px; + width: 44px; + height: 44px; + min-width: 44px; + } + + .language-toggle { + top: 58px; + } + + .screen-lock-toggle { + top: 108px; + } + + .exit-game { + padding: 0 12px; + font-size: 0.75em; + height: 44px; + } + + /* Ocultar textos en móvil, solo emojis */ + .exit-text, + .language-text { + display: none; + } + + .exit-game { + padding: 0; + width: 44px; + min-width: 44px; + } + + .language-toggle { + padding: 0; + width: 44px; + min-width: 44px; + } + + .exit-icon, + .language-icon { + font-size: 1.4em; + } + + .timer { + font-size: 2.5em; + padding: 16px; + } + + .welcome-title { + font-size: 1.8em; + } + + .role { + font-size: 1.6em; + padding: 10px 18px; + } + + .word { + font-size: 1.3em; + padding: 12px 20px; + } + + .form-group { + margin-bottom: 10px; + } + + .form-group.compact { + margin-bottom: 8px; + } + + button { + padding: 12px 16px; + margin-top: 8px; + } + + .rule-section { + padding: 12px; + margin-bottom: 12px; + } + + .rule-section h3 { + font-size: 0.85em; + margin-bottom: 10px; + } + + .rule-section p { + font-size: 0.8em; + margin: 6px 0; + } + + .player-name-item { + padding: 10px; + margin-bottom: 6px; + } + + .player-name-item span { + font-size: 0.75em; + min-width: 70px; + } + + .player-item { + padding: 14px 10px; + min-height: 72px; /* Altura fija también en móvil */ + font-size: 0.8em; + } + + .pool-btn { + padding: 10px 8px; + font-size: 0.75em; + } + + .pool-buttons-wrapper { + max-height: 240px; /* Ajustado para mostrar fila parcial en móvil - efecto peek */ + padding: 10px; + margin: 10px 0; + } + + .player-names-list { + max-height: 280px; /* Ajustado para mostrar fila parcial en móvil - efecto peek */ + padding: 10px; + } + + .info-text { + padding: 12px 14px; + font-size: 0.8em; + margin: 10px 0; + } + + .curtain { + height: 240px; + margin: 10px 0; + } +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + UTILITY ANIMATIONS + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +@keyframes typewriter { + from { width: 0; } + to { width: 100%; } +} + +@keyframes glitch { + 0% { transform: translate(0); } + 20% { transform: translate(-2px, 2px); } + 40% { transform: translate(-2px, -2px); } + 60% { transform: translate(2px, 2px); } + 80% { transform: translate(2px, -2px); } + 100% { transform: translate(0); } +} + +/* Smooth scrolling */ +* { + scrollbar-width: thin; + scrollbar-color: var(--border-medium) transparent; +} + +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; +} + +::-webkit-scrollbar-thumb { + background: var(--border-medium); + border-radius: 0; +} + +::-webkit-scrollbar-thumb:hover { + background: var(--border-heavy); +} diff --git a/styles.26a5b74f.css b/styles.26a5b74f.css deleted file mode 100644 index 5b64e14..0000000 --- a/styles.26a5b74f.css +++ /dev/null @@ -1,455 +0,0 @@ -/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - EXPEDIENTE CLASIFICADO - IMPOSTOR GAME - Noir Cyberpunk Interrogation Aesthetic - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ - -@import url('https://fonts.googleapis.com/css2?family=Bebas+Neue&family=Special+Elite&display=swap'); - -:root { - /* LIGHT THEME: Interrogation Room */ - --bg-primary: #dcd9d2; - --bg-secondary: #c8c3b8; - --bg-overlay: rgba(0, 0, 0, 0.05); - - --surface-glass: rgba(255, 255, 255, 0.85); - --surface-card: rgba(255, 255, 255, 0.95); - --surface-hover: rgba(255, 255, 255, 1); - - --text-primary: #0a0a0a; - --text-secondary: #2a2a2a; - --text-tertiary: #5a5a5a; - --text-inverted: #ffffff; - - --accent-warning: #e6a73c; - --accent-danger: #d93626; - --accent-success: #2d8b3d; - --accent-info: #2e4e7a; - - --border-light: rgba(0, 0, 0, 0.18); - --border-medium: rgba(0, 0, 0, 0.35); - --border-heavy: rgba(0, 0, 0, 0.55); - - --shadow-sm: 0 3px 12px rgba(0, 0, 0, 0.15); - --shadow-md: 0 6px 24px rgba(0, 0, 0, 0.22); - --shadow-lg: 0 12px 48px rgba(0, 0, 0, 0.28); - --shadow-harsh: 6px 6px 0px rgba(0, 0, 0, 0.25); - - --grain-opacity: 0.05; - --scanline-opacity: 0.025; - - /* Spotlight effect */ - --spotlight-color: rgba(255, 235, 180, 0.08); - --vignette-intensity: 0.4; -} - -[data-theme="dark"] { - /* DARK THEME: Night Investigation */ - --bg-primary: #050505; - --bg-secondary: #0f0f0f; - --bg-overlay: rgba(255, 255, 255, 0.03); - - --surface-glass: rgba(25, 25, 25, 0.9); - --surface-card: rgba(35, 35, 35, 0.95); - --surface-hover: rgba(45, 45, 45, 1); - - --text-primary: #f5f5f5; - --text-secondary: #d0d0d0; - --text-tertiary: #909090; - --text-inverted: #0a0a0a; - - --accent-warning: #ffb84d; - --accent-danger: #ff3d2e; - --accent-success: #3dd46b; - --accent-info: #4d8ce0; - - --border-light: rgba(255, 255, 255, 0.12); - --border-medium: rgba(255, 255, 255, 0.22); - --border-heavy: rgba(255, 255, 255, 0.35); - - --shadow-sm: 0 3px 12px rgba(0, 0, 0, 0.6); - --shadow-md: 0 6px 24px rgba(0, 0, 0, 0.8); - --shadow-lg: 0 12px 48px rgba(0, 0, 0, 0.95); - --shadow-harsh: 6px 6px 0px rgba(0, 0, 0, 0.7); - - --grain-opacity: 0.07; - --scanline-opacity: 0.035; - - /* Spotlight effect */ - --spotlight-color: rgba(255, 200, 100, 0.04); - --vignette-intensity: 0.6; -} - -/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - BASE STYLES & TYPOGRAPHY - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ - -* { - margin: 0; - padding: 0; - box-sizing: border-box; - -webkit-tap-highlight-color: transparent; -} - -body { - font-family: 'JetBrains Mono', 'Courier Prime', 'Courier New', monospace; - background: - radial-gradient(ellipse 80% 50% at 50% 20%, var(--spotlight-color) 0%, transparent 50%), - radial-gradient(circle at 20% 30%, rgba(230, 167, 60, 0.08) 0%, transparent 40%), - radial-gradient(circle at 80% 70%, rgba(217, 54, 38, 0.06) 0%, transparent 40%), - var(--bg-primary); - min-height: 100vh; - min-height: 100dvh; - display: flex; - justify-content: center; - align-items: center; - padding: 70px 16px 16px; - color: var(--text-primary); - position: relative; - overflow: hidden; - font-size: 14px; - letter-spacing: 0px; - transition: background 0.5s ease, color 0.3s ease; -} - -/* Film grain texture overlay */ -body::before { - content: ''; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.5'/%3E%3C/svg%3E"); - opacity: var(--grain-opacity); - pointer-events: none; - z-index: 9999; - mix-blend-mode: overlay; - animation: grain 8s steps(10) infinite; -} - -@keyframes grain { - 0%, 100% { transform: translate(0, 0); } - 10% { transform: translate(-5%, -10%); } - 20% { transform: translate(-15%, 5%); } - 30% { transform: translate(7%, -25%); } - 40% { transform: translate(-5%, 25%); } - 50% { transform: translate(-15%, 10%); } - 60% { transform: translate(15%, 0%); } - 70% { transform: translate(0%, 15%); } - 80% { transform: translate(3%, 35%); } - 90% { transform: translate(-10%, 10%); } -} - -/* Scanlines */ -body::after { - content: ''; - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: repeating-linear-gradient( - 0deg, - transparent, - transparent 2px, - rgba(0, 0, 0, 0.1) 2px, - rgba(0, 0, 0, 0.1) 4px - ); - opacity: var(--scanline-opacity); - pointer-events: none; - z-index: 9998; -} - -/* Dramatic vignette overlay */ -.vignette-overlay { - position: fixed; - inset: 0; - background: radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,var(--vignette-intensity)) 100%); - pointer-events: none; - z-index: 9997; -} - -/* VHS interference effect */ -@keyframes vhsInterference { - 0%, 100% { opacity: 0; } - 5% { opacity: 0.03; transform: translateX(-2px); } - 10% { opacity: 0; } - 15% { opacity: 0.02; transform: translateX(1px); } - 20% { opacity: 0; } -} - -.vhs-line { - position: fixed; - left: 0; - width: 100%; - height: 3px; - background: linear-gradient(90deg, transparent, rgba(255,255,255,0.8), transparent); - pointer-events: none; - z-index: 9996; - animation: vhsScan 8s linear infinite; - opacity: 0.04; -} - -@keyframes vhsScan { - 0% { top: -10px; } - 100% { top: 110%; } -} - -h1 { - font-family: 'Bebas Neue', 'Crimson Text', Georgia, serif; - text-align: center; - margin-bottom: 20px; - font-size: 2.6em; - font-weight: 400; - letter-spacing: 4px; - text-transform: uppercase; - position: relative; - text-shadow: 3px 3px 0px var(--bg-secondary), 0 0 30px rgba(230, 167, 60, 0.2); - line-height: 1.1; - animation: titleReveal 0.6s cubic-bezier(0.22, 1, 0.36, 1) forwards; -} - -@keyframes titleReveal { - from { - opacity: 0; - letter-spacing: 20px; - filter: blur(8px); - } - to { - opacity: 1; - letter-spacing: 4px; - filter: blur(0); - } -} - -h1::after { - content: ''; - display: block; - width: 80px; - height: 4px; - background: linear-gradient(90deg, var(--accent-danger) 0%, var(--accent-warning) 50%, var(--accent-danger) 100%); - background-size: 200% 100%; - margin: 14px auto 0; - box-shadow: 0 0 15px rgba(230, 167, 60, 0.5); - animation: shimmer 3s ease-in-out infinite; -} - -@keyframes shimmer { - 0%, 100% { background-position: 0% 50%; } - 50% { background-position: 100% 50%; } -} - -h2 { - font-family: 'Crimson Text', Georgia, serif; - text-align: center; - margin: 16px 0; - font-size: 1.4em; - font-weight: 700; - letter-spacing: 0.5px; -} - -h3 { - font-family: 'JetBrains Mono', monospace; - font-size: 1em; - font-weight: 800; - text-transform: uppercase; - letter-spacing: 1.5px; - margin-bottom: 12px; -} - -/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - CONTAINER & SCREENS - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ - -.container { - width: 100%; - max-width: 480px; - background: var(--surface-glass); - backdrop-filter: blur(20px) saturate(150%); - border-radius: 0; - padding: 28px 22px; - box-shadow: var(--shadow-harsh), var(--shadow-lg); - border: 4px solid var(--border-heavy); - display: flex; - flex-direction: column; - transition: all 0.4s ease; - margin-bottom: 20px; - position: relative; - overflow: hidden; - clip-path: polygon( - 0 20px, - 20px 0, - 100% 0, - 100% calc(100% - 20px), - calc(100% - 20px) 100%, - 0 100% - ); -} - -.container::before { - content: '⬢ CLASSIFIED ⬢'; - position: absolute; - top: 8px; - left: 50%; - transform: translateX(-50%); - font-size: 0.65em; - letter-spacing: 3px; - opacity: 0.4; - font-weight: 800; - color: var(--accent-danger); - text-shadow: 0 0 10px rgba(217, 54, 38, 0.3); - animation: classifiedPulse 4s ease-in-out infinite; -} - -@keyframes classifiedPulse { - 0%, 100% { opacity: 0.4; text-shadow: 0 0 10px rgba(217, 54, 38, 0.3); } - 50% { opacity: 0.6; text-shadow: 0 0 20px rgba(217, 54, 38, 0.6); } -} - -/* Diagonal classified stamp */ -.container::after { - content: 'EXPEDIENTE'; - position: absolute; - bottom: 15px; - right: -30px; - font-family: 'Special Elite', 'Courier Prime', monospace; - font-size: 0.7em; - letter-spacing: 3px; - color: var(--accent-danger); - opacity: 0.12; - transform: rotate(-45deg); - font-weight: 400; - white-space: nowrap; - pointer-events: none; -} - -.screen { - display: none; - animation: screenEnter 0.35s cubic-bezier(0.22, 1, 0.36, 1); - flex: 1; - overflow: hidden; - min-height: 0; -} - -.screen.active { - display: flex; - flex-direction: column; -} - -@keyframes screenEnter { - 0% { - opacity: 0; - transform: translateY(30px) scale(0.95); - filter: blur(4px); - } - 60% { - opacity: 1; - filter: blur(0); - } - 100% { - opacity: 1; - transform: translateY(0) scale(1); - filter: blur(0); - } -} - -/* Staggered children animation */ -.screen.active > * { - animation: fadeSlideUp 0.5s cubic-bezier(0.22, 1, 0.36, 1) backwards; -} - -.screen.active > *:nth-child(1) { animation-delay: 0.05s; } -.screen.active > *:nth-child(2) { animation-delay: 0.1s; } -.screen.active > *:nth-child(3) { animation-delay: 0.15s; } -.screen.active > *:nth-child(4) { animation-delay: 0.2s; } -.screen.active > *:nth-child(5) { animation-delay: 0.25s; } -.screen.active > *:nth-child(6) { animation-delay: 0.3s; } -.screen.active > *:nth-child(7) { animation-delay: 0.35s; } -.screen.active > *:nth-child(8) { animation-delay: 0.4s; } - -@keyframes fadeSlideUp { - from { - opacity: 0; - transform: translateY(15px); - } - to { - opacity: 1; - transform: translateY(0); - } -} - -/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - FORM CONTROLS - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ - -.form-group { - margin-bottom: 16px; -} - -.form-group.compact { - margin-bottom: 12px; -} - -label { - display: block; - margin-bottom: 8px; - font-weight: 700; - font-size: 0.8em; - color: var(--text-secondary); - text-transform: uppercase; - letter-spacing: 1.2px; -} - -input { - width: 100%; - padding: 12px 14px; - border: 2px solid var(--border-medium); - border-radius: 0; - font-size: 0.95em; - font-family: 'JetBrains Mono', monospace; - background: var(--surface-card); - color: var(--text-primary); - transition: all 0.2s ease; - box-shadow: inset 2px 2px 4px rgba(0, 0, 0, 0.1); -} - -input:focus { - outline: none; - border-color: var(--accent-warning); - box-shadow: inset 2px 2px 4px rgba(0, 0, 0, 0.1), 0 0 0 3px rgba(212, 165, 116, 0.2); - transform: translateY(-1px); -} - -/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - BUTTONS - ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ - -button { - width: 100%; - padding: 16px 20px; - border: 3px solid var(--text-primary); - border-radius: 0; - font-size: 0.9em; - font-weight: 800; - font-family: 'JetBrains Mono', monospace; - cursor: pointer; - background: var(--text-primary); - color: var(--text-inverted); - box-shadow: var(--shadow-harsh); - transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1); - margin-top: 12px; - text-transform: uppercase; - letter-spacing: 0.8px; - position: relative; - overflow: hidden; - clip-path: polygon( - 0 0, - calc(100% - 12px) 0, - 100% 12px, - 100% 100%, - 12px 100%, - 0 calc(100% - 12px) - ); -} - - From d012bfe1cac808fc6bd2c5246cf5c08e06fec306 Mon Sep 17 00:00:00 2001 From: Dasemu Date: Wed, 14 Jan 2026 13:34:09 +0100 Subject: [PATCH 6/9] feat(versioning): update asset references in HTML for new versioning scheme --- index.html | 4 ++-- version-assets.sh | 14 +++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/index.html b/index.html index 74489f2..6ab1b9c 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,7 @@ - + @@ -186,7 +186,7 @@
- + diff --git a/version-assets.sh b/version-assets.sh index b729216..7355a1c 100755 --- a/version-assets.sh +++ b/version-assets.sh @@ -49,17 +49,21 @@ for asset in "${ASSETS[@]}"; do echo "📦 Versionando: $asset → $versioned" cp "$asset" "$versioned" - # Actualizar referencia en HTML + # Obtener nombre base y extensión para el patrón + base="${asset%.*}" + ext="${asset##*.}" + + # Actualizar referencia en HTML (busca versión original o hasheada) case "$asset" in *.js) - sed -i "s|src=\"${asset}\"|src=\"${versioned}\"|g" "$HTML_FILE" + sed -i -E "s|src=\"${base}(\.[a-f0-9]{8})?\.${ext}\"|src=\"${versioned}\"|g" "$HTML_FILE" ;; *.css) - sed -i "s|href=\"${asset}\"|href=\"${versioned}\"|g" "$HTML_FILE" + sed -i -E "s|href=\"${base}(\.[a-f0-9]{8})?\.${ext}\"|href=\"${versioned}\"|g" "$HTML_FILE" ;; *.png) - sed -i "s|href=\"${asset}\"|href=\"${versioned}\"|g" "$HTML_FILE" - sed -i "s|src=\"${asset}\"|src=\"${versioned}\"|g" "$HTML_FILE" + sed -i -E "s|href=\"${base}(\.[a-f0-9]{8})?\.${ext}\"|href=\"${versioned}\"|g" "$HTML_FILE" + sed -i -E "s|src=\"${base}(\.[a-f0-9]{8})?\.${ext}\"|src=\"${versioned}\"|g" "$HTML_FILE" ;; esac From 4f85ea84d3cee9fe7595119cfd8266207aa76058 Mon Sep 17 00:00:00 2001 From: Dasemu Date: Wed, 14 Jan 2026 13:34:09 +0100 Subject: [PATCH 7/9] feat(versioning): add additional asset references for versioning in YAML configuration --- .github/workflows/version-assets.yml | 3 +++ index.html | 4 ++-- version-assets.sh | 14 +++++++++----- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/.github/workflows/version-assets.yml b/.github/workflows/version-assets.yml index 9f577da..15ce8c3 100644 --- a/.github/workflows/version-assets.yml +++ b/.github/workflows/version-assets.yml @@ -8,6 +8,9 @@ on: - 'script.js' - 'styles.css' - 'logo.png' + - 'index.html' + - 'version-assets.sh' + - '.github/workflows/version-assets.yml' jobs: version-assets: diff --git a/index.html b/index.html index 74489f2..6ab1b9c 100644 --- a/index.html +++ b/index.html @@ -7,7 +7,7 @@ - + @@ -186,7 +186,7 @@ - + diff --git a/version-assets.sh b/version-assets.sh index b729216..7355a1c 100755 --- a/version-assets.sh +++ b/version-assets.sh @@ -49,17 +49,21 @@ for asset in "${ASSETS[@]}"; do echo "📦 Versionando: $asset → $versioned" cp "$asset" "$versioned" - # Actualizar referencia en HTML + # Obtener nombre base y extensión para el patrón + base="${asset%.*}" + ext="${asset##*.}" + + # Actualizar referencia en HTML (busca versión original o hasheada) case "$asset" in *.js) - sed -i "s|src=\"${asset}\"|src=\"${versioned}\"|g" "$HTML_FILE" + sed -i -E "s|src=\"${base}(\.[a-f0-9]{8})?\.${ext}\"|src=\"${versioned}\"|g" "$HTML_FILE" ;; *.css) - sed -i "s|href=\"${asset}\"|href=\"${versioned}\"|g" "$HTML_FILE" + sed -i -E "s|href=\"${base}(\.[a-f0-9]{8})?\.${ext}\"|href=\"${versioned}\"|g" "$HTML_FILE" ;; *.png) - sed -i "s|href=\"${asset}\"|href=\"${versioned}\"|g" "$HTML_FILE" - sed -i "s|src=\"${asset}\"|src=\"${versioned}\"|g" "$HTML_FILE" + sed -i -E "s|href=\"${base}(\.[a-f0-9]{8})?\.${ext}\"|href=\"${versioned}\"|g" "$HTML_FILE" + sed -i -E "s|src=\"${base}(\.[a-f0-9]{8})?\.${ext}\"|src=\"${versioned}\"|g" "$HTML_FILE" ;; esac From dcc279ddda038b43bc0b8657ad509a69e3af94f1 Mon Sep 17 00:00:00 2001 From: Dasemu Date: Sat, 17 Jan 2026 18:45:01 +0100 Subject: [PATCH 8/9] feat(seo): add comprehensive SEO optimization for better search visibility Add meta tags (description, keywords, author, robots, theme-color), Open Graph and Twitter Card tags for social sharing, JSON-LD structured data, canonical URLs with hreflang support, robots.txt and sitemap.xml. --- index.html | 73 ++++++++++++++++++++++++++++++++++++++++++++++++++++- robots.txt | 18 +++++++++++++ sitemap.xml | 13 ++++++++++ 3 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 robots.txt create mode 100644 sitemap.xml diff --git a/index.html b/index.html index 6ab1b9c..5648931 100644 --- a/index.html +++ b/index.html @@ -3,12 +3,83 @@ - Juego del Impostor + Juego del Impostor - Juego de Rol Gratis Online | Encuentra al Impostor + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..c5ee997 --- /dev/null +++ b/robots.txt @@ -0,0 +1,18 @@ +# Robots.txt for Juego del Impostor +# https://impostor.dariosevilla.es + +User-agent: * +Allow: / + +# Sitemap location +Sitemap: https://impostor.dariosevilla.es/sitemap.xml + +# Allow all crawlers to access the main content +User-agent: Googlebot +Allow: / + +User-agent: Bingbot +Allow: / + +# Crawl-delay for polite crawling (optional) +Crawl-delay: 1 diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..817dc5f --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,13 @@ + + + + https://impostor.dariosevilla.es/ + 2026-01-17 + monthly + 1.0 + + + + + From 880d327a5d2e031eff5642f14346a82c573bf81d Mon Sep 17 00:00:00 2001 From: Dasemu Date: Sat, 17 Jan 2026 18:54:01 +0100 Subject: [PATCH 9/9] Chore: Changing from github actions to gitea actions only. --- {.github => .gitea}/workflows/version-assets.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {.github => .gitea}/workflows/version-assets.yml (100%) diff --git a/.github/workflows/version-assets.yml b/.gitea/workflows/version-assets.yml similarity index 100% rename from .github/workflows/version-assets.yml rename to .gitea/workflows/version-assets.yml