document.addEventListener('DOMContentLoaded', () => { // DOM Elements const currentPlayerArea = document.getElementById('current-player-area'); const currentPlayerNameEl = document.getElementById('current-player-name'); const currentPlayerTimerEl = document.getElementById('current-player-timer'); const currentPlayerPhotoEl = document.getElementById('current-player-photo'); const nextPlayerArea = document.getElementById('next-player-area'); const nextPlayerNameEl = document.getElementById('next-player-name'); const nextPlayerTimerEl = document.getElementById('next-player-timer'); const nextPlayerPhotoEl = document.getElementById('next-player-photo'); const managePlayersBtn = document.getElementById('manage-players-btn'); const gameModeBtn = document.getElementById('game-mode-btn'); const resetGameBtn = document.getElementById('reset-game-btn'); const muteBtn = document.getElementById('mute-btn'); const managePlayersModal = document.getElementById('manage-players-modal'); const closeModalBtn = document.querySelector('.close-modal-btn'); const playerListEditor = document.getElementById('player-list-editor'); const addPlayerFormBtn = document.getElementById('add-player-form-btn'); // <<<< ENSURED DEFINITION const shufflePlayersBtn = document.getElementById('shuffle-players-btn'); const reversePlayersBtn = document.getElementById('reverse-players-btn'); const addEditPlayerForm = document.getElementById('add-edit-player-form'); const playerFormTitle = document.getElementById('player-form-title'); const editPlayerIdInput = document.getElementById('edit-player-id'); const playerNameInput = document.getElementById('player-name-input'); const playerTimeInput = document.getElementById('player-time-input'); // playerPhotoInput (for URL) is removed, new elements for camera: const playerPhotoCaptureInput = document.getElementById('player-photo-capture-input'); const playerPhotoPreviewEl = document.getElementById('player-photo-preview'); const removePhotoBtn = document.getElementById('remove-photo-btn'); const playerHotkeyInput = document.getElementById('player-hotkey-input'); const playerAdminInput = document.getElementById('player-admin-input'); const savePlayerBtn = document.getElementById('save-player-btn'); const cancelEditPlayerBtn = document.getElementById('cancel-edit-player-btn'); const savePlayerManagementBtn = document.getElementById('save-player-management-btn'); const appContainer = document.getElementById('app-container'); // Web Audio API let audioContext; let continuousTickIntervalId = null; let shortTickIntervalId = null; let shortTickTimeoutId = null; // Game State let players = []; let currentPlayerIndex = 0; let gameMode = 'normal'; let isMuted = false; let playerTimers = {}; let focusedPlayerIndexInAllTimersMode = 0; let currentPhotoDataUrl = null; // Temp store for new photo in form const DEFAULT_PHOTO_URL = 'assets/default-avatar.svg'; const MAX_NEGATIVE_TIME_SECONDS = -3599; const TICK_FREQUENCY_HZ = 1200; const TICK_DURATION_S = 0.05; const CONTINUOUS_TICK_INTERVAL_MS = 750; const SHORT_TICK_DURATION_MS = 3000; // --- Web Audio API Initialization --- function initAudio() { try { audioContext = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { console.error("Web Audio API is not supported in this browser", e); isMuted = true; updateMuteButton(); } } function resumeAudioContext() { if (audioContext && audioContext.state === 'suspended') { audioContext.resume().then(() => { // console.log("AudioContext resumed successfully"); // Kept for debugging if needed }).catch(e => console.error("Error resuming AudioContext:", e)); } } // --- Persistence --- function saveState() { const state = { players: players.map(p => ({ ...p, timerInstance: undefined })), currentPlayerIndex, gameMode, isMuted, focusedPlayerIndexInAllTimersMode }; localStorage.setItem('nexusTimerState', JSON.stringify(state)); } function loadState() { const savedState = localStorage.getItem('nexusTimerState'); if (savedState) { const state = JSON.parse(savedState); players = state.players.map(p => ({ ...p, currentTime: parseInt(p.currentTime, 10), initialTime: parseInt(p.initialTime, 10), isSkipped: p.isSkipped || false, })); currentPlayerIndex = state.currentPlayerIndex || 0; gameMode = state.gameMode || 'normal'; isMuted = state.isMuted || false; focusedPlayerIndexInAllTimersMode = state.focusedPlayerIndexInAllTimersMode || 0; if (players.length === 0) setupDefaultPlayers(); } else { setupDefaultPlayers(); } renderPlayerManagementList(); updateDisplay(); updateGameModeUI(); } function setupDefaultPlayers() { players = [ { id: Date.now(), name: 'Player 1', initialTime: 3600, currentTime: 3600, photo: DEFAULT_PHOTO_URL, hotkey: 'a', isAdmin: true, isSkipped: false }, { id: Date.now() + 1, name: 'Player 2', initialTime: 3600, currentTime: 3600, photo: DEFAULT_PHOTO_URL, hotkey: 'b', isAdmin: false, isSkipped: false }, ]; currentPlayerIndex = 0; } // --- Time Formatting --- function formatTime(totalSeconds) { const isNegative = totalSeconds < 0; if (isNegative) totalSeconds = -totalSeconds; const minutes = Math.floor(totalSeconds / 60); const seconds = totalSeconds % 60; const sign = isNegative ? '-' : ''; return `${sign}${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; } function parseTimeInput(timeStr) { const parts = timeStr.split(':'); if (parts.length === 2) { const minutes = parseInt(parts[0], 10); const seconds = parseInt(parts[1], 10); if (!isNaN(minutes) && !isNaN(seconds) && seconds < 60 && seconds >= 0 && minutes >= 0) { return (minutes * 60) + seconds; } } return 3600; } // --- UI Updates --- function updatePlayerDisplay(player, nameEl, timerEl, photoEl, isSmallTimer = false) { if (player) { nameEl.textContent = player.name; timerEl.textContent = formatTime(player.currentTime); timerEl.className = `timer-display ${isSmallTimer ? 'small-timer' : ''} ${player.currentTime < 0 ? 'negative' : ''}`; photoEl.src = player.photo || DEFAULT_PHOTO_URL; // player.photo can be data URL photoEl.alt = `${player.name}'s Photo`; const parentArea = nameEl.closest('.player-area'); if (parentArea) { parentArea.classList.toggle('player-skipped', !!player.isSkipped); parentArea.classList.remove('pulsating-background', 'pulsating-text', 'pulsating-negative-text'); if (playerTimers[player.id] && !player.isSkipped) { if (player.currentTime >= 0) parentArea.classList.add('pulsating-background'); else parentArea.classList.add('pulsating-negative-text'); } } } else { nameEl.textContent = '---'; timerEl.textContent = '00:00'; timerEl.className = `timer-display ${isSmallTimer ? 'small-timer' : ''}`; photoEl.src = DEFAULT_PHOTO_URL; photoEl.alt = 'Player Photo'; const parentArea = nameEl.closest('.player-area'); if (parentArea) parentArea.classList.remove('player-skipped', 'pulsating-background', 'pulsating-text', 'pulsating-negative-text'); } } function updateDisplay() { if (players.length === 0) { updatePlayerDisplay(null, currentPlayerNameEl, currentPlayerTimerEl, currentPlayerPhotoEl); updatePlayerDisplay(null, nextPlayerNameEl, nextPlayerTimerEl, nextPlayerPhotoEl, true); return; } let currentP, nextP; if (gameMode === 'allTimersRunning') { const activePlayers = players.filter(p => !p.isSkipped); if (activePlayers.length > 0) { focusedPlayerIndexInAllTimersMode = focusedPlayerIndexInAllTimersMode % activePlayers.length; currentP = activePlayers[focusedPlayerIndexInAllTimersMode]; nextP = activePlayers.length > 1 ? activePlayers[(focusedPlayerIndexInAllTimersMode + 1) % activePlayers.length] : null; } else { currentP = players[0]; nextP = players.length > 1 ? players[1] : null; } } else { currentP = players[currentPlayerIndex]; nextP = players.length > 1 ? players[(currentPlayerIndex + 1) % players.length] : null; } updatePlayerDisplay(currentP, currentPlayerNameEl, currentPlayerTimerEl, currentPlayerPhotoEl); updatePlayerDisplay(nextP, nextPlayerNameEl, nextPlayerTimerEl, nextPlayerPhotoEl, true); saveState(); } function updateGameModeUI() { if (gameMode === 'allTimersRunning') { gameModeBtn.textContent = 'Stop All Timers'; let anyTimerRunning = Object.values(playerTimers).some(id => id !== null); if (anyTimerRunning) { appContainer.classList.add('pulsating-background'); if (!isMuted) playContinuousTick(true); } else { gameModeBtn.textContent = 'Start All Timers'; appContainer.classList.remove('pulsating-background'); playContinuousTick(false); } } else { gameModeBtn.textContent = 'All Timers Mode'; appContainer.classList.remove('pulsating-background'); playContinuousTick(false); } } // --- Timer Logic --- function startTimer(playerId) { const player = players.find(p => p.id === playerId); if (!player || player.isSkipped || playerTimers[playerId]) return; if (gameMode === 'normal' && !isMuted) playShortTick(); playerTimers[playerId] = setInterval(() => { player.currentTime--; if (player.currentTime < MAX_NEGATIVE_TIME_SECONDS) { player.currentTime = MAX_NEGATIVE_TIME_SECONDS; player.isSkipped = true; pauseTimer(playerId, false); if (gameMode === 'normal' && players[currentPlayerIndex].id === playerId) passTurn(); } updateDisplay(); }, 1000); updateDisplay(); } function pauseTimer(playerId, checkAllTimersModeRevert = true) { if (playerTimers[playerId]) { clearInterval(playerTimers[playerId]); playerTimers[playerId] = null; } if (gameMode === 'normal' && players[currentPlayerIndex]?.id === playerId) stopShortTick(); updateDisplay(); if (gameMode === 'allTimersRunning' && checkAllTimersModeRevert) { const allPaused = players.every(p => p.isSkipped || !playerTimers[p.id]); if (allPaused) switchToNormalMode(); else updateGameModeUI(); } } function resetPlayerTimer(player) { player.currentTime = player.initialTime; player.isSkipped = false; if (playerTimers[player.id]) pauseTimer(player.id); } // --- Game Flow & Modes --- function passTurn() { if (players.length < 1 || gameMode !== 'normal') return; const currentP = players[currentPlayerIndex]; const currentTimerWasActive = !!playerTimers[currentP.id]; pauseTimer(currentP.id); let nextIndex = (currentPlayerIndex + 1) % players.length; let attempts = 0; while (players[nextIndex].isSkipped && attempts < players.length) { nextIndex = (nextIndex + 1) % players.length; attempts++; } if (players[nextIndex].isSkipped && attempts >= players.length) { currentPlayerIndex = nextIndex; console.log("All subsequent players are skipped. Turn passed to a skipped player."); } else { currentPlayerIndex = nextIndex; const nextP = players[currentPlayerIndex]; if (currentTimerWasActive) { startTimer(nextP.id); } } updateDisplay(); } function switchToNormalMode() { gameMode = 'normal'; players.forEach(p => pauseTimer(p.id, false)); const activePlayers = players.filter(p => !p.isSkipped); if (activePlayers.length > 0 && focusedPlayerIndexInAllTimersMode < activePlayers.length) { const focusedPlayer = activePlayers[focusedPlayerIndexInAllTimersMode]; const newCurrentIndex = players.findIndex(p => p.id === focusedPlayer.id); if (newCurrentIndex !== -1) currentPlayerIndex = newCurrentIndex; } else if (activePlayers.length > 0) { currentPlayerIndex = players.findIndex(p => p.id === activePlayers[0].id); } updateGameModeUI(); updateDisplay(); } function switchToAllTimersMode(startTimers = true) { gameMode = 'allTimersRunning'; if (startTimers) { players.forEach(p => { if (!p.isSkipped) startTimer(p.id); }); const currentActualPlayer = players[currentPlayerIndex]; const activePlayers = players.filter(p => !p.isSkipped); const focusIdx = activePlayers.findIndex(p => p.id === currentActualPlayer.id); focusedPlayerIndexInAllTimersMode = (focusIdx !== -1) ? focusIdx : 0; } updateGameModeUI(); updateDisplay(); } function changeFocusInAllTimersMode() { if (gameMode !== 'allTimersRunning' || players.length === 0) return; const activePlayers = players.filter(p => !p.isSkipped); if (activePlayers.length <= 1) return; focusedPlayerIndexInAllTimersMode = (focusedPlayerIndexInAllTimersMode + 1) % activePlayers.length; updateDisplay(); } function resetGame() { if (!confirm("Reset game? All timers will be restored to initial values.")) return; players.forEach(resetPlayerTimer); currentPlayerIndex = 0; focusedPlayerIndexInAllTimersMode = 0; if (gameMode === 'allTimersRunning') switchToNormalMode(); updateDisplay(); saveState(); } // --- Player Management --- function renderPlayerManagementList() { playerListEditor.innerHTML = ''; if (players.length === 0) { playerListEditor.innerHTML = '

No players yet. Add some!

'; } players.forEach((player, index) => { const entry = document.createElement('div'); entry.className = 'player-editor-entry'; const photoSrc = (player.photo && player.photo.startsWith('data:image')) ? player.photo : DEFAULT_PHOTO_URL; entry.innerHTML = ` P ${index + 1}. ${player.name} (${formatTime(player.initialTime)}) ${player.isAdmin ? '(Admin)' : ''} ${player.hotkey ? `[${player.hotkey}]`: ''}
${index > 0 ? `` : ''} ${index < players.length - 1 ? `` : ''}
`; playerListEditor.appendChild(entry); }); document.querySelectorAll('.edit-player-btn').forEach(btn => btn.addEventListener('click', handleEditPlayerForm)); document.querySelectorAll('.delete-player-btn').forEach(btn => btn.addEventListener('click', handleDeletePlayer)); document.querySelectorAll('.move-player-up-btn').forEach(btn => btn.addEventListener('click', handleMovePlayerUp)); document.querySelectorAll('.move-player-down-btn').forEach(btn => btn.addEventListener('click', handleMovePlayerDown)); } function openPlayerForm(playerToEdit = null) { addEditPlayerForm.style.display = 'block'; currentPhotoDataUrl = null; playerPhotoCaptureInput.value = ''; if (playerToEdit) { playerFormTitle.textContent = 'Edit Player'; editPlayerIdInput.value = playerToEdit.id; playerNameInput.value = playerToEdit.name; playerTimeInput.value = formatTime(playerToEdit.initialTime).replace('-', ''); playerPhotoPreviewEl.src = playerToEdit.photo || DEFAULT_PHOTO_URL; currentPhotoDataUrl = (playerToEdit.photo && playerToEdit.photo.startsWith('data:image')) ? playerToEdit.photo : null; playerHotkeyInput.value = playerToEdit.hotkey || ''; playerAdminInput.checked = playerToEdit.isAdmin || false; } else { playerFormTitle.textContent = 'Add Player'; editPlayerIdInput.value = ''; playerNameInput.value = ''; playerTimeInput.value = '60:00'; playerPhotoPreviewEl.src = DEFAULT_PHOTO_URL; playerHotkeyInput.value = ''; playerAdminInput.checked = false; } removePhotoBtn.style.display = (playerPhotoPreviewEl.src !== DEFAULT_PHOTO_URL && playerPhotoPreviewEl.src !== '') ? 'block' : 'none'; playerNameInput.focus(); } playerPhotoCaptureInput.addEventListener('change', (event) => { const file = event.target.files[0]; if (file) { const reader = new FileReader(); reader.onload = (e) => { currentPhotoDataUrl = e.target.result; playerPhotoPreviewEl.src = currentPhotoDataUrl; removePhotoBtn.style.display = 'block'; }; reader.readAsDataURL(file); } }); removePhotoBtn.addEventListener('click', () => { currentPhotoDataUrl = null; playerPhotoPreviewEl.src = DEFAULT_PHOTO_URL; playerPhotoCaptureInput.value = ''; removePhotoBtn.style.display = 'none'; }); addPlayerFormBtn.addEventListener('click', () => { // Event listener for addPlayerFormBtn if (players.length >= 10) { alert("Max 10 players."); return; } openPlayerForm(); }); cancelEditPlayerBtn.addEventListener('click', () => { addEditPlayerForm.style.display = 'none'; }); savePlayerBtn.addEventListener('click', () => { const id = editPlayerIdInput.value; const name = playerNameInput.value.trim(); const initialTimeSeconds = parseTimeInput(playerTimeInput.value); let photoToSave; if (currentPhotoDataUrl) { photoToSave = currentPhotoDataUrl; } else if (id) { const existingPlayer = players.find(p => p.id === parseInt(id)); if (playerPhotoPreviewEl.src === DEFAULT_PHOTO_URL && !currentPhotoDataUrl) { photoToSave = DEFAULT_PHOTO_URL; } else { photoToSave = existingPlayer.photo; } } else { photoToSave = DEFAULT_PHOTO_URL; } const hotkey = playerHotkeyInput.value.trim().toLowerCase(); const isAdmin = playerAdminInput.checked; if (!name) { alert('Name empty.'); return; } if (hotkey.length > 1) { alert('Hotkey single char.'); return; } if (hotkey && players.some(p => p.hotkey === hotkey && p.id !== (id ? parseInt(id) : null))) { alert(`Hotkey '${hotkey}' taken.`); return; } if (isAdmin) { players.forEach(p => { if (p.id !== (id ? parseInt(id) : null)) p.isAdmin = false; }); } if (id) { const player = players.find(p => p.id === parseInt(id)); if (player) { player.name = name; player.initialTime = initialTimeSeconds; if (player.initialTime !== initialTimeSeconds && !playerTimers[player.id]) player.currentTime = initialTimeSeconds; player.photo = photoToSave; player.hotkey = hotkey; player.isAdmin = isAdmin; } } else { if (players.length >= 10) { alert("Max 10 players."); return; } players.push({ id: Date.now(), name, initialTime: initialTimeSeconds, currentTime: initialTimeSeconds, photo: photoToSave, hotkey, isAdmin, isSkipped: false }); } addEditPlayerForm.style.display = 'none'; renderPlayerManagementList(); updateDisplay(); saveState(); }); function handleEditPlayerForm(event) { openPlayerForm(players.find(p => p.id === parseInt(event.target.dataset.id))); } function handleDeletePlayer(event) { const playerId = parseInt(event.target.dataset.id); if (players.length <= 2) { alert("Min 2 players."); return; } if (confirm("Delete player?")) { const playerIndex = players.findIndex(p => p.id === playerId); if (playerIndex > -1) { if (playerIndex === currentPlayerIndex) { if (gameMode === 'normal') pauseTimer(players[currentPlayerIndex].id); } else if (playerIndex < currentPlayerIndex) { currentPlayerIndex--; } if (playerTimers[playerId]) { pauseTimer(playerId, false); delete playerTimers[playerId]; } players.splice(playerIndex, 1); if (players.length > 0) { currentPlayerIndex = Math.max(0, Math.min(currentPlayerIndex, players.length - 1)); } else { currentPlayerIndex = 0; } focusedPlayerIndexInAllTimersMode = Math.min(focusedPlayerIndexInAllTimersMode, players.filter(p => !p.isSkipped).length -1); if (focusedPlayerIndexInAllTimersMode < 0) focusedPlayerIndexInAllTimersMode = 0; renderPlayerManagementList(); updateDisplay(); saveState(); } } } function handleMovePlayerUp(event) { const index = parseInt(event.target.dataset.index); if (index > 0) { [players[index-1], players[index]] = [players[index], players[index-1]]; if (currentPlayerIndex === index) currentPlayerIndex = index - 1; else if (currentPlayerIndex === index - 1) currentPlayerIndex = index; renderPlayerManagementList(); updateDisplay(); saveState(); } } function handleMovePlayerDown(event) { const index = parseInt(event.target.dataset.index); if (index < players.length - 1) { [players[index+1], players[index]] = [players[index], players[index+1]]; if (currentPlayerIndex === index) currentPlayerIndex = index + 1; else if (currentPlayerIndex === index + 1) currentPlayerIndex = index; renderPlayerManagementList(); updateDisplay(); saveState(); } } shufflePlayersBtn.addEventListener('click', () => { for (let i = players.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [players[i], players[j]] = [players[j], players[i]]; } currentPlayerIndex = 0; focusedPlayerIndexInAllTimersMode = 0; renderPlayerManagementList(); updateDisplay(); saveState(); }); reversePlayersBtn.addEventListener('click', () => { players.reverse(); if (players.length > 0) currentPlayerIndex = (players.length - 1) - currentPlayerIndex; focusedPlayerIndexInAllTimersMode = 0; renderPlayerManagementList(); updateDisplay(); saveState(); }); // --- Audio (Synthesized Ticks) --- function playSingleTick() { if (!audioContext || isMuted) return; resumeAudioContext(); const time = audioContext.currentTime; const osc = audioContext.createOscillator(); const gain = audioContext.createGain(); osc.type = 'sine'; osc.frequency.setValueAtTime(TICK_FREQUENCY_HZ, time); gain.gain.setValueAtTime(0, time); gain.gain.linearRampToValueAtTime(0.3, time + TICK_DURATION_S / 2); gain.gain.linearRampToValueAtTime(0, time + TICK_DURATION_S); osc.connect(gain); gain.connect(audioContext.destination); osc.start(time); osc.stop(time + TICK_DURATION_S); } function playContinuousTick(play) { if (!audioContext) return; resumeAudioContext(); if (continuousTickIntervalId) { clearInterval(continuousTickIntervalId); continuousTickIntervalId = null; } if (play && !isMuted) { playSingleTick(); continuousTickIntervalId = setInterval(playSingleTick, CONTINUOUS_TICK_INTERVAL_MS); } } function playShortTick() { if (!audioContext || isMuted) return; resumeAudioContext(); stopShortTick(); playSingleTick(); shortTickIntervalId = setInterval(playSingleTick, CONTINUOUS_TICK_INTERVAL_MS); shortTickTimeoutId = setTimeout(stopShortTick, SHORT_TICK_DURATION_MS); } function stopShortTick() { if (shortTickIntervalId) { clearInterval(shortTickIntervalId); shortTickIntervalId = null; } if (shortTickTimeoutId) { clearTimeout(shortTickTimeoutId); shortTickTimeoutId = null; } } function updateMuteButton() { muteBtn.textContent = isMuted ? '🔊 Unmute' : '🔇 Mute'; if (isMuted) { playContinuousTick(false); stopShortTick(); } else { if (gameMode === 'allTimersRunning' && Object.values(playerTimers).some(id => id !== null)) { playContinuousTick(true); } } } // --- Event Listeners --- function handleUserInteractionForAudio() { resumeAudioContext(); } document.addEventListener('click', handleUserInteractionForAudio, { once: true }); document.addEventListener('touchstart', handleUserInteractionForAudio, { once: true }); document.addEventListener('keydown', handleUserInteractionForAudio, { once: true }); managePlayersBtn.addEventListener('click', () => { managePlayersModal.style.display = 'block'; renderPlayerManagementList(); addEditPlayerForm.style.display = 'none'; }); closeModalBtn.addEventListener('click', () => managePlayersModal.style.display = 'none'); savePlayerManagementBtn.addEventListener('click', () => { if (players.length < 2) { alert("Min 2 players."); return; } managePlayersModal.style.display = 'none'; updateDisplay(); saveState(); }); window.addEventListener('click', (event) => { if (event.target === managePlayersModal) managePlayersModal.style.display = 'none'; }); gameModeBtn.addEventListener('click', () => { if (players.length < 2) { alert("Min 2 players."); return; } if (gameMode === 'normal') switchToAllTimersMode(true); else { const anyTimerRunning = Object.values(playerTimers).some(id => id !== null); if (anyTimerRunning) switchToNormalMode(); else switchToAllTimersMode(true); } }); resetGameBtn.addEventListener('click', resetGame); muteBtn.addEventListener('click', () => { isMuted = !isMuted; updateMuteButton(); saveState(); }); currentPlayerArea.addEventListener('click', () => { if (players.length === 0) return; if (gameMode === 'normal') { const currentP = players[currentPlayerIndex]; if (playerTimers[currentP.id]) pauseTimer(currentP.id); else if (!currentP.isSkipped) startTimer(currentP.id); } else { const activePlayers = players.filter(p => !p.isSkipped); if (activePlayers.length > 0) { const focusedPlayer = activePlayers[focusedPlayerIndexInAllTimersMode]; if (playerTimers[focusedPlayer.id]) pauseTimer(focusedPlayer.id); else if (!focusedPlayer.isSkipped) startTimer(focusedPlayer.id); } } }); let touchStartY = 0; nextPlayerArea.addEventListener('touchstart', (e) => { if (e.touches.length === 1) touchStartY = e.touches[0].clientY; }, { passive: true }); nextPlayerArea.addEventListener('touchend', (e) => { if (players.length === 0 || touchStartY === 0 || e.changedTouches.length === 0) return; if ((touchStartY - e.changedTouches[0].clientY) > 50) { // Swipe Up if (gameMode === 'normal') passTurn(); else changeFocusInAllTimersMode(); } touchStartY = 0; }); document.addEventListener('keydown', (event) => { const key = event.key.toLowerCase(); if (document.activeElement && (document.activeElement.tagName === 'INPUT' || document.activeElement.tagName === 'TEXTAREA')) return; const triggeredPlayer = players.find(p => p.hotkey === key); if (triggeredPlayer) { event.preventDefault(); if (gameMode === 'normal') { if (players[currentPlayerIndex].id === triggeredPlayer.id) passTurn(); } else { if (playerTimers[triggeredPlayer.id]) pauseTimer(triggeredPlayer.id); else if (!triggeredPlayer.isSkipped) startTimer(triggeredPlayer.id); } return; } const adminPlayer = players.find(p => p.isAdmin); if (adminPlayer && key === 's') { event.preventDefault(); if (gameMode === 'normal') { const currentP = players[currentPlayerIndex]; if (playerTimers[currentP.id]) pauseTimer(currentP.id); else if (!currentP.isSkipped) startTimer(currentP.id); } else { const anyTimerRunning = Object.values(playerTimers).some(id => id !== null); if (anyTimerRunning) switchToNormalMode(); else switchToAllTimersMode(true); } } }); // --- Initialization --- function init() { initAudio(); loadState(); updateMuteButton(); if ('serviceWorker' in navigator) { navigator.serviceWorker.register('sw.js') .then(reg => console.log('SW registered:', reg)) .catch(err => console.error('SW registration failed:', err)); } } init(); });