649 lines
30 KiB
JavaScript
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();
|
|
}); |