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 = `