diff --git a/www/index.html b/www/index.html index ba715ca..901a44e 100644 --- a/www/index.html +++ b/www/index.html @@ -123,6 +123,12 @@
+

Creado por DarΓ­o Sevilla

@@ -268,6 +274,16 @@ + +
+

EMPATE

+
+

JUGADOR/ES ELIMINADOS:

+

+
+ +
+

Resultados

diff --git a/www/script.js b/www/script.js index 3889ae6..03910fc 100644 --- a/www/script.js +++ b/www/script.js @@ -6,15 +6,20 @@ const POOLS_MANIFEST_URL = 'word-pools/manifest.json'; const THEME_STORAGE_KEY = 'impostorGameTheme'; const LANGUAGE_STORAGE_KEY = 'impostorGameLanguage'; const SCREEN_LOCK_STORAGE_KEY = 'impostorGameScreenLock'; +const APP_VERSION = 1; // Simple incremental version number +const LATEST_VERSION_URL = 'https://git.dariosevilla.es/api/v1/repos/dasemu/web-imposter-game/releases/latest'; +const APK_DOWNLOAD_URL = 'https://git.dariosevilla.es/dasemu/web-imposter-game/releases/download/latest/impostor-game.apk'; // Detect if running as a native app (Capacitor/Cordova) +let isNativeApp = false; (function detectNativeApp() { const isCapacitor = window.Capacitor !== undefined; const isCordova = window.cordova !== undefined; - const isNativeApp = isCapacitor || isCordova; + isNativeApp = isCapacitor || isCordova; if (isNativeApp) { document.documentElement.classList.add('native-app'); + checkAppVersion(); } })(); @@ -96,7 +101,15 @@ const TRANSLATIONS = { raisedHandVotingDesc: 'VotaciΓ³n grupal ΓΊnica', groupVotingTitle: 'VotaciΓ³n a mano alzada', groupVotingInstruction: 'Decidid en grupo a quiΓ©n eliminar. Seleccionad', - groupVotingSuspects: 'sospechoso(s)' + groupVotingSuspects: 'sospechoso(s)', + tie: 'EMPATE', + eliminatedPlayers: 'JUGADOR/ES ELIMINADOS:', + goToTiebreaker: 'IR A DESEMPATE', + downloadApp: 'Descargar App Android', + updateAvailable: 'ActualizaciΓ³n disponible', + updateMessage: 'Hay una nueva versiΓ³n de la app disponible. ΒΏQuieres actualizar ahora?', + update: 'Actualizar', + later: 'MΓ‘s tarde' }, en: { gameTitle: 'The Impostor Game', @@ -174,7 +187,15 @@ const TRANSLATIONS = { raisedHandVotingDesc: 'Single group vote', groupVotingTitle: 'Raised hand voting', groupVotingInstruction: 'Decide as a group who to eliminate. Select', - groupVotingSuspects: 'suspect(s)' + groupVotingSuspects: 'suspect(s)', + tie: 'TIE', + eliminatedPlayers: 'ELIMINATED PLAYER(S):', + goToTiebreaker: 'GO TO TIEBREAKER', + downloadApp: 'Download Android App', + updateAvailable: 'Update available', + updateMessage: 'A new version of the app is available. Do you want to update now?', + update: 'Update', + later: 'Later' } }; @@ -249,6 +270,8 @@ async function updateUI() { renderVoting(); } else if (state.phase === 'results') { showResults(); + } else if (state.phase === 'tie') { + showTieScreen(); } } @@ -347,6 +370,10 @@ function updateStaticTexts() { const votingTitle = document.querySelector('#voting-screen h1'); if (votingTitle) votingTitle.textContent = t('secretVoting'); + // Tie screen + const tieTitle = document.querySelector('#tie-screen h1'); + if (tieTitle) tieTitle.textContent = t('tie'); + // Results screen const resultsTitle = document.querySelector('#results-screen h1'); if (resultsTitle) resultsTitle.textContent = t('results'); @@ -371,11 +398,18 @@ function updateStaticTexts() { else if (btn.getAttribute('onclick') === 'skipToVoting()') btn.textContent = t('goToVoting') + ' β†’'; else if (btn.id === 'confirm-vote-btn') btn.textContent = t('confirmVote'); else if (btn.getAttribute('onclick') === 'newMatch()') btn.textContent = t('newMatch'); + else if (btn.getAttribute('onclick') === 'proceedToTiebreaker()') btn.textContent = t('goToTiebreaker'); }); // Exit game button const exitText = document.querySelector('.exit-text'); if (exitText) exitText.textContent = t('exitGame'); + + // Download app button + const downloadBtn = document.getElementById('download-app-btn'); + if (downloadBtn && !isNativeApp) { + downloadBtn.textContent = `πŸ“± ${t('downloadApp')}`; + } } // Embedded pools with impostor words [civilian_word, impostor_word] @@ -1110,6 +1144,18 @@ function renderVoting() { } // Individual voting mode + const eliminated = state.executed || []; + + // Skip eliminated players in tiebreaker + while (state.isTiebreak && eliminated.includes(state.votingPlayer) && state.votingPlayer < state.numPlayers) { + state.votingPlayer++; + } + + if (state.votingPlayer >= state.numPlayers) { + handleVoteOutcome(); + return; + } + const voter = state.playerNames[state.votingPlayer]; document.getElementById('voter-name').textContent = voter; document.getElementById('votes-needed').textContent = state.numImpostors; @@ -1225,7 +1271,18 @@ function confirmCurrentVote() { state.votingPlayer++; state.selections = []; saveState(); - if (state.votingPlayer >= state.numPlayers) { handleVoteOutcome(); return; } + + const eliminated = state.executed || []; + + // Skip eliminated players in tiebreaker + while (state.isTiebreak && eliminated.includes(state.votingPlayer) && state.votingPlayer < state.numPlayers) { + state.votingPlayer++; + } + + if (state.votingPlayer >= state.numPlayers) { + handleVoteOutcome(); + return; + } renderVoting(); } @@ -1254,7 +1311,10 @@ function handleVoteOutcome() { showResults(true); return; } - startTiebreakDeliberation(group); + // Show tie screen with eliminated players + state.executed = executed; + state.tiebreakCandidates = group; + showTieScreen(); return; } } @@ -1291,6 +1351,28 @@ function showResults(isTiebreak = false) { showScreen('results-screen'); } +// ---------- Tie screen ---------- +function showTieScreen() { + state.phase = 'tie'; + saveState(); + + const eliminated = state.executed || []; + const tieScreen = document.getElementById('tie-screen'); + const eliminatedLabel = document.getElementById('tie-eliminated-label'); + const eliminatedPlayers = document.getElementById('tie-eliminated-players'); + + eliminatedLabel.textContent = t('eliminatedPlayers'); + eliminatedPlayers.textContent = eliminated.length > 0 + ? eliminated.map(i => state.playerNames[i]).join(', ') + : t('nobody'); + + showScreen('tie-screen'); +} + +function proceedToTiebreaker() { + startTiebreakDeliberation(state.tiebreakCandidates); +} + // ---------- Utilities ---------- function showScreen(id) { document.querySelectorAll('.screen').forEach(s => s.classList.remove('active')); @@ -1402,6 +1484,65 @@ function toggleScreenLock() { } // Event listener for theme and language buttons +// ---------- App version checking ---------- +async function checkAppVersion() { + if (!isNativeApp) return; + + try { + const response = await fetch(LATEST_VERSION_URL); + if (!response.ok) return; + + const releaseData = await response.json(); + const latestVersion = parseInt(releaseData.name) || 0; + + if (latestVersion > APP_VERSION) { + showUpdateNotification(); + } + } catch (error) { + console.error('Error checking app version:', error); + } +} + +function showUpdateNotification() { + const message = t('updateMessage'); + const updateBtn = t('update'); + const laterBtn = t('later'); + + // Create custom notification overlay + const overlay = document.createElement('div'); + overlay.className = 'update-notification-overlay'; + overlay.innerHTML = ` +
+

