Files
nexus-timer/script.js
2025-05-07 20:37:29 +02:00

649 lines
30 KiB
JavaScript

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 = '<p>No players yet. Add some!</p>';
}
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 = `
<img src="${photoSrc}" alt="P" style="width:20px; height:20px; border-radius:50%; margin-right: 5px; object-fit:cover;">
<span>${index + 1}. ${player.name} (${formatTime(player.initialTime)}) ${player.isAdmin ? '(Admin)' : ''} ${player.hotkey ? `[${player.hotkey}]`: ''}</span>
<div>
<button class="edit-player-btn" data-id="${player.id}">Edit</button>
<button class="delete-player-btn" data-id="${player.id}">Del</button>
${index > 0 ? `<button class="move-player-up-btn" data-index="${index}">↑</button>` : ''}
${index < players.length - 1 ? `<button class="move-player-down-btn" data-index="${index}">↓</button>` : ''}
</div>
`;
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();
});