Initial commit
This commit is contained in:
92
js/core/eventHandlers.js
Normal file
92
js/core/eventHandlers.js
Normal file
@@ -0,0 +1,92 @@
|
||||
// eventHandlers.js - UI event handlers
|
||||
import * as config from '../config.js';
|
||||
import * as state from './state.js';
|
||||
import * as ui from '../ui/ui.js';
|
||||
import audioManager from '../ui/audio.js';
|
||||
import camera from '../ui/camera.js';
|
||||
import { togglePauseResume, fullResetApp } from './gameActions.js';
|
||||
import { handlePlayerFormSubmit, handleDeletePlayer } from './playerManager.js';
|
||||
|
||||
export function handleGameButtonClick() {
|
||||
audioManager.play('buttonClick');
|
||||
togglePauseResume();
|
||||
}
|
||||
|
||||
export function handleSetupButtonClick() {
|
||||
audioManager.play('buttonClick');
|
||||
if (state.getGameState() === config.GAME_STATES.RUNNING) {
|
||||
alert('Please pause the game before editing players.');
|
||||
return;
|
||||
}
|
||||
const currentPlayer = state.getCurrentPlayer();
|
||||
if (!currentPlayer) {
|
||||
console.warn("Edit clicked but no current player?");
|
||||
return; // Or show Add Player modal?
|
||||
}
|
||||
camera.stopStream(); // Ensure camera is off before opening modal
|
||||
ui.showPlayerModal(false, currentPlayer);
|
||||
}
|
||||
|
||||
export function handleAddPlayerButtonClick() {
|
||||
audioManager.play('buttonClick');
|
||||
if (state.getGameState() === config.GAME_STATES.RUNNING) {
|
||||
alert('Please pause the game before adding players.');
|
||||
return;
|
||||
}
|
||||
camera.stopStream(); // Ensure camera is off before opening modal
|
||||
ui.showPlayerModal(true);
|
||||
}
|
||||
|
||||
export function handleResetButtonClick() {
|
||||
audioManager.play('buttonClick');
|
||||
if (state.getGameState() === config.GAME_STATES.RUNNING) {
|
||||
alert('Please pause the game before resetting.');
|
||||
return;
|
||||
}
|
||||
ui.showResetModal();
|
||||
}
|
||||
|
||||
export function handlePlayerModalCancel() {
|
||||
audioManager.play('buttonClick');
|
||||
ui.hidePlayerModal();
|
||||
camera.stopStream(); // Make sure camera turns off
|
||||
}
|
||||
|
||||
export function handleResetConfirm() {
|
||||
audioManager.play('buttonClick');
|
||||
fullResetApp();
|
||||
}
|
||||
|
||||
export function handleResetCancel() {
|
||||
audioManager.play('buttonClick');
|
||||
ui.hideResetModal();
|
||||
}
|
||||
|
||||
export function handleCameraButtonClick(event) {
|
||||
event.preventDefault(); // Prevent form submission if inside form
|
||||
audioManager.play('buttonClick');
|
||||
camera.open(); // Open the camera interface
|
||||
}
|
||||
|
||||
// --- Timer Callbacks ---
|
||||
export function handleTimerTick() {
|
||||
// Timer module already updated the state, just need to redraw UI
|
||||
ui.renderPlayers();
|
||||
}
|
||||
|
||||
export function handlePlayerSwitchOnTimer(newPlayerIndex) {
|
||||
// Timer detected current player ran out, found next player
|
||||
console.log(`Timer switching to player index: ${newPlayerIndex}`);
|
||||
// Import switchToPlayer dynamically to avoid circular dependency
|
||||
import('./playerManager.js').then(module => {
|
||||
module.switchToPlayer(newPlayerIndex);
|
||||
});
|
||||
// Sound is handled in switchToPlayer
|
||||
}
|
||||
|
||||
// --- Camera Callback ---
|
||||
export function handleCameraCapture(imageDataUrl) {
|
||||
console.log("Image captured");
|
||||
ui.updateImagePreviewFromDataUrl(imageDataUrl);
|
||||
// Camera module already closed the camera UI
|
||||
}
|
||||
134
js/core/gameActions.js
Normal file
134
js/core/gameActions.js
Normal file
@@ -0,0 +1,134 @@
|
||||
// gameActions.js - Core game action functions
|
||||
import * as config from '../config.js';
|
||||
import * as state from './state.js';
|
||||
import * as ui from '../ui/ui.js';
|
||||
import * as timer from './timer.js';
|
||||
import audioManager from '../ui/audio.js';
|
||||
import * as screenLockManager from '../services/screenLockManager.js'; // Import screen lock manager
|
||||
|
||||
// --- Core Game Actions ---
|
||||
|
||||
// Declare handleGameOver at the top level to avoid referencing before definition
|
||||
export function handleGameOver() {
|
||||
state.setGameState(config.GAME_STATES.OVER);
|
||||
audioManager.play('gameOver');
|
||||
timer.stopTimer(); // Ensure timer is stopped
|
||||
|
||||
// Release screen wake lock when game is over
|
||||
screenLockManager.releaseWakeLock().then(success => {
|
||||
if (success) {
|
||||
console.log('Screen wake lock released on game over');
|
||||
}
|
||||
});
|
||||
|
||||
ui.updateGameButton();
|
||||
ui.renderPlayers(); // Update to show final state
|
||||
}
|
||||
|
||||
export function startGame() {
|
||||
if (state.getPlayers().length < 2) {
|
||||
alert('You need at least 2 players to start.');
|
||||
return;
|
||||
}
|
||||
if (state.getGameState() === config.GAME_STATES.SETUP || state.getGameState() === config.GAME_STATES.PAUSED) {
|
||||
state.setGameState(config.GAME_STATES.RUNNING);
|
||||
audioManager.play('gameStart');
|
||||
timer.startTimer();
|
||||
|
||||
// Acquire screen wake lock when game starts
|
||||
screenLockManager.acquireWakeLock().then(success => {
|
||||
if (success) {
|
||||
console.log('Screen wake lock acquired for game');
|
||||
} else {
|
||||
console.warn('Failed to acquire screen wake lock');
|
||||
}
|
||||
});
|
||||
|
||||
ui.updateGameButton();
|
||||
ui.renderPlayers(); // Ensure active timer styling is applied
|
||||
}
|
||||
}
|
||||
|
||||
export function pauseGame() {
|
||||
if (state.getGameState() === config.GAME_STATES.RUNNING) {
|
||||
state.setGameState(config.GAME_STATES.PAUSED);
|
||||
audioManager.play('gamePause');
|
||||
timer.stopTimer();
|
||||
|
||||
// Release screen wake lock when game is paused
|
||||
screenLockManager.releaseWakeLock().then(success => {
|
||||
if (success) {
|
||||
console.log('Screen wake lock released on pause');
|
||||
}
|
||||
});
|
||||
|
||||
ui.updateGameButton();
|
||||
ui.renderPlayers(); // Ensure active timer styling is removed
|
||||
}
|
||||
}
|
||||
|
||||
export function resumeGame() {
|
||||
if (state.getGameState() === config.GAME_STATES.PAUSED) {
|
||||
// Check if there's actually a player with time left
|
||||
if (state.findNextPlayerWithTime() === -1) {
|
||||
console.log("Cannot resume, no players have time left.");
|
||||
// Optionally set state to OVER here
|
||||
handleGameOver();
|
||||
return;
|
||||
}
|
||||
state.setGameState(config.GAME_STATES.RUNNING);
|
||||
audioManager.play('gameResume');
|
||||
timer.startTimer();
|
||||
ui.updateGameButton();
|
||||
ui.renderPlayers(); // Ensure active timer styling is applied
|
||||
}
|
||||
}
|
||||
|
||||
export function togglePauseResume() {
|
||||
const currentGameState = state.getGameState();
|
||||
if (currentGameState === config.GAME_STATES.RUNNING) {
|
||||
pauseGame();
|
||||
} else if (currentGameState === config.GAME_STATES.PAUSED) {
|
||||
resumeGame();
|
||||
} else if (currentGameState === config.GAME_STATES.SETUP) {
|
||||
startGame();
|
||||
} else if (currentGameState === config.GAME_STATES.OVER) {
|
||||
resetGame(); // Or just go back to setup? Let's reset.
|
||||
startGame();
|
||||
}
|
||||
}
|
||||
|
||||
export function resetGame() {
|
||||
timer.stopTimer(); // Stop timer if running/paused
|
||||
state.resetPlayersTime();
|
||||
state.setGameState(config.GAME_STATES.SETUP);
|
||||
state.setCurrentPlayerIndex(0); // Go back to first player
|
||||
|
||||
// Release screen wake lock when game is reset
|
||||
screenLockManager.releaseWakeLock().then(success => {
|
||||
if (success) {
|
||||
console.log('Screen wake lock released on reset');
|
||||
}
|
||||
});
|
||||
|
||||
audioManager.play('buttonClick'); // Or a specific reset sound?
|
||||
ui.updateGameButton();
|
||||
ui.renderPlayers();
|
||||
}
|
||||
|
||||
export function fullResetApp() {
|
||||
timer.stopTimer();
|
||||
state.resetToDefaults();
|
||||
|
||||
// Release screen wake lock on full reset
|
||||
screenLockManager.releaseWakeLock().then(success => {
|
||||
if (success) {
|
||||
console.log('Screen wake lock released on full reset');
|
||||
}
|
||||
});
|
||||
|
||||
audioManager.play('gameOver'); // Use game over sound for full reset
|
||||
ui.hideResetModal();
|
||||
ui.updateGameButton();
|
||||
ui.renderPlayers();
|
||||
}
|
||||
154
js/core/playerManager.js
Normal file
154
js/core/playerManager.js
Normal file
@@ -0,0 +1,154 @@
|
||||
// playerManager.js - Player-related operations
|
||||
import * as config from '../config.js';
|
||||
import * as state from './state.js';
|
||||
import * as ui from '../ui/ui.js';
|
||||
import * as timer from './timer.js';
|
||||
import audioManager from '../ui/audio.js';
|
||||
import camera from '../ui/camera.js';
|
||||
|
||||
export function switchToPlayer(index) {
|
||||
if (index >= 0 && index < state.getPlayers().length) {
|
||||
const previousIndex = state.getCurrentPlayerIndex();
|
||||
if(index !== previousIndex) {
|
||||
state.setCurrentPlayerIndex(index);
|
||||
audioManager.play('playerSwitch');
|
||||
ui.renderPlayers(); // Update UI immediately
|
||||
|
||||
// If the game is running, restart the timer for the new player
|
||||
// The timer interval callback will handle the decrementing
|
||||
if (state.getGameState() === config.GAME_STATES.RUNNING) {
|
||||
timer.startTimer(); // This clears the old interval and starts anew
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function nextPlayer() {
|
||||
const currentGameState = state.getGameState();
|
||||
let newIndex = -1;
|
||||
|
||||
if (currentGameState === config.GAME_STATES.RUNNING) {
|
||||
newIndex = state.findNextPlayerWithTimeCircular(1); // Find next with time
|
||||
} else {
|
||||
// Allow cycling through all players if not running
|
||||
const playerCount = state.getPlayers().length;
|
||||
if(playerCount > 0) {
|
||||
newIndex = (state.getCurrentPlayerIndex() + 1) % playerCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (newIndex !== -1) {
|
||||
switchToPlayer(newIndex);
|
||||
} else if (currentGameState === config.GAME_STATES.RUNNING) {
|
||||
console.log("NextPlayer: No other player has time remaining.");
|
||||
// Optionally handle game over immediately? Timer logic should catch this too.
|
||||
}
|
||||
}
|
||||
|
||||
export function previousPlayer() {
|
||||
const currentGameState = state.getGameState();
|
||||
let newIndex = -1;
|
||||
|
||||
if (currentGameState === config.GAME_STATES.RUNNING) {
|
||||
newIndex = state.findNextPlayerWithTimeCircular(-1); // Find previous with time
|
||||
} else {
|
||||
// Allow cycling through all players if not running
|
||||
const playerCount = state.getPlayers().length;
|
||||
if (playerCount > 0) {
|
||||
newIndex = (state.getCurrentPlayerIndex() - 1 + playerCount) % playerCount;
|
||||
}
|
||||
}
|
||||
|
||||
if (newIndex !== -1) {
|
||||
switchToPlayer(newIndex);
|
||||
} else if (currentGameState === config.GAME_STATES.RUNNING) {
|
||||
console.log("PreviousPlayer: No other player has time remaining.");
|
||||
}
|
||||
}
|
||||
|
||||
export function handlePlayerFormSubmit(event) {
|
||||
event.preventDefault();
|
||||
audioManager.play('buttonClick');
|
||||
|
||||
const name = ui.elements.playerNameInput.value.trim();
|
||||
const timeInMinutes = parseInt(ui.elements.playerTimeInput.value, 10);
|
||||
let remainingTimeSeconds = 0; // Default
|
||||
const isNewPlayer = ui.elements.modalTitle.textContent === 'Add New Player';
|
||||
const currentGameState = state.getGameState();
|
||||
|
||||
if (!name || isNaN(timeInMinutes) || timeInMinutes <= 0) {
|
||||
alert('Please enter a valid name and positive time.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Get remaining time ONLY if editing and game is paused/over
|
||||
if (!isNewPlayer && (currentGameState === config.GAME_STATES.PAUSED || currentGameState === config.GAME_STATES.OVER)) {
|
||||
const remainingTimeString = ui.elements.playerRemainingTimeInput.value;
|
||||
const parsedSeconds = ui.parseTimeString(remainingTimeString);
|
||||
if (parsedSeconds === null) { // Check if parsing failed
|
||||
alert('Please enter remaining time in MM:SS format (e.g., 05:30).');
|
||||
return;
|
||||
}
|
||||
remainingTimeSeconds = parsedSeconds;
|
||||
// Validate remaining time against total time? Optional.
|
||||
if (remainingTimeSeconds > timeInMinutes * 60) {
|
||||
alert('Remaining time cannot be greater than the total time.');
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// For new players or when editing in setup, remaining time matches total time
|
||||
remainingTimeSeconds = timeInMinutes * 60;
|
||||
}
|
||||
|
||||
let imageDataUrl = ui.elements.playerImageInput.dataset.capturedImage || null;
|
||||
const imageFile = ui.elements.playerImageInput.files[0];
|
||||
|
||||
const saveAction = (finalImageData) => {
|
||||
if (isNewPlayer) {
|
||||
state.addPlayer(name, timeInMinutes, finalImageData);
|
||||
audioManager.play('playerAdded');
|
||||
} else {
|
||||
const playerIndex = state.getCurrentPlayerIndex();
|
||||
// Use 'undefined' for image if no new image is provided, so state.updatePlayer keeps the old one
|
||||
const imageArg = finalImageData !== null ? finalImageData : (isNewPlayer ? null : undefined);
|
||||
state.updatePlayer(playerIndex, name, timeInMinutes, remainingTimeSeconds, imageArg);
|
||||
audioManager.play('playerEdited');
|
||||
}
|
||||
ui.hidePlayerModal();
|
||||
ui.renderPlayers();
|
||||
ui.updateGameButton(); // Update in case player count changed for setup state
|
||||
camera.stopStream(); // Ensure camera is stopped
|
||||
};
|
||||
|
||||
if (!imageDataUrl && imageFile) {
|
||||
// Handle file upload: Read file as Data URL
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => saveAction(e.target.result);
|
||||
reader.onerror = (e) => {
|
||||
console.error("Error reading image file:", e);
|
||||
alert("Error processing image file.");
|
||||
};
|
||||
reader.readAsDataURL(imageFile);
|
||||
} else {
|
||||
// Handle captured image or no image change
|
||||
const currentImage = isNewPlayer ? null : state.getCurrentPlayer()?.image;
|
||||
// If imageDataUrl has content (from camera), use it.
|
||||
// If not, and no file was selected, keep the current image (by passing undefined to updatePlayer later).
|
||||
// If it's a new player and no image, pass null.
|
||||
saveAction(imageDataUrl ?? (isNewPlayer ? null : currentImage));
|
||||
}
|
||||
}
|
||||
|
||||
export function handleDeletePlayer() {
|
||||
audioManager.play('buttonClick');
|
||||
const success = state.deletePlayer(state.getCurrentPlayerIndex());
|
||||
if (success) {
|
||||
audioManager.play('playerDeleted');
|
||||
ui.hidePlayerModal();
|
||||
ui.renderPlayers();
|
||||
ui.updateGameButton(); // Update in case player count dropped below 2
|
||||
} else {
|
||||
alert('Cannot delete player. Minimum of 2 players required.');
|
||||
}
|
||||
camera.stopStream();
|
||||
}
|
||||
230
js/core/state.js
Normal file
230
js/core/state.js
Normal file
@@ -0,0 +1,230 @@
|
||||
// state.js
|
||||
import { LOCAL_STORAGE_KEY, DEFAULT_PLAYERS, GAME_STATES, DEFAULT_PLAYER_TIME_SECONDS } from '../config.js';
|
||||
|
||||
let players = [];
|
||||
let currentPlayerIndex = 0;
|
||||
let gameState = GAME_STATES.SETUP;
|
||||
|
||||
// --- State Accessors ---
|
||||
|
||||
export function getPlayers() {
|
||||
return [...players]; // Return a copy to prevent direct mutation
|
||||
}
|
||||
|
||||
export function getCurrentPlayer() {
|
||||
if (players.length === 0) return null;
|
||||
return players[currentPlayerIndex];
|
||||
}
|
||||
|
||||
export function getPlayerById(id) {
|
||||
return players.find(p => p.id === id);
|
||||
}
|
||||
|
||||
export function getCurrentPlayerIndex() {
|
||||
return currentPlayerIndex;
|
||||
}
|
||||
|
||||
export function getGameState() {
|
||||
return gameState;
|
||||
}
|
||||
|
||||
// --- State Mutators ---
|
||||
|
||||
export function setPlayers(newPlayers) {
|
||||
players = newPlayers;
|
||||
saveData();
|
||||
}
|
||||
|
||||
export function setCurrentPlayerIndex(index) {
|
||||
if (index >= 0 && index < players.length) {
|
||||
currentPlayerIndex = index;
|
||||
saveData();
|
||||
} else {
|
||||
console.error(`Invalid player index: ${index}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function setGameState(newState) {
|
||||
if (Object.values(GAME_STATES).includes(newState)) {
|
||||
gameState = newState;
|
||||
saveData();
|
||||
} else {
|
||||
console.error(`Invalid game state: ${newState}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function updatePlayerTime(index, remainingTime) {
|
||||
if (index >= 0 && index < players.length) {
|
||||
players[index].remainingTime = Math.max(0, remainingTime); // Ensure time doesn't go below 0
|
||||
saveData(); // Save data whenever time updates
|
||||
}
|
||||
}
|
||||
|
||||
export function addPlayer(name, timeInMinutes, image = null) {
|
||||
const timeInSeconds = timeInMinutes * 60;
|
||||
const newId = Date.now();
|
||||
players.push({
|
||||
id: newId,
|
||||
name: name,
|
||||
timeInSeconds: timeInSeconds,
|
||||
remainingTime: timeInSeconds,
|
||||
image: image
|
||||
});
|
||||
currentPlayerIndex = players.length - 1; // Focus new player
|
||||
saveData();
|
||||
return players[players.length - 1]; // Return the newly added player
|
||||
}
|
||||
|
||||
export function updatePlayer(index, name, timeInMinutes, remainingTimeSeconds, image) {
|
||||
if (index >= 0 && index < players.length) {
|
||||
const player = players[index];
|
||||
const timeInSeconds = timeInMinutes * 60;
|
||||
|
||||
player.name = name;
|
||||
player.timeInSeconds = timeInSeconds;
|
||||
|
||||
// Update remaining time carefully based on game state
|
||||
if (gameState === GAME_STATES.SETUP) {
|
||||
player.remainingTime = timeInSeconds;
|
||||
} else if (gameState === GAME_STATES.PAUSED || gameState === GAME_STATES.OVER) {
|
||||
// Allow direct setting of remaining time only when paused or over
|
||||
player.remainingTime = remainingTimeSeconds;
|
||||
}
|
||||
// If running, remaining time is managed by the timer, don't override here unless intended
|
||||
|
||||
if (image !== undefined) { // Allow updating image (null means remove image)
|
||||
player.image = image;
|
||||
}
|
||||
saveData();
|
||||
return player;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function deletePlayer(index) {
|
||||
if (players.length <= 2) {
|
||||
console.warn('Cannot delete player, minimum 2 players required.');
|
||||
return false; // Indicate deletion failed
|
||||
}
|
||||
if (index >= 0 && index < players.length) {
|
||||
players.splice(index, 1);
|
||||
if (currentPlayerIndex >= players.length) {
|
||||
currentPlayerIndex = players.length - 1;
|
||||
} else if (currentPlayerIndex > index) {
|
||||
// Adjust index if deleting someone before the current player
|
||||
// No adjustment needed if deleting current or after current
|
||||
}
|
||||
saveData();
|
||||
return true; // Indicate success
|
||||
}
|
||||
return false; // Indicate deletion failed
|
||||
}
|
||||
|
||||
export function resetPlayersTime() {
|
||||
players.forEach(player => {
|
||||
player.remainingTime = player.timeInSeconds;
|
||||
});
|
||||
saveData();
|
||||
}
|
||||
|
||||
export function resetToDefaults() {
|
||||
// Deep copy default players to avoid modifying the constant
|
||||
players = JSON.parse(JSON.stringify(DEFAULT_PLAYERS));
|
||||
gameState = GAME_STATES.SETUP;
|
||||
currentPlayerIndex = 0;
|
||||
saveData();
|
||||
}
|
||||
|
||||
export function areAllTimersFinished() {
|
||||
return players.every(player => player.remainingTime <= 0);
|
||||
}
|
||||
|
||||
// Returns the index of the next player with time > 0, or -1 if none
|
||||
export function findNextPlayerWithTime() {
|
||||
if (players.length === 0) return -1;
|
||||
const startIndex = (currentPlayerIndex + 1) % players.length;
|
||||
let index = startIndex;
|
||||
|
||||
do {
|
||||
if (players[index].remainingTime > 0) {
|
||||
return index;
|
||||
}
|
||||
index = (index + 1) % players.length;
|
||||
} while (index !== startIndex);
|
||||
|
||||
// Check current player last if no others found
|
||||
if(players[currentPlayerIndex].remainingTime > 0) {
|
||||
return currentPlayerIndex;
|
||||
}
|
||||
|
||||
return -1; // No player has time left
|
||||
}
|
||||
|
||||
// Find next player with time in specified direction (1 for next, -1 for prev)
|
||||
export function findNextPlayerWithTimeCircular(direction) {
|
||||
if (players.length === 0) return -1;
|
||||
let index = currentPlayerIndex;
|
||||
|
||||
for (let i = 0; i < players.length; i++) {
|
||||
index = (index + direction + players.length) % players.length;
|
||||
if (players[index]?.remainingTime > 0) { // Check if player exists and has time
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
// If no other player found, check if current player has time (only relevant if direction search fails)
|
||||
if (players[currentPlayerIndex]?.remainingTime > 0) {
|
||||
return currentPlayerIndex;
|
||||
}
|
||||
|
||||
return -1; // No player has time left
|
||||
}
|
||||
|
||||
|
||||
// --- Persistence ---
|
||||
|
||||
export function saveData() {
|
||||
const dataToSave = {
|
||||
players,
|
||||
gameState,
|
||||
currentPlayerIndex
|
||||
};
|
||||
try {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(dataToSave));
|
||||
} catch (error) {
|
||||
console.error("Error saving data to localStorage:", error);
|
||||
// Maybe notify the user that settings won't be saved
|
||||
}
|
||||
}
|
||||
|
||||
export function loadData() {
|
||||
const savedData = localStorage.getItem(LOCAL_STORAGE_KEY);
|
||||
if (savedData) {
|
||||
try {
|
||||
const parsedData = JSON.parse(savedData);
|
||||
players = parsedData.players || JSON.parse(JSON.stringify(DEFAULT_PLAYERS));
|
||||
gameState = parsedData.gameState || GAME_STATES.SETUP;
|
||||
currentPlayerIndex = parsedData.currentPlayerIndex || 0;
|
||||
|
||||
// Basic validation/migration if needed
|
||||
if (currentPlayerIndex >= players.length) {
|
||||
currentPlayerIndex = 0;
|
||||
}
|
||||
// Ensure all players have necessary properties
|
||||
players = players.map(p => ({
|
||||
id: p.id || Date.now() + Math.random(), // Ensure ID exists
|
||||
name: p.name || 'Player',
|
||||
timeInSeconds: p.timeInSeconds || DEFAULT_PLAYER_TIME_SECONDS,
|
||||
remainingTime: p.remainingTime !== undefined ? p.remainingTime : (p.timeInSeconds || DEFAULT_PLAYER_TIME_SECONDS),
|
||||
image: p.image || null
|
||||
}));
|
||||
|
||||
} catch (error) {
|
||||
console.error("Error parsing data from localStorage:", error);
|
||||
resetToDefaults(); // Reset to defaults if stored data is corrupt
|
||||
}
|
||||
} else {
|
||||
resetToDefaults(); // Use defaults if no saved data
|
||||
}
|
||||
// No saveData() here, loadData just loads the state
|
||||
}
|
||||
104
js/core/timer.js
Normal file
104
js/core/timer.js
Normal file
@@ -0,0 +1,104 @@
|
||||
// timer.js
|
||||
import * as state from './state.js';
|
||||
import { GAME_STATES } from '../config.js';
|
||||
import audioManager from '../ui/audio.js';
|
||||
|
||||
let timerInterval = null;
|
||||
let onTimerTickCallback = null; // Callback for UI updates
|
||||
let onPlayerSwitchCallback = null; // Callback for when player switches due to time running out
|
||||
let onGameOverCallback = null; // Callback for when all players run out of time
|
||||
let timeExpiredFlagsById = new Map(); // Track which players have had their timeout sound played
|
||||
|
||||
export function initTimer(options) {
|
||||
onTimerTickCallback = options.onTimerTick;
|
||||
onPlayerSwitchCallback = options.onPlayerSwitch;
|
||||
onGameOverCallback = options.onGameOver;
|
||||
timeExpiredFlagsById.clear(); // Reset flags on init
|
||||
}
|
||||
|
||||
export function startTimer() {
|
||||
if (timerInterval) clearInterval(timerInterval); // Clear existing interval if any
|
||||
|
||||
// Stop any previous sounds (like low time warning) before starting fresh
|
||||
audioManager.stopAllSounds();
|
||||
|
||||
// Reset the expired sound flags when starting a new timer
|
||||
timeExpiredFlagsById.clear();
|
||||
|
||||
timerInterval = setInterval(() => {
|
||||
const currentPlayerIndex = state.getCurrentPlayerIndex();
|
||||
const currentPlayer = state.getCurrentPlayer(); // Get player data after index
|
||||
|
||||
if (!currentPlayer) {
|
||||
console.warn("Timer running but no current player found.");
|
||||
stopTimer();
|
||||
return;
|
||||
}
|
||||
|
||||
// Only decrease time if the current player has time left
|
||||
if (currentPlayer.remainingTime > 0) {
|
||||
const newTime = currentPlayer.remainingTime - 1;
|
||||
state.updatePlayerTime(currentPlayerIndex, newTime); // Update state
|
||||
|
||||
// Play timer sounds - ensure we're not leaking audio resources
|
||||
audioManager.playTimerSound(newTime);
|
||||
|
||||
// Notify UI to update
|
||||
if (onTimerTickCallback) onTimerTickCallback();
|
||||
|
||||
} else { // Current player's time just hit 0 or was already 0
|
||||
// Ensure time is exactly 0 if it somehow went negative
|
||||
if(currentPlayer.remainingTime < 0) {
|
||||
state.updatePlayerTime(currentPlayerIndex, 0);
|
||||
}
|
||||
|
||||
// Play time expired sound (only once per player per game)
|
||||
if (!timeExpiredFlagsById.has(currentPlayer.id)) {
|
||||
audioManager.playTimerExpired();
|
||||
timeExpiredFlagsById.set(currentPlayer.id, true);
|
||||
}
|
||||
|
||||
// Check if the game should end or switch player
|
||||
if (state.areAllTimersFinished()) {
|
||||
stopTimer();
|
||||
if (onGameOverCallback) onGameOverCallback();
|
||||
} else {
|
||||
// Find the *next* player who still has time
|
||||
const nextPlayerIndex = state.findNextPlayerWithTime(); // This finds ANY player with time
|
||||
if (nextPlayerIndex !== -1 && nextPlayerIndex !== currentPlayerIndex) {
|
||||
// Switch player and ensure we stop any sounds from current player
|
||||
audioManager.stopTimerSounds(); // Stop specific timer sounds before switching
|
||||
|
||||
if (onPlayerSwitchCallback) onPlayerSwitchCallback(nextPlayerIndex);
|
||||
|
||||
// Immediately update UI after switch
|
||||
if (onTimerTickCallback) onTimerTickCallback();
|
||||
} else if (nextPlayerIndex === -1) {
|
||||
// This case shouldn't be reached if areAllTimersFinished is checked first, but as a safeguard:
|
||||
console.warn("Timer tick: Current player out of time, but no next player found, yet not all timers finished?");
|
||||
stopTimer(); // Stop timer if state is inconsistent
|
||||
if (onGameOverCallback) onGameOverCallback(); // Treat as game over
|
||||
}
|
||||
// If nextPlayerIndex is the same as currentPlayerIndex, means others are out of time, let this timer continue
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
export function stopTimer() {
|
||||
clearInterval(timerInterval);
|
||||
timerInterval = null;
|
||||
// Stop all timer-related sounds to prevent them from continuing to play
|
||||
audioManager.stopTimerSounds();
|
||||
}
|
||||
|
||||
export function isTimerRunning() {
|
||||
return timerInterval !== null;
|
||||
}
|
||||
|
||||
// Clean up resources when the application is closing or component unmounts
|
||||
export function cleanup() {
|
||||
stopTimer();
|
||||
timeExpiredFlagsById.clear();
|
||||
audioManager.stopAllSounds();
|
||||
}
|
||||
Reference in New Issue
Block a user