${t('updateAvailable')}

+

${message}

+
+ + +
+
+ `; + document.body.appendChild(overlay); +} + +function downloadUpdate() { + window.location.href = APK_DOWNLOAD_URL; + dismissUpdate(); +} + +function dismissUpdate() { + const overlay = document.querySelector('.update-notification-overlay'); + if (overlay) overlay.remove(); +} + +// Show download button only on web (not native app) +function initDownloadButton() { + const downloadBtn = document.getElementById('download-app-btn'); + if (downloadBtn && !isNativeApp) { + downloadBtn.style.display = 'inline-block'; + downloadBtn.textContent = `πŸ“± ${t('downloadApp')}`; + } +} + document.addEventListener('DOMContentLoaded', () => { const themeToggle = document.getElementById('theme-toggle'); if (themeToggle) { @@ -1423,6 +1564,9 @@ document.addEventListener('DOMContentLoaded', () => { currentLanguage = loadLanguage(); setLanguage(currentLanguage); + // Initialize download button + initDownloadButton(); + // Detect system theme changes window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => { // Only apply automatically if user hasn't manually selected a theme diff --git a/www/styles.715a864d.css b/www/styles.715a864d.css index 5b0730d..6e9ea10 100644 --- a/www/styles.715a864d.css +++ b/www/styles.715a864d.css @@ -538,6 +538,146 @@ button:disabled { box-shadow: var(--shadow-harsh), 0 0 20px rgba(46, 78, 122, 0.3); } +.btn-download { + display: inline-block; + padding: 14px 22px; + font-size: 0.95em; + font-family: 'JetBrains Mono', monospace; + font-weight: 700; + letter-spacing: 1px; + text-transform: uppercase; + text-decoration: none; + color: var(--text-primary); + background: linear-gradient(135deg, var(--accent-success) 0%, #1e7a3e 100%); + border: 3px solid var(--accent-success); + border-radius: 0; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.22, 1, 0.36, 1); + box-shadow: var(--shadow-harsh), 0 0 20px rgba(46, 204, 113, 0.3); + text-align: center; +} + +.btn-download:hover { + transform: translate(-2px, -2px); + box-shadow: 6px 6px 0px var(--bg-secondary), 0 0 30px rgba(46, 204, 113, 0.5); + filter: brightness(1.15); +} + +.btn-download:active { + transform: translate(0, 0); + box-shadow: 3px 3px 0px var(--bg-secondary), 0 0 15px rgba(46, 204, 113, 0.3); +} + +/* Hide download button in native app */ +.native-app .btn-download { + display: none !important; +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + UPDATE NOTIFICATION + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.update-notification-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.85); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + animation: overlayFadeIn 0.3s ease; +} + +@keyframes overlayFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.update-notification { + background: var(--surface-card); + border: 3px solid var(--accent-warning); + border-radius: 0; + padding: 30px; + max-width: 400px; + margin: 20px; + box-shadow: var(--shadow-harsh), 0 0 40px rgba(230, 167, 60, 0.5); + animation: notificationSlideIn 0.4s cubic-bezier(0.22, 1, 0.36, 1); +} + +@keyframes notificationSlideIn { + from { + opacity: 0; + transform: translateY(-30px) scale(0.9); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.update-notification h2 { + margin: 0 0 15px 0; + font-family: 'Bebas Neue', 'Crimson Text', Georgia, serif; + font-size: 1.8em; + letter-spacing: 3px; + color: var(--accent-warning); + text-align: center; +} + +.update-notification p { + margin: 0 0 25px 0; + color: var(--text-primary); + line-height: 1.6; + text-align: center; +} + +.update-buttons { + display: flex; + gap: 12px; + justify-content: center; +} + +.btn-update, +.btn-later { + padding: 12px 24px; + font-size: 0.9em; + font-family: 'JetBrains Mono', monospace; + font-weight: 700; + letter-spacing: 1px; + text-transform: uppercase; + background: var(--surface-hover); + border: 3px solid var(--border-medium); + border-radius: 0; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.22, 1, 0.36, 1); + box-shadow: var(--shadow-md); +} + +.btn-update { + background: linear-gradient(135deg, var(--accent-success) 0%, #1e7a3e 100%); + border-color: var(--accent-success); + color: var(--text-primary); +} + +.btn-update:hover { + transform: translate(-2px, -2px); + box-shadow: 4px 4px 0px var(--bg-secondary), 0 0 20px rgba(46, 204, 113, 0.5); + filter: brightness(1.15); +} + +.btn-later:hover { + transform: translate(-2px, -2px); + box-shadow: 4px 4px 0px var(--bg-secondary); + filter: brightness(1.1); +} + /* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ WELCOME SCREEN ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ @@ -1379,6 +1519,64 @@ button:disabled { text-transform: uppercase; } +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + TIE SCREEN + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +#tie-screen h1 { + font-size: 2.5em; + color: var(--accent-warning); + text-align: center; + margin-bottom: 20px; + animation: tieFlash 1s ease-in-out infinite alternate; +} + +@keyframes tieFlash { + 0% { + text-shadow: 0 0 10px var(--accent-warning); + } + 100% { + text-shadow: 0 0 20px var(--accent-warning), 0 0 30px var(--accent-warning); + } +} + +.tie-info { + background: var(--surface-card); + border: 2px solid var(--border-medium); + border-radius: 0; + padding: 20px; + margin: 20px 0; + text-align: center; + box-shadow: var(--shadow-md); +} + +.tie-info .info-text { + font-size: 1.1em; + margin-bottom: 15px; + color: var(--text-primary); +} + +.eliminated-players { + font-size: 1.3em; + font-weight: 700; + color: var(--accent-danger); + margin-top: 10px; + letter-spacing: 1px; +} + +#tie-screen button { + margin-top: 20px; + font-size: 1.1em; + padding: 15px 30px; + background: var(--accent-warning); + color: var(--text-primary); +} + +#tie-screen button:hover { + background: var(--accent-warning); + filter: brightness(1.2); +} + /* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ POOL SELECTION ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ diff --git a/www/styles.css b/www/styles.css index 5b0730d..6e9ea10 100644 --- a/www/styles.css +++ b/www/styles.css @@ -538,6 +538,146 @@ button:disabled { box-shadow: var(--shadow-harsh), 0 0 20px rgba(46, 78, 122, 0.3); } +.btn-download { + display: inline-block; + padding: 14px 22px; + font-size: 0.95em; + font-family: 'JetBrains Mono', monospace; + font-weight: 700; + letter-spacing: 1px; + text-transform: uppercase; + text-decoration: none; + color: var(--text-primary); + background: linear-gradient(135deg, var(--accent-success) 0%, #1e7a3e 100%); + border: 3px solid var(--accent-success); + border-radius: 0; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.22, 1, 0.36, 1); + box-shadow: var(--shadow-harsh), 0 0 20px rgba(46, 204, 113, 0.3); + text-align: center; +} + +.btn-download:hover { + transform: translate(-2px, -2px); + box-shadow: 6px 6px 0px var(--bg-secondary), 0 0 30px rgba(46, 204, 113, 0.5); + filter: brightness(1.15); +} + +.btn-download:active { + transform: translate(0, 0); + box-shadow: 3px 3px 0px var(--bg-secondary), 0 0 15px rgba(46, 204, 113, 0.3); +} + +/* Hide download button in native app */ +.native-app .btn-download { + display: none !important; +} + +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + UPDATE NOTIFICATION + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +.update-notification-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.85); + display: flex; + align-items: center; + justify-content: center; + z-index: 10000; + animation: overlayFadeIn 0.3s ease; +} + +@keyframes overlayFadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.update-notification { + background: var(--surface-card); + border: 3px solid var(--accent-warning); + border-radius: 0; + padding: 30px; + max-width: 400px; + margin: 20px; + box-shadow: var(--shadow-harsh), 0 0 40px rgba(230, 167, 60, 0.5); + animation: notificationSlideIn 0.4s cubic-bezier(0.22, 1, 0.36, 1); +} + +@keyframes notificationSlideIn { + from { + opacity: 0; + transform: translateY(-30px) scale(0.9); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +.update-notification h2 { + margin: 0 0 15px 0; + font-family: 'Bebas Neue', 'Crimson Text', Georgia, serif; + font-size: 1.8em; + letter-spacing: 3px; + color: var(--accent-warning); + text-align: center; +} + +.update-notification p { + margin: 0 0 25px 0; + color: var(--text-primary); + line-height: 1.6; + text-align: center; +} + +.update-buttons { + display: flex; + gap: 12px; + justify-content: center; +} + +.btn-update, +.btn-later { + padding: 12px 24px; + font-size: 0.9em; + font-family: 'JetBrains Mono', monospace; + font-weight: 700; + letter-spacing: 1px; + text-transform: uppercase; + background: var(--surface-hover); + border: 3px solid var(--border-medium); + border-radius: 0; + cursor: pointer; + transition: all 0.2s cubic-bezier(0.22, 1, 0.36, 1); + box-shadow: var(--shadow-md); +} + +.btn-update { + background: linear-gradient(135deg, var(--accent-success) 0%, #1e7a3e 100%); + border-color: var(--accent-success); + color: var(--text-primary); +} + +.btn-update:hover { + transform: translate(-2px, -2px); + box-shadow: 4px 4px 0px var(--bg-secondary), 0 0 20px rgba(46, 204, 113, 0.5); + filter: brightness(1.15); +} + +.btn-later:hover { + transform: translate(-2px, -2px); + box-shadow: 4px 4px 0px var(--bg-secondary); + filter: brightness(1.1); +} + /* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ WELCOME SCREEN ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ @@ -1379,6 +1519,64 @@ button:disabled { text-transform: uppercase; } +/* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + TIE SCREEN + ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */ + +#tie-screen h1 { + font-size: 2.5em; + color: var(--accent-warning); + text-align: center; + margin-bottom: 20px; + animation: tieFlash 1s ease-in-out infinite alternate; +} + +@keyframes tieFlash { + 0% { + text-shadow: 0 0 10px var(--accent-warning); + } + 100% { + text-shadow: 0 0 20px var(--accent-warning), 0 0 30px var(--accent-warning); + } +} + +.tie-info { + background: var(--surface-card); + border: 2px solid var(--border-medium); + border-radius: 0; + padding: 20px; + margin: 20px 0; + text-align: center; + box-shadow: var(--shadow-md); +} + +.tie-info .info-text { + font-size: 1.1em; + margin-bottom: 15px; + color: var(--text-primary); +} + +.eliminated-players { + font-size: 1.3em; + font-weight: 700; + color: var(--accent-danger); + margin-top: 10px; + letter-spacing: 1px; +} + +#tie-screen button { + margin-top: 20px; + font-size: 1.1em; + padding: 15px 30px; + background: var(--accent-warning); + color: var(--text-primary); +} + +#tie-screen button:hover { + background: var(--accent-warning); + filter: brightness(1.2); +} + /* ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ POOL SELECTION ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ */