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:
116
index.html
116
index.html
@@ -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 class="rule-section">
|
|
||||||
<h3>🎲 Preparación</h3>
|
|
||||||
<p>1. Cada jugador recibe una palabra secreta</p>
|
|
||||||
<p>2. Los civiles reciben la misma palabra</p>
|
|
||||||
<p>3. Los impostores reciben una palabra diferente pero relacionada</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rule-section">
|
|
||||||
<h3>🗣️ Partida</h3>
|
|
||||||
<p>1. Por turnos, cada jugador da un sinónimo o descripción de su palabra</p>
|
|
||||||
<p>2. Intenta ser específico pero no revelar tu palabra exacta</p>
|
|
||||||
<p>3. Los impostores deben intentar pasar desapercibidos</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="rule-section">
|
|
||||||
<h3>🗳️ Votación</h3>
|
|
||||||
<p>1. Tras el tiempo de juego y deliberación, vota en secreto</p>
|
|
||||||
<p>2. Los más votados son eliminados</p>
|
|
||||||
<p>3. Si todos los impostores son eliminados, ganan los civiles</p>
|
|
||||||
<p>4. Si queda algún impostor, ellos ganan</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<button onclick="showScreen('welcome-screen')">← Volver</button>
|
<div class="rule-section">
|
||||||
|
<h3>Preparación</h3>
|
||||||
|
<p>1. Cada jugador recibe una palabra secreta</p>
|
||||||
|
<p>2. Los civiles reciben la misma palabra</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>
|
||||||
|
|
||||||
|
<!-- Rules screen 2 -->
|
||||||
|
<div id="rules-screen-2" class="screen">
|
||||||
|
<h1>Reglas del Juego</h1>
|
||||||
|
<div class="rule-section">
|
||||||
|
<h3>Partida</h3>
|
||||||
|
<p>1. Por turnos, da un sinónimo de tu palabra</p>
|
||||||
|
<p>2. Sé específico pero no reveles tu palabra</p>
|
||||||
|
<p>3. Los impostores deben pasar desapercibidos</p>
|
||||||
|
</div>
|
||||||
|
<div class="rule-section">
|
||||||
|
<h3>Votación</h3>
|
||||||
|
<p>1. Tras deliberar, vota en secreto</p>
|
||||||
|
<p>2. Los más votados son eliminados</p>
|
||||||
|
<p>3. Civiles ganan si eliminan a todos los impostores</p>
|
||||||
|
</div>
|
||||||
|
<button onclick="showScreen('welcome-screen')">Entendido ✓</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 class="pool-buttons-wrapper">
|
</div>
|
||||||
<div id="pool-buttons" class="pool-buttons"></div>
|
|
||||||
</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 id="pool-buttons" class="pool-buttons"></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>
|
||||||
|
|||||||
355
script.js
355
script.js
@@ -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.onclick = () => toggleSelection(i, item);
|
||||||
item.style.pointerEvents = 'none';
|
}
|
||||||
} else {
|
|
||||||
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
2015
styles.css
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user