diff --git a/.github/workflows/build-apk.yml b/.github/workflows/build-apk.yml
index c23b061..3f1b406 100644
--- a/.github/workflows/build-apk.yml
+++ b/.github/workflows/build-apk.yml
@@ -38,7 +38,7 @@ jobs:
working-directory: android
run: ./gradlew assembleDebug
- - name: Upload APK
+ - name: Upload APK>
uses: actions/upload-artifact@v4
with:
name: impostor-game-debug
diff --git a/.gitea/workflows/version-assets.yml b/.github/workflows/version-assets.yml
similarity index 84%
rename from .gitea/workflows/version-assets.yml
rename to .github/workflows/version-assets.yml
index 15ce8c3..5d05864 100644
--- a/.gitea/workflows/version-assets.yml
+++ b/.github/workflows/version-assets.yml
@@ -5,10 +5,10 @@ on:
branches:
- main
paths:
- - 'script.js'
- - 'styles.css'
- - 'logo.png'
- - 'index.html'
+ - 'www/script.js'
+ - 'www/styles.css'
+ - 'www/logo.png'
+ - 'www/index.html'
- 'version-assets.sh'
- '.github/workflows/version-assets.yml'
@@ -26,7 +26,7 @@ jobs:
- name: Delete old versioned assets
run: |
echo "ποΈ Borrando archivos hasheados antiguos..."
- rm -f *.*.js *.*.css *.*.png || true
+ rm -f www/*.*.js www/*.*.css www/*.*.png || true
git add -A
- name: Run asset versioning
@@ -49,6 +49,6 @@ jobs:
run: |
git config --local user.email "ci@dariosevilla.es"
git config --local user.name "CI Action"
- git add *.*.js *.*.css *.*.png index.html
+ git add www/*.*.js www/*.*.css www/*.*.png www/index.html
git commit -m "chore: update asset versions [skip ci]"
git push
\ No newline at end of file
diff --git a/logo.78f51359.png b/logo.78f51359.png
deleted file mode 100644
index 61cc5bf..0000000
Binary files a/logo.78f51359.png and /dev/null differ
diff --git a/script.f88d8968.js b/script.f88d8968.js
deleted file mode 100644
index 05c0b74..0000000
--- a/script.f88d8968.js
+++ /dev/null
@@ -1,1347 +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 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
deleted file mode 100644
index 8e4ebaf..0000000
--- a/styles.1a37b506.css
+++ /dev/null
@@ -1,1829 +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)
- );
-}
-
-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/version-assets.sh b/version-assets.sh
index 7355a1c..2dcc46f 100755
--- a/version-assets.sh
+++ b/version-assets.sh
@@ -7,6 +7,9 @@ set -e
echo "π Iniciando versionado de archivos estΓ‘ticos..."
echo ""
+# Directorio de trabajo
+WWW_DIR="www"
+
# Archivos a versionar
ASSETS=("script.js" "styles.css" "logo.png")
HTML_FILE="index.html"
@@ -28,26 +31,29 @@ get_versioned_name() {
# Limpiar archivos versionados antiguos
echo "ποΈ Limpiando versiones antiguas..."
-rm -f *.*.js *.*.css *.*.png 2>/dev/null || true
+rm -f "$WWW_DIR"/*.*.js "$WWW_DIR"/*.*.css "$WWW_DIR"/*.*.png 2>/dev/null || true
echo ""
# Crear backup del HTML
-cp "$HTML_FILE" "${HTML_FILE}.bak"
+cp "$WWW_DIR/$HTML_FILE" "$WWW_DIR/${HTML_FILE}.bak"
# Versionar cada archivo
for asset in "${ASSETS[@]}"; do
- if [[ ! -f "$asset" ]]; then
- echo "β οΈ Advertencia: $asset no encontrado, saltando..."
+ asset_path="$WWW_DIR/$asset"
+
+ if [[ ! -f "$asset_path" ]]; then
+ echo "β οΈ Advertencia: $asset_path no encontrado, saltando..."
continue
fi
# Generar hash
- hash=$(generate_hash "$asset")
+ hash=$(generate_hash "$asset_path")
versioned=$(get_versioned_name "$asset" "$hash")
+ versioned_path="$WWW_DIR/$versioned"
# Copiar archivo con versiΓ³n
echo "π¦ Versionando: $asset β $versioned"
- cp "$asset" "$versioned"
+ cp "$asset_path" "$versioned_path"
# Obtener nombre base y extensiΓ³n para el patrΓ³n
base="${asset%.*}"
@@ -56,14 +62,14 @@ for asset in "${ASSETS[@]}"; do
# Actualizar referencia en HTML (busca versiΓ³n original o hasheada)
case "$asset" in
*.js)
- sed -i -E "s|src=\"${base}(\.[a-f0-9]{8})?\.${ext}\"|src=\"${versioned}\"|g" "$HTML_FILE"
+ sed -i -E "s|src=\"${base}(\.[a-f0-9]{8})?\.${ext}\"|src=\"${versioned}\"|g" "$WWW_DIR/$HTML_FILE"
;;
*.css)
- sed -i -E "s|href=\"${base}(\.[a-f0-9]{8})?\.${ext}\"|href=\"${versioned}\"|g" "$HTML_FILE"
+ sed -i -E "s|href=\"${base}(\.[a-f0-9]{8})?\.${ext}\"|href=\"${versioned}\"|g" "$WWW_DIR/$HTML_FILE"
;;
*.png)
- 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"
+ sed -i -E "s|href=\"${base}(\.[a-f0-9]{8})?\.${ext}\"|href=\"${versioned}\"|g" "$WWW_DIR/$HTML_FILE"
+ sed -i -E "s|src=\"${base}(\.[a-f0-9]{8})?\.${ext}\"|src=\"${versioned}\"|g" "$WWW_DIR/$HTML_FILE"
;;
esac
@@ -72,9 +78,9 @@ for asset in "${ASSETS[@]}"; do
done
# Limpiar backup
-rm -f "${HTML_FILE}.bak"
+rm -f "$WWW_DIR/${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
+ls -1 "$WWW_DIR"/*.*.{js,css,png} 2>/dev/null || echo " (ninguno)"
diff --git a/www/index.html b/www/index.html
index 8c94588..e6ebbac 100644
--- a/www/index.html
+++ b/www/index.html
@@ -77,17 +77,17 @@
-
+
+
+
+
-
-
-
-
+
@@ -117,7 +117,7 @@
-

+
Juego del Impostor
ΒΏPodrΓ‘s descubrir quiΓ©n es el impostor?
@@ -265,7 +265,7 @@
-
+