feat: enhance UI with screen lock, timer improvements and better styles

- Implement screen lock for iOS/Android (Wake Lock API + video workaround)
- Fix curtain reveal animation to prevent visual confusion
- Add audio alarm when timer ends for game and deliberation phases
- Improve overall UI/UX with scroll enhancements and mobile optimizations
This commit is contained in:
2026-01-14 04:04:12 +01:00
parent 3b39080cca
commit 49e981a4b5
3 changed files with 2038 additions and 448 deletions

View File

@@ -4,11 +4,18 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no"> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Juego del Impostor</title> <title>Juego del Impostor</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Crimson+Text:wght@600;700&family=Courier+Prime:wght@400;700&family=JetBrains+Mono:wght@400;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="styles.css">
<link rel="icon" type="image/png" href="logo.png"> <link rel="icon" type="image/png" href="logo.png">
<script defer src="https://analytics.dariosevilla.es/script.js" data-website-id="0520a008-d309-477f-9742-b4a674ac42eb"></script> <script defer src="https://analytics.dariosevilla.es/script.js" data-website-id="0520a008-d309-477f-9742-b4a674ac42eb"></script>
</head> </head>
<body> <body>
<!-- Cinematic overlays -->
<div class="vignette-overlay"></div>
<div class="vhs-line"></div>
<button id="theme-toggle" class="theme-toggle" aria-label="Cambiar tema"> <button id="theme-toggle" class="theme-toggle" aria-label="Cambiar tema">
<span class="theme-icon">🌙</span> <span class="theme-icon">🌙</span>
</button> </button>
@@ -18,6 +25,10 @@
<span class="language-text">EN</span> <span class="language-text">EN</span>
</button> </button>
<button id="screen-lock-toggle" class="screen-lock-toggle" aria-label="Screen lock" title="Bloqueo de pantalla">
<span class="screen-lock-icon">🔓</span>
</button>
<button id="exit-game" class="exit-game" onclick="confirmExitGame()"> <button id="exit-game" class="exit-game" onclick="confirmExitGame()">
<span class="exit-icon">🚪</span> <span class="exit-icon">🚪</span>
<span class="exit-text" data-i18n="exitGame">Salir de la partida</span> <span class="exit-text" data-i18n="exitGame">Salir de la partida</span>
@@ -28,53 +39,55 @@
<div id="welcome-screen" class="screen active"> <div id="welcome-screen" class="screen active">
<div class="welcome-content"> <div class="welcome-content">
<img src="logo.png" alt="Logo" class="welcome-logo"> <img src="logo.png" alt="Logo" class="welcome-logo">
<h1 class="welcome-title">🎭 Juego del Impostor</h1> <h1 class="welcome-title">Juego del Impostor</h1>
<p class="welcome-subtitle">¿Podrás descubrir quién es el impostor?</p> <p class="welcome-subtitle">¿Podrás descubrir quién es el impostor?</p>
<div class="welcome-buttons"> <div class="welcome-buttons">
<button onclick="showScreen('setup-screen')" class="btn-primary">▶️ Jugar</button> <button onclick="showScreen('setup-screen')" class="btn-primary">Jugar</button>
<button onclick="showScreen('rules-screen')" class="btn-secondary">📖 Reglas</button> <button onclick="showScreen('rules-screen')" class="btn-secondary">Reglas</button>
</div> </div>
<p class="welcome-credits">Creado por Darío Sevilla</p> <p class="welcome-credits">Creado por Darío Sevilla</p>
</div> </div>
</div> </div>
<!-- Rules screen --> <!-- Rules screen 1 -->
<div id="rules-screen" class="screen"> <div id="rules-screen" class="screen">
<h1>📖 Reglas del Juego</h1> <h1>Reglas del Juego</h1>
<div class="rules-content">
<div class="rule-section"> <div class="rule-section">
<h3>🎯 Objetivo</h3> <h3>Objetivo</h3>
<p>Los <strong>civiles</strong> deben identificar a los <strong>impostores</strong> antes de que termine el tiempo.</p> <p>Los <strong>civiles</strong> deben identificar a los <strong>impostores</strong> antes de que termine el tiempo.</p>
</div> </div>
<div class="rule-section"> <div class="rule-section">
<h3>🎲 Preparación</h3> <h3>Preparación</h3>
<p>1. Cada jugador recibe una palabra secreta</p> <p>1. Cada jugador recibe una palabra secreta</p>
<p>2. Los civiles reciben la misma palabra</p> <p>2. Los civiles reciben la misma palabra</p>
<p>3. Los impostores reciben una palabra diferente pero relacionada</p> <p>3. Los impostores reciben una palabra diferente</p>
</div>
<button onclick="showScreen('rules-screen-2')">Siguiente →</button>
<button class="ghost" onclick="showScreen('welcome-screen')">← Volver</button>
</div> </div>
<!-- Rules screen 2 -->
<div id="rules-screen-2" class="screen">
<h1>Reglas del Juego</h1>
<div class="rule-section"> <div class="rule-section">
<h3>🗣️ Partida</h3> <h3>Partida</h3>
<p>1. Por turnos, cada jugador da un sinónimo o descripción de su palabra</p> <p>1. Por turnos, da un sinónimo de tu palabra</p>
<p>2. Intenta ser específico pero no revelar tu palabra exacta</p> <p>2. específico pero no reveles tu palabra</p>
<p>3. Los impostores deben intentar pasar desapercibidos</p> <p>3. Los impostores deben pasar desapercibidos</p>
</div> </div>
<div class="rule-section"> <div class="rule-section">
<h3>🗳️ Votación</h3> <h3>Votación</h3>
<p>1. Tras el tiempo de juego y deliberación, vota en secreto</p> <p>1. Tras deliberar, vota en secreto</p>
<p>2. Los más votados son eliminados</p> <p>2. Los más votados son eliminados</p>
<p>3. Si todos los impostores son eliminados, ganan los civiles</p> <p>3. Civiles ganan si eliminan a todos los impostores</p>
<p>4. Si queda algún impostor, ellos ganan</p>
</div> </div>
</div> <button onclick="showScreen('welcome-screen')">Entendido ✓</button>
<button onclick="showScreen('welcome-screen')">Volver</button> <button class="ghost" onclick="showScreen('rules-screen')">Atrás</button>
</div> </div>
<!-- Setup screen --> <!-- Setup screen -->
<div id="setup-screen" class="screen"> <div id="setup-screen" class="screen">
<h1>⚙️ Configuración</h1> <h1>Configuración</h1>
<div class="form-group compact"> <div class="form-group compact">
<label for="num-players">Jugadores:</label> <label for="num-players">Jugadores:</label>
<input type="number" id="num-players" min="3" max="10" value="6"> <input type="number" id="num-players" min="3" max="10" value="6">
@@ -91,27 +104,32 @@
<label for="deliberation-time">Deliberación (seg):</label> <label for="deliberation-time">Deliberación (seg):</label>
<input type="number" id="deliberation-time" min="30" max="300" step="10" value="170"> <input type="number" id="deliberation-time" min="30" max="300" step="10" value="170">
</div> </div>
<div class="form-group"> <button onclick="goToPools()">Siguiente</button>
<label>Pools (toca para seleccionar):</label> <button class="ghost" onclick="showScreen('welcome-screen')">← Volver</button>
</div>
<!-- Pools selection screen -->
<div id="pools-screen" class="screen">
<h1>Selección de Pools</h1>
<p class="info-text">Toca para seleccionar las categorías de palabras que quieres usar en la partida.</p>
<div class="pool-buttons-wrapper"> <div class="pool-buttons-wrapper">
<div id="pool-buttons" class="pool-buttons"></div> <div id="pool-buttons" class="pool-buttons"></div>
</div> </div>
</div>
<button onclick="goToNames()">Siguiente</button> <button onclick="goToNames()">Siguiente</button>
<button class="ghost" onclick="showScreen('welcome-screen')">← Volver</button> <button class="ghost" onclick="showScreen('setup-screen')">← Volver</button>
</div> </div>
<!-- Player names screen --> <!-- Player names screen -->
<div id="names-screen" class="screen"> <div id="names-screen" class="screen">
<h1>👥 Nombres de jugadores</h1> <h1>Nombres de jugadores</h1>
<div class="player-names-list" id="player-names-list"></div> <div class="player-names-list" id="player-names-list"></div>
<button onclick="startGame()">Comenzar partida</button> <button onclick="startGame()">Comenzar partida</button>
<button class="ghost" onclick="showScreen('setup-screen')">← Volver</button> <button class="ghost" onclick="showScreen('pools-screen')">← Volver</button>
</div> </div>
<!-- Pre-reveal screen --> <!-- Pre-reveal screen -->
<div id="pre-reveal-screen" class="screen"> <div id="pre-reveal-screen" class="screen">
<h1>🎲 Listo para revelar</h1> <h1>Listo para revelar</h1>
<div class="info-text" id="config-summary"></div> <div class="info-text" id="config-summary"></div>
<p class="info-text">Cada jugador debe ver su rol en secreto. Desliza la cortina hacia arriba para revelar.</p> <p class="info-text">Cada jugador debe ver su rol en secreto. Desliza la cortina hacia arriba para revelar.</p>
<button onclick="showScreen('reveal-screen'); loadCurrentReveal();">Empezar revelación</button> <button onclick="showScreen('reveal-screen'); loadCurrentReveal();">Empezar revelación</button>
@@ -120,11 +138,11 @@
<!-- Revelation screen --> <!-- Revelation screen -->
<div id="reveal-screen" class="screen"> <div id="reveal-screen" class="screen">
<h1>🔍 Revelación</h1> <h1>Revelación</h1>
<p class="info-text">Turno de: <strong><span id="current-player-name">Jugador 1</span></strong><br><small>Los demás, no miréis. Mantén levantada la cortina para ver tu rol.</small></p> <p class="info-text">Turno de: <strong><span id="current-player-name">Jugador 1</span></strong><br><small>Los demás, no miréis. Mantén levantada la cortina para ver tu rol.</small></p>
<div class="curtain" id="curtain"> <div class="curtain" id="curtain">
<div class="curtain-cover" id="curtain-cover"> <div class="curtain-cover" id="curtain-cover">
<div class="curtain-icon">⬆️</div> <div class="curtain-icon"></div>
<div>LEVANTA LA CORTINA</div> <div>LEVANTA LA CORTINA</div>
</div> </div>
<div class="curtain-content"> <div class="curtain-content">
@@ -138,7 +156,7 @@
<!-- Game screen --> <!-- Game screen -->
<div id="game-screen" class="screen"> <div id="game-screen" class="screen">
<h1>🎮 Partida en curso</h1> <h1>Partida en curso</h1>
<p class="info-text">A decir sinónimos!</p> <p class="info-text">A decir sinónimos!</p>
<div class="timer" id="game-timer">3:00</div> <div class="timer" id="game-timer">3:00</div>
<button class="secondary" onclick="skipToDeliberation()">Saltar a deliberación →</button> <button class="secondary" onclick="skipToDeliberation()">Saltar a deliberación →</button>
@@ -146,7 +164,7 @@
<!-- Deliberation screen --> <!-- Deliberation screen -->
<div id="deliberation-screen" class="screen"> <div id="deliberation-screen" class="screen">
<h1>🗣️ Deliberación</h1> <h1>Deliberación</h1>
<p class="info-text">Últimos argumentos antes de votar.</p> <p class="info-text">Últimos argumentos antes de votar.</p>
<div class="timer" id="deliberation-timer">1:00</div> <div class="timer" id="deliberation-timer">1:00</div>
<button class="secondary" onclick="skipToVoting()">Ir a votación →</button> <button class="secondary" onclick="skipToVoting()">Ir a votación →</button>
@@ -154,7 +172,7 @@
<!-- Voting screen --> <!-- Voting screen -->
<div id="voting-screen" class="screen"> <div id="voting-screen" class="screen">
<h1>🗳️ Votación secreta</h1> <h1>Votación secreta</h1>
<p class="info-text">Pasa el móvil a <strong id="voter-name">Jugador</strong>. Elige <span id="votes-needed">1</span> sospechoso(s).</p> <p class="info-text">Pasa el móvil a <strong id="voter-name">Jugador</strong>. Elige <span id="votes-needed">1</span> sospechoso(s).</p>
<div class="player-list" id="vote-list"></div> <div class="player-list" id="vote-list"></div>
<button id="confirm-vote-btn" disabled onclick="confirmCurrentVote()">Confirmar voto</button> <button id="confirm-vote-btn" disabled onclick="confirmCurrentVote()">Confirmar voto</button>
@@ -162,7 +180,7 @@
<!-- Results screen --> <!-- Results screen -->
<div id="results-screen" class="screen"> <div id="results-screen" class="screen">
<h1>🏆 Resultados</h1> <h1>Resultados</h1>
<div class="results" id="results-content"></div> <div class="results" id="results-content"></div>
<button onclick="newMatch()">Nueva partida</button> <button onclick="newMatch()">Nueva partida</button>
</div> </div>

351
script.js
View File

@@ -5,6 +5,7 @@ const POOLS_CACHE_KEY = 'impostorWordPoolsV1';
const POOLS_MANIFEST_URL = 'word-pools/manifest.json'; const POOLS_MANIFEST_URL = 'word-pools/manifest.json';
const THEME_STORAGE_KEY = 'impostorGameTheme'; const THEME_STORAGE_KEY = 'impostorGameTheme';
const LANGUAGE_STORAGE_KEY = 'impostorGameLanguage'; const LANGUAGE_STORAGE_KEY = 'impostorGameLanguage';
const SCREEN_LOCK_STORAGE_KEY = 'impostorGameScreenLock';
// ---------- Internationalization system ---------- // ---------- Internationalization system ----------
const TRANSLATIONS = { const TRANSLATIONS = {
@@ -74,7 +75,9 @@ const TRANSLATIONS = {
impostorsMustBeLess: 'Impostores debe ser menor que jugadores', impostorsMustBeLess: 'Impostores debe ser menor que jugadores',
animalsNature: 'Animales y Naturaleza', animalsNature: 'Animales y Naturaleza',
everydayObjects: 'Objetos Cotidianos', everydayObjects: 'Objetos Cotidianos',
exitGame: 'Salir de la partida' 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: { en: {
gameTitle: 'The Impostor Game', gameTitle: 'The Impostor Game',
@@ -142,7 +145,9 @@ const TRANSLATIONS = {
impostorsMustBeLess: 'Impostors must be less than players', impostorsMustBeLess: 'Impostors must be less than players',
animalsNature: 'Animals and Nature', animalsNature: 'Animals and Nature',
everydayObjects: 'Everyday Objects', everydayObjects: 'Everyday Objects',
exitGame: 'Exit Game' exitGame: 'Exit Game',
poolsSelection: 'Pool Selection',
poolsSelectionText: 'Tap to select the word categories you want to use in the game.'
} }
}; };
@@ -206,42 +211,42 @@ async function updateUI() {
function updateStaticTexts() { function updateStaticTexts() {
// Welcome screen // Welcome screen
const welcomeTitle = document.querySelector('.welcome-title'); const welcomeTitle = document.querySelector('.welcome-title');
if (welcomeTitle) welcomeTitle.innerHTML = `🎭 ${t('gameTitle')}`; if (welcomeTitle) welcomeTitle.textContent = t('gameTitle');
const welcomeSubtitle = document.querySelector('.welcome-subtitle'); const welcomeSubtitle = document.querySelector('.welcome-subtitle');
if (welcomeSubtitle) welcomeSubtitle.textContent = t('gameSubtitle'); if (welcomeSubtitle) welcomeSubtitle.textContent = t('gameSubtitle');
const playBtn = document.querySelector('.btn-primary'); const playBtn = document.querySelector('.btn-primary');
if (playBtn) playBtn.innerHTML = `▶️ ${t('play')}`; if (playBtn) playBtn.textContent = t('play');
const rulesBtn = document.querySelector('.btn-secondary'); const rulesBtn = document.querySelector('.btn-secondary');
if (rulesBtn) rulesBtn.innerHTML = `📖 ${t('rules')}`; if (rulesBtn) rulesBtn.textContent = t('rules');
const credits = document.querySelector('.welcome-credits'); const credits = document.querySelector('.welcome-credits');
if (credits) credits.textContent = t('createdBy'); if (credits) credits.textContent = t('createdBy');
// Rules screen // Rules screen
const rulesTitle = document.querySelector('#rules-screen h1'); const rulesTitle = document.querySelector('#rules-screen h1');
if (rulesTitle) rulesTitle.innerHTML = `📖 ${t('rulesTitle')}`; if (rulesTitle) rulesTitle.textContent = t('rulesTitle');
const ruleSections = document.querySelectorAll('.rule-section'); const ruleSections = document.querySelectorAll('.rule-section');
if (ruleSections.length >= 4) { if (ruleSections.length >= 4) {
ruleSections[0].querySelector('h3').innerHTML = `🎯 ${t('objective')}`; ruleSections[0].querySelector('h3').textContent = t('objective');
ruleSections[0].querySelector('p').innerHTML = t('objectiveText'); ruleSections[0].querySelector('p').innerHTML = t('objectiveText');
ruleSections[1].querySelector('h3').innerHTML = `🎲 ${t('preparation')}`; ruleSections[1].querySelector('h3').textContent = t('preparation');
const prepSteps = t('preparationSteps'); const prepSteps = t('preparationSteps');
ruleSections[1].querySelectorAll('p').forEach((p, i) => { ruleSections[1].querySelectorAll('p').forEach((p, i) => {
if (prepSteps[i]) p.textContent = `${i + 1}. ${prepSteps[i]}`; if (prepSteps[i]) p.textContent = `${i + 1}. ${prepSteps[i]}`;
}); });
ruleSections[2].querySelector('h3').innerHTML = `🗣️ ${t('gameplay')}`; ruleSections[2].querySelector('h3').textContent = t('gameplay');
const gameSteps = t('gameplaySteps'); const gameSteps = t('gameplaySteps');
ruleSections[2].querySelectorAll('p').forEach((p, i) => { ruleSections[2].querySelectorAll('p').forEach((p, i) => {
if (gameSteps[i]) p.textContent = `${i + 1}. ${gameSteps[i]}`; if (gameSteps[i]) p.textContent = `${i + 1}. ${gameSteps[i]}`;
}); });
ruleSections[3].querySelector('h3').innerHTML = `🗳️ ${t('voting')}`; ruleSections[3].querySelector('h3').textContent = t('voting');
const voteSteps = t('votingSteps'); const voteSteps = t('votingSteps');
ruleSections[3].querySelectorAll('p').forEach((p, i) => { ruleSections[3].querySelectorAll('p').forEach((p, i) => {
if (voteSteps[i]) p.textContent = `${i + 1}. ${voteSteps[i]}`; if (voteSteps[i]) p.textContent = `${i + 1}. ${voteSteps[i]}`;
@@ -250,7 +255,7 @@ function updateStaticTexts() {
// Setup screen // Setup screen
const setupTitle = document.querySelector('#setup-screen h1'); const setupTitle = document.querySelector('#setup-screen h1');
if (setupTitle) setupTitle.innerHTML = `⚙️ ${t('configuration')}`; if (setupTitle) setupTitle.textContent = t('configuration');
const labels = { const labels = {
'num-players': t('players'), 'num-players': t('players'),
@@ -264,45 +269,49 @@ function updateStaticTexts() {
if (label) label.textContent = text + ':'; if (label) label.textContent = text + ':';
}); });
const poolsLabel = document.querySelector('#setup-screen .form-group:last-of-type label'); // Pools screen
if (poolsLabel) poolsLabel.textContent = t('pools') + ':'; 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 // Names screen
const namesTitle = document.querySelector('#names-screen h1'); const namesTitle = document.querySelector('#names-screen h1');
if (namesTitle) namesTitle.innerHTML = `👥 ${t('playerNames')}`; if (namesTitle) namesTitle.textContent = t('playerNames');
// Pre-reveal screen // Pre-reveal screen
const preRevealTitle = document.querySelector('#pre-reveal-screen h1'); const preRevealTitle = document.querySelector('#pre-reveal-screen h1');
if (preRevealTitle) preRevealTitle.innerHTML = `🎲 ${t('readyToReveal')}`; if (preRevealTitle) preRevealTitle.textContent = t('readyToReveal');
const preRevealText = document.querySelector('#pre-reveal-screen .info-text:not(#config-summary)'); const preRevealText = document.querySelector('#pre-reveal-screen .info-text:not(#config-summary)');
if (preRevealText) preRevealText.textContent = t('eachPlayerSecret'); if (preRevealText) preRevealText.textContent = t('eachPlayerSecret');
// Reveal screen // Reveal screen
const revealTitle = document.querySelector('#reveal-screen h1'); const revealTitle = document.querySelector('#reveal-screen h1');
if (revealTitle) revealTitle.innerHTML = `🔍 ${t('revelation')}`; if (revealTitle) revealTitle.textContent = t('revelation');
// Game screen // Game screen
const gameTitle = document.querySelector('#game-screen h1'); const gameTitle = document.querySelector('#game-screen h1');
if (gameTitle) gameTitle.innerHTML = `🎮 ${t('gameInProgress')}`; if (gameTitle) gameTitle.textContent = t('gameInProgress');
const gameText = document.querySelector('#game-screen .info-text'); const gameText = document.querySelector('#game-screen .info-text');
if (gameText) gameText.textContent = t('giveSynonyms'); if (gameText) gameText.textContent = t('giveSynonyms');
// Deliberation screen // Deliberation screen
const delibTitle = document.querySelector('#deliberation-screen h1'); const delibTitle = document.querySelector('#deliberation-screen h1');
if (delibTitle) delibTitle.innerHTML = `🗣️ ${t('deliberation')}`; if (delibTitle) delibTitle.textContent = t('deliberation');
const delibText = document.querySelector('#deliberation-screen .info-text'); const delibText = document.querySelector('#deliberation-screen .info-text');
if (delibText) delibText.textContent = t('lastArguments'); if (delibText) delibText.textContent = t('lastArguments');
// Voting screen // Voting screen
const votingTitle = document.querySelector('#voting-screen h1'); const votingTitle = document.querySelector('#voting-screen h1');
if (votingTitle) votingTitle.innerHTML = `🗳️ ${t('secretVoting')}`; if (votingTitle) votingTitle.textContent = t('secretVoting');
// Results screen // Results screen
const resultsTitle = document.querySelector('#results-screen h1'); const resultsTitle = document.querySelector('#results-screen h1');
if (resultsTitle) resultsTitle.innerHTML = `🏆 ${t('results')}`; if (resultsTitle) resultsTitle.textContent = t('results');
// Buttons // Buttons
const backButtons = document.querySelectorAll('button.ghost'); const backButtons = document.querySelectorAll('button.ghost');
@@ -314,7 +323,8 @@ function updateStaticTexts() {
// Update all other buttons based on their onclick or content // Update all other buttons based on their onclick or content
document.querySelectorAll('button').forEach(btn => { document.querySelectorAll('button').forEach(btn => {
if (btn.getAttribute('onclick') === 'goToNames()') btn.textContent = t('next'); 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') === 'startGame()') btn.textContent = t('startGame');
else if (btn.getAttribute('onclick') === "showScreen('reveal-screen'); loadCurrentReveal();") btn.textContent = t('startReveal'); 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 === 'next-player-btn') btn.textContent = t('nextPlayer') + ' →';
@@ -561,7 +571,7 @@ function renderPoolButtons() {
} }
// ---------- Setup and player names ---------- // ---------- Setup and player names ----------
function goToNames() { function goToPools() {
let nPlayers = parseInt(document.getElementById('num-players').value) || MIN_PLAYERS; let nPlayers = parseInt(document.getElementById('num-players').value) || MIN_PLAYERS;
nPlayers = Math.min(Math.max(nPlayers, MIN_PLAYERS), MAX_PLAYERS); nPlayers = Math.min(Math.max(nPlayers, MIN_PLAYERS), MAX_PLAYERS);
const maxImpostors = Math.max(1, Math.floor(nPlayers / 2)); const maxImpostors = Math.max(1, Math.floor(nPlayers / 2));
@@ -572,7 +582,14 @@ function goToNames() {
let dTime = parseInt(document.getElementById('deliberation-time').value) || defaultDeliberation(gTime); let dTime = parseInt(document.getElementById('deliberation-time').value) || defaultDeliberation(gTime);
dTime = Math.min(Math.max(dTime, 30), Math.round(900 / 3)); dTime = Math.min(Math.max(dTime, 30), Math.round(900 / 3));
if (nImpostors >= nPlayers) { alert(t('impostorsMustBeLess')); return; } if (nImpostors >= nPlayers) { alert(t('impostorsMustBeLess')); return; }
state.numPlayers = nPlayers; state.numImpostors = nImpostors; state.gameTime = gTime; state.deliberationTime = dTime; state.numPlayers = nPlayers;
state.numImpostors = nImpostors;
state.gameTime = gTime;
state.deliberationTime = dTime;
showScreen('pools-screen');
}
function goToNames() {
buildNameInputs(); buildNameInputs();
showScreen('names-screen'); showScreen('names-screen');
} }
@@ -662,6 +679,10 @@ function renderSummary() {
// ---------- Role revelation ---------- // ---------- Role revelation ----------
function loadCurrentReveal() { function loadCurrentReveal() {
state.phase = 'reveal'; saveState(); state.phase = 'reveal'; saveState();
// Activar Wake Lock para mantener pantalla encendida durante el juego
requestWakeLock();
if (!state.revealOrder || state.revealOrder.length !== state.numPlayers) { if (!state.revealOrder || state.revealOrder.length !== state.numPlayers) {
const step = state.turnDirection === 'horario' ? 1 : -1; const step = state.turnDirection === 'horario' ? 1 : -1;
state.revealOrder = Array.from({length: state.numPlayers}, (_, k) => (state.startPlayer + step * k + state.numPlayers) % state.numPlayers); state.revealOrder = Array.from({length: state.numPlayers}, (_, k) => (state.startPlayer + step * k + state.numPlayers) % state.numPlayers);
@@ -716,13 +737,17 @@ function nextReveal() { state.currentReveal++; saveState(); loadCurrentReveal();
// Curtain system with GRAVITY - The curtain always tends to fall // Curtain system with GRAVITY - The curtain always tends to fall
// Supports both touch (mobile) and mouse (desktop) // 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 curtainState = { isRevealed: false };
let curtainDragState = {
startY: null,
isDragging: false,
currentTranslateY: 0
};
(() => { function initCurtainHandlers() {
const curtain = document.getElementById('curtain'); const curtain = document.getElementById('curtain');
const cover = document.getElementById('curtain-cover'); if (!curtain) return;
let startY = null;
let isDragging = false;
// Function to get Y position from event (touch or mouse) // Function to get Y position from event (touch or mouse)
const getY = (e) => { const getY = (e) => {
@@ -731,9 +756,9 @@ let curtainState = { isRevealed: false };
// Start function (touch and mouse) // Start function (touch and mouse)
const handleStart = (e) => { const handleStart = (e) => {
const coverEl = document.getElementById('curtain-cover'); curtainDragState.startY = getY(e);
startY = getY(e); curtainDragState.isDragging = true;
isDragging = true; curtainDragState.currentTranslateY = 0;
if (e.type === 'mousedown') { if (e.type === 'mousedown') {
e.preventDefault(); // Prevent text selection on desktop e.preventDefault(); // Prevent text selection on desktop
} }
@@ -741,21 +766,22 @@ let curtainState = { isRevealed: false };
// Move function (touch and mouse) // Move function (touch and mouse)
const handleMove = (e) => { const handleMove = (e) => {
if (startY === null || !isDragging) return; if (curtainDragState.startY === null || !curtainDragState.isDragging) return;
const currentY = getY(e); const currentY = getY(e);
const dy = currentY - startY; const dy = currentY - curtainDragState.startY;
const coverEl = document.getElementById('curtain-cover'); const coverEl = document.getElementById('curtain-cover');
if (!coverEl) return;
// Calculate displacement: negative = up, positive = down // Calculate displacement: negative = up, positive = down
// Limit upward movement (not beyond curtain height) // Allow going further up than the curtain height (user can keep dragging up)
// and don't allow going below initial position (0) // but don't allow going below initial position (0)
const translateY = Math.max(Math.min(dy, 0), -cover.offsetHeight); curtainDragState.currentTranslateY = Math.min(dy, 0);
coverEl.style.transform = `translateY(${translateY}px)`; coverEl.style.transform = `translateY(${curtainDragState.currentTranslateY}px)`;
coverEl.style.transition = 'none'; coverEl.style.transition = 'none';
// If lifted enough, show content // If lifted enough, show content
if (translateY < -80 && !curtainState.isRevealed) { if (curtainDragState.currentTranslateY < -80 && !curtainState.isRevealed) {
curtainState.isRevealed = true; curtainState.isRevealed = true;
const idx = state.revealOrder[state.currentReveal]; const idx = state.revealOrder[state.currentReveal];
const role = state.roles[idx]; const role = state.roles[idx];
@@ -766,15 +792,14 @@ let curtainState = { isRevealed: false };
document.getElementById('word-text').textContent = word; document.getElementById('word-text').textContent = word;
} }
if (e.type === 'mousemove') { e.preventDefault(); // Prevent selection
e.preventDefault(); // Prevent selection on desktop
}
}; };
// End function (touch and mouse) // End function (touch and mouse)
const handleEnd = (e) => { const handleEnd = (e) => {
if (!isDragging || startY === null) return; if (!curtainDragState.isDragging || curtainDragState.startY === null) return;
const coverEl = document.getElementById('curtain-cover'); const coverEl = document.getElementById('curtain-cover');
if (!coverEl) return;
// ALWAYS bring the curtain down when released (GRAVITY) // ALWAYS bring the curtain down when released (GRAVITY)
coverEl.style.transition = 'transform 0.4s ease'; coverEl.style.transition = 'transform 0.4s ease';
@@ -791,32 +816,126 @@ let curtainState = { isRevealed: false };
}, 400); }, 400);
} }
startY = null; curtainDragState.startY = null;
isDragging = false; curtainDragState.isDragging = false;
curtainDragState.currentTranslateY = 0;
}; };
// Touch events (mobile) // Touch events (mobile)
curtain.addEventListener('touchstart', handleStart, {passive:true}); curtain.addEventListener('touchstart', handleStart, {passive: false});
curtain.addEventListener('touchmove', handleMove, {passive:true}); curtain.addEventListener('touchmove', handleMove, {passive: false});
curtain.addEventListener('touchend', handleEnd, {passive:true}); curtain.addEventListener('touchend', handleEnd, {passive: true});
curtain.addEventListener('touchcancel', handleEnd, {passive:true}); curtain.addEventListener('touchcancel', handleEnd, {passive: true});
// Mouse events (desktop) // Mouse events (desktop) - start on curtain only
curtain.addEventListener('mousedown', handleStart); curtain.addEventListener('mousedown', handleStart);
curtain.addEventListener('mousemove', handleMove);
curtain.addEventListener('mouseup', handleEnd); // Mouse move and up events on WINDOW so we can track even when cursor leaves everything
curtain.addEventListener('mouseleave', (e) => { window.addEventListener('mousemove', handleMove);
// If mouse leaves area while dragging, release (gravity) window.addEventListener('mouseup', handleEnd);
if (isDragging) { }
handleEnd(e);
} // 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 ---------- // ---------- Timers ----------
let timerInterval = null; let timerInterval = null;
function startPhaseTimer(phase, seconds, elementId, onEnd) { async function startPhaseTimer(phase, seconds, elementId, onEnd) {
if (timerInterval) clearInterval(timerInterval); if (timerInterval) clearInterval(timerInterval);
// Request wake lock to keep screen on during timer
await requestWakeLock();
const now = Date.now(); const now = Date.now();
state.timerPhase = phase; state.timerPhase = phase;
state.timerEndAt = now + seconds*1000; state.timerEndAt = now + seconds*1000;
@@ -825,7 +944,12 @@ function startPhaseTimer(phase, seconds, elementId, onEnd) {
const tick = () => { const tick = () => {
const remaining = Math.max(0, Math.round((state.timerEndAt - Date.now())/1000)); const remaining = Math.max(0, Math.round((state.timerEndAt - Date.now())/1000));
updateTimerDisplay(el, remaining); updateTimerDisplay(el, remaining);
if (remaining <= 0) { clearInterval(timerInterval); playBeep(); onEnd(); } if (remaining <= 0) {
clearInterval(timerInterval);
releaseWakeLock(); // Release wake lock when timer ends
playBeep();
onEnd();
}
}; };
tick(); tick();
timerInterval = setInterval(tick, 1000); timerInterval = setInterval(tick, 1000);
@@ -847,17 +971,48 @@ function updateTimerDisplay(el, remaining) {
} }
function playBeep() { function playBeep() {
// Play alarm sound - 3 ascending beeps pattern repeated twice
const ctx = new (window.AudioContext || window.webkitAudioContext)(); const ctx = new (window.AudioContext || window.webkitAudioContext)();
const osc = ctx.createOscillator(); const gain = ctx.createGain(); const now = ctx.currentTime;
osc.connect(gain); gain.connect(ctx.destination); osc.frequency.value = 820; osc.type = 'sine';
gain.gain.setValueAtTime(0.3, ctx.currentTime); gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.45); // Frequencies for alarm pattern (ascending)
osc.start(); osc.stop(ctx.currentTime + 0.45); 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 ---------- // ---------- Game phases ----------
function startGamePhase() { state.phase = 'game'; saveState(); showScreen('game-screen'); startPhaseTimer('game', state.gameTime, 'game-timer', startDeliberationPhase); } 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 startDeliberationPhase() { state.phase = 'deliberation'; saveState(); showScreen('deliberation-screen'); startPhaseTimer('deliberation', state.deliberationTime, 'deliberation-timer', startVotingPhase); }
function startVotingPhase(candidates = null, isTiebreak = false) { function startVotingPhase(candidates = null, isTiebreak = false) {
releaseWakeLock(); // Release wake lock when voting starts (no timer)
state.phase = 'voting'; state.phase = 'voting';
state.votingPlayer = 0; state.votingPlayer = 0;
state.votes = {}; state.votes = {};
@@ -868,8 +1023,8 @@ function startVotingPhase(candidates = null, isTiebreak = false) {
renderVoting(); renderVoting();
showScreen('voting-screen'); showScreen('voting-screen');
} }
function skipToDeliberation() { if (timerInterval) clearInterval(timerInterval); startDeliberationPhase(); } function skipToDeliberation() { if (timerInterval) clearInterval(timerInterval); releaseWakeLock(); startDeliberationPhase(); }
function skipToVoting() { if (timerInterval) clearInterval(timerInterval); startVotingPhase(); } function skipToVoting() { if (timerInterval) clearInterval(timerInterval); releaseWakeLock(); startVotingPhase(); }
function startTiebreakDeliberation(candidates) { function startTiebreakDeliberation(candidates) {
state.phase = 'deliberation'; state.phase = 'deliberation';
state.tiebreakCandidates = candidates; state.tiebreakCandidates = candidates;
@@ -896,16 +1051,22 @@ function renderVoting() {
pool.forEach(i => { pool.forEach(i => {
const item = document.createElement('div'); const item = document.createElement('div');
item.className = 'player-item'; 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]; item.textContent = state.playerNames[i];
if (state.votes[i]) item.innerHTML += `<span class="vote-count">${t('votes')}: ${state.votes[i]}</span>`; if (state.votes[i]) item.innerHTML += `<span class="vote-count">${t('votes')}: ${state.votes[i]}</span>`;
if (state.selections.includes(i)) item.classList.add('selected'); if (state.selections.includes(i)) item.classList.add('selected');
if (i === state.votingPlayer) {
item.classList.add('disabled'); if (i !== state.votingPlayer) {
item.style.opacity = '0.5';
item.style.pointerEvents = 'none';
} else {
item.onclick = () => toggleSelection(i, item); item.onclick = () => toggleSelection(i, item);
} }
list.appendChild(item); list.appendChild(item);
}); });
updateConfirmButton(); updateConfirmButton();
@@ -973,6 +1134,10 @@ function handleVoteOutcome() {
// ---------- Results ---------- // ---------- Results ----------
function showResults(isTiebreak = false) { function showResults(isTiebreak = false) {
state.phase = 'results'; saveState(); state.phase = 'results'; saveState();
// Liberar Wake Lock cuando termina la partida
releaseWakeLock();
const executed = state.executed || []; const executed = state.executed || [];
let impostorsAlive = 0; let impostorsAlive = 0;
state.roles.forEach((r,i) => { if (r === 'IMPOSTOR' && !executed.includes(i)) impostorsAlive++; }); state.roles.forEach((r,i) => { if (r === 'IMPOSTOR' && !executed.includes(i)) impostorsAlive++; });
@@ -1005,6 +1170,8 @@ function showScreen(id) {
function newMatch() { function newMatch() {
clearState(); 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:[] }; state = { ...state, phase:'welcome', timerEndAt:null, timerPhase:null, votingPool:null, isTiebreak:false, tiebreakCandidates:[] };
saveState(); saveState();
showScreen('welcome-screen'); showScreen('welcome-screen');
@@ -1023,14 +1190,20 @@ function confirmExitGame() {
function updateExitButtonVisibility() { function updateExitButtonVisibility() {
const exitBtn = document.getElementById('exit-game'); const exitBtn = document.getElementById('exit-game');
const langBtn = document.getElementById('language-toggle'); const langBtn = document.getElementById('language-toggle');
const screenLockBtn = document.getElementById('screen-lock-toggle');
// Show exit button and hide language toggle in all phases except welcome and setup // Show exit button and hide language/screen-lock toggles in all phases except welcome and setup
if (state.phase !== 'welcome' && state.phase !== 'setup') { if (state.phase !== 'welcome' && state.phase !== 'setup') {
exitBtn.classList.add('visible'); exitBtn.classList.add('visible');
if (langBtn) langBtn.style.display = 'none'; if (langBtn) langBtn.style.display = 'none';
if (screenLockBtn) screenLockBtn.classList.remove('visible');
} else { } else {
exitBtn.classList.remove('visible'); exitBtn.classList.remove('visible');
if (langBtn) langBtn.style.display = 'inline-flex'; if (langBtn) langBtn.style.display = 'inline-flex';
// Only show screen lock button on iOS
if (screenLockBtn && isIOS()) {
screenLockBtn.classList.add('visible');
}
} }
} }
@@ -1067,6 +1240,35 @@ function toggleTheme() {
const initialTheme = loadTheme(); const initialTheme = loadTheme();
applyTheme(initialTheme); 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 // Event listener for theme and language buttons
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const themeToggle = document.getElementById('theme-toggle'); const themeToggle = document.getElementById('theme-toggle');
@@ -1079,6 +1281,12 @@ document.addEventListener('DOMContentLoaded', () => {
languageToggle.addEventListener('click', toggleLanguage); languageToggle.addEventListener('click', toggleLanguage);
} }
const screenLockToggle = document.getElementById('screen-lock-toggle');
if (screenLockToggle) {
screenLockToggle.addEventListener('click', toggleScreenLock);
updateScreenLockButton();
}
// Initialize language // Initialize language
currentLanguage = loadLanguage(); currentLanguage = loadLanguage();
setLanguage(currentLanguage); setLanguage(currentLanguage);
@@ -1133,4 +1341,7 @@ document.addEventListener('DOMContentLoaded', () => {
// Initialize exit button visibility // Initialize exit button visibility
updateExitButtonVisibility(); updateExitButtonVisibility();
// Initialize screen lock button for iOS
initScreenLockButton();
})(); })();

2015
styles.css

File diff suppressed because it is too large Load Diff