// OTAI SECTION: header let gameState = { boxesCollected: 0, bombPosition: js_vars.bomb_position, gridSize: js_vars.grid_size, variant: js_vars.variant, gameEnded: false, timeoutExpired: false, bonusBoxesAvailable: js_vars.extra_boxes, closeThreshold: js_vars.close_threshold, isCollecting: false, collectionInterval: null, stoppedByPlayer: false, startTime: null, timerInterval: null, timeRemaining: Math.floor(js_vars.fancy_timeout_seconds), luckyNumbers: js_vars.lucky_numbers, hasStartedOnce: false, bombProtected: false, bombExploded: false, isPaused: false, }; const closeMessages = [ "The bomb was close!", "You were very close to the bomb!", "Careful! The bomb was nearby!", "That was a narrow escape!", "Close call! The bomb is near!", "You almost hit the bomb!" ]; const isFancyMode = gameState.variant === 'fancy' || gameState.variant === 'speed' || gameState.variant === 'stopstart' || gameState.variant === 'lucky'; const isStopStartMode = gameState.variant === 'stopstart' || gameState.variant === 'lucky'; if (isFancyMode) { document.body.classList.add('fancy-mode'); } // OTAI SECTION: functions function checkAndShowCloseDialog() { const boxesFromBomb = gameState.bombPosition - gameState.boxesCollected; if (!gameState.bombExploded && boxesFromBomb > 0 && boxesFromBomb <= gameState.closeThreshold) { showCloseDialog(); } } function collectBox(boxNumber) { if (!isFancyMode && !gameState.hasStartedOnce) { gameState.hasStartedOnce = true; startTimer(); } if (isFancyMode) { const boxEle = docQuerySelectorStrict(`.grid-box[data-box-number="${boxNumber}"]`); boxEle.classList.add('collected'); } else { for (let i = gameState.boxesCollected + 1; i <= boxNumber; i++) { const boxEle = docQuerySelectorStrict(`.grid-box[data-box-number="${i}"]`); boxEle.classList.add('collected'); } } gameState.boxesCollected += 1; docQuerySelectorStrict('#boxes-count').textContent = gameState.boxesCollected; docQuerySelectorStrict('#earnings').textContent = gameState.boxesCollected * js_vars.points_per_box; liveSend({ type: 'collect', boxes: gameState.boxesCollected, box_id: boxNumber }); } function createGrid() { const container = docQuerySelectorStrict('#grid-container'); container.innerHTML = ''; for (let i = 1; i <= gameState.gridSize; i++) { const box = document.createElement('div'); box.className = 'grid-box'; box.dataset.boxNumber = i; box.textContent = i; if (isLuckyNumber(i)) { box.classList.add('lucky-number'); } if (!isFancyMode) { box.addEventListener('click', function() { if (!gameState.gameEnded && gameState.boxesCollected < i) { collectBox(i); } }); } container.appendChild(box); } } function endGame() { gameState.gameEnded = true; if (gameState.timerInterval) { clearInterval(gameState.timerInterval); gameState.timerInterval = null; } docQuerySelectorStrict('#form').submit(); } function finalStopGame() { stopAutoCollection(); gameState.gameEnded = true; gameState.stoppedByPlayer = true; liveSend({type: 'stop'}); // SHOW WAITING FIRST showWaitingDialog(); setTimeout(function() { hideWaitingDialog(); // THEN close-call message checkAndShowCloseDialog(); setTimeout(function() { revealBombResult(); }, 2600); }, js_vars.waiting_delay_seconds * js_vars.ms_per_second); } function getCurrentSpeed() { if (gameState.variant !== 'speed' && gameState.variant !== 'stopstart' && gameState.variant !== 'lucky') { return js_vars.boxes_per_second; } if (!gameState.startTime) { return js_vars.speed_phase_1_bps; } const elapsedSeconds = (Date.now() - gameState.startTime) / js_vars.ms_per_second; if (elapsedSeconds < js_vars.speed_phase_1_duration) { return js_vars.speed_phase_1_bps; } else if (elapsedSeconds < js_vars.speed_phase_2_duration) { return js_vars.speed_phase_2_bps; } else { return js_vars.speed_phase_3_bps; } } function handleBonusAccept(dialog) { dialog.close(); const bonus = Math.min( gameState.bonusBoxesAvailable, gameState.gridSize - gameState.boxesCollected ); let revealed = 0; const interval = setInterval(function() { if (revealed < bonus) { const nextBoxNumber = gameState.boxesCollected + 1; collectBox(nextBoxNumber); revealed++; } else { clearInterval(interval); liveSend({ type: 'bonus', bonus_used: revealed }); endGame(); } }, js_vars.reveal_delay_ms); } function handleBonusDecline(dialog) { dialog.close(); liveSend({type: 'bonus', bonus_used: 0}); endGame(); } function handleTimeout() { if (!gameState.timeoutExpired) { gameState.timeoutExpired = true; stopAutoCollection(); showWaitingDialog(); setTimeout(function() { hideWaitingDialog(); if (!gameState.gameEnded && !gameState.bombExploded) { showBonusDialog(false); } }, js_vars.waiting_delay_seconds * js_vars.ms_per_second); } } function hideWaitingDialog() { const dialog = docQuerySelectorStrict('#waiting-dialog'); dialog.close(); } function initGame() { createGrid(); docQuerySelectorStrict('#close-dialog-ok').addEventListener('click', function() { docQuerySelectorStrict('#close-dialog').close(); }); if (isFancyMode) { const startButton = docQuerySelectorStrict('#start-button'); startButton.addEventListener('click', startAutoCollection); if (isStopStartMode) { const pauseButton = docQuerySelectorStrict('#pause-button'); pauseButton.addEventListener('click', function() { stopAutoCollection(); gameState.isPaused = true; docQuerySelectorStrict('#pause-button').classList.add('hidden'); docQuerySelectorStrict('#restart-button').classList.remove('hidden'); }); const restartButton = docQuerySelectorStrict('#restart-button'); restartButton.addEventListener('click', function() { startAutoCollection(); docQuerySelectorStrict('#restart-button').classList.add('hidden'); docQuerySelectorStrict('#pause-button').classList.remove('hidden'); }); const finalStopButton = docQuerySelectorStrict('#final-stop-button'); finalStopButton.addEventListener('click', finalStopGame); } else { const stopButton = docQuerySelectorStrict('#stop-button'); stopButton.addEventListener('click', stopGame); } } else { const stopButton = docQuerySelectorStrict('#stop-button'); stopButton.addEventListener('click', stopGame); } } function isLuckyNumber(boxNumber) { return gameState.luckyNumbers.includes(boxNumber); } function liveRecv(data) { if (data.bomb_exploded) { // Record that the bomb was collected, but DO NOT reveal yet gameState.bombExploded = true; } if (data.bomb_protected) { gameState.bombProtected = true; } } function onDOMReady() { initGame(); updateTimerDisplay(); } function showBonusDialog() { const dialog = docQuerySelectorStrict('#bonus-dialog'); const messageEle = docQuerySelectorStrict('#bonus-message'); if (gameState.stoppedByPlayer) { messageEle.textContent = 'You pressed stop!'; } else { messageEle.textContent = "Time's up!"; } docQuerySelectorStrict('#extra-boxes-count').textContent = js_vars.extra_boxes; docQuerySelectorStrict('#extra-boxes-accept').textContent = js_vars.extra_boxes; dialog.showModal(); const acceptBtn = docQuerySelectorStrict('#bonus-accept'); const declineBtn = docQuerySelectorStrict('#bonus-decline'); const acceptHandler = function() { acceptBtn.removeEventListener('click', acceptHandler); declineBtn.removeEventListener('click', declineHandler); handleBonusAccept(dialog); }; const declineHandler = function() { acceptBtn.removeEventListener('click', acceptHandler); declineBtn.removeEventListener('click', declineHandler); handleBonusDecline(dialog); }; acceptBtn.addEventListener('click', acceptHandler); declineBtn.addEventListener('click', declineHandler); } function showCloseDialog() { const dialog = docQuerySelectorStrict('#close-dialog'); const messageEle = docQuerySelectorStrict('#close-dialog-message'); if (dialog.open) return; const randomIndex = Math.floor(Math.random() * closeMessages.length); messageEle.textContent = closeMessages[randomIndex]; dialog.showModal(); setTimeout(() => { if (dialog.open) dialog.close(); }, 2500); } function showLuckyResultDialog() { const dialog = docQuerySelectorStrict('#lucky-result-dialog'); const titleEle = docQuerySelectorStrict('#lucky-result-title'); const messageEle = docQuerySelectorStrict('#lucky-result-message'); if (gameState.bombProtected) { titleEle.textContent = 'Lucky Numbers Saved You!'; messageEle.textContent = 'The bomb was at position ' + gameState.bombPosition + ', which was one of your lucky numbers. You are protected!'; } else { titleEle.textContent = 'Results'; messageEle.textContent = 'The bomb was at position ' + gameState.bombPosition + '.'; } dialog.showModal(); docQuerySelectorStrict('#lucky-result-ok').addEventListener('click', function() { dialog.close(); docQuerySelectorStrict('#form').submit(); }); } function showStopStartButtons() { docQuerySelectorStrict('#start-button').classList.add('hidden'); docQuerySelectorStrict('#pause-button').classList.remove('hidden'); docQuerySelectorStrict('#restart-button').classList.remove('hidden'); docQuerySelectorStrict('#final-stop-button').classList.remove('hidden'); } function showWaitingDialog() { const dialog = docQuerySelectorStrict('#waiting-dialog'); dialog.showModal(); } function startAutoCollection() { if (gameState.isCollecting || gameState.gameEnded) { return; } if (!gameState.hasStartedOnce) { gameState.hasStartedOnce = true; gameState.startTime = Date.now(); startTimer(); if (isStopStartMode) { showStopStartButtons(); } } gameState.isCollecting = true; gameState.isPaused = false; if (!isStopStartMode) { docQuerySelectorStrict('#start-button').classList.add('hidden'); docQuerySelectorStrict('#stop-button').classList.remove('hidden'); } const collectNext = function() { if (gameState.boxesCollected < gameState.gridSize && !gameState.gameEnded && gameState.isCollecting) { const nextBox = gameState.boxesCollected + 1; collectBox(nextBox); const currentSpeed = getCurrentSpeed(); const msPerBox = js_vars.ms_per_second / currentSpeed; gameState.collectionInterval = setTimeout(collectNext, msPerBox); } else { stopAutoCollection(); } }; const initialSpeed = getCurrentSpeed(); const msPerBox = js_vars.ms_per_second / initialSpeed; gameState.collectionInterval = setTimeout(collectNext, msPerBox); } function startTimer() { if (gameState.timerInterval) { return; } updateTimerDisplay(); gameState.timerInterval = setInterval(function() { gameState.timeRemaining--; updateTimerDisplay(); if (gameState.timeRemaining <= 0) { clearInterval(gameState.timerInterval); gameState.timerInterval = null; handleTimeout(); } }, js_vars.ms_per_second); } function stopAutoCollection() { if (gameState.collectionInterval) { clearTimeout(gameState.collectionInterval); gameState.collectionInterval = null; } gameState.isCollecting = false; } function stopGame() { if (!gameState.gameEnded) { if (isFancyMode) { stopAutoCollection(); gameState.stoppedByPlayer = true; liveSend({type: 'stop'}); // SHOW WAITING FIRST showWaitingDialog(); setTimeout(function() { hideWaitingDialog(); // THEN show close dialog if conditions met checkAndShowCloseDialog(); setTimeout(function() { revealBombResult(); }, 2600); // allow close dialog to display }, js_vars.waiting_delay_seconds * js_vars.ms_per_second); } else { if (!gameState.hasStartedOnce) { gameState.hasStartedOnce = true; startTimer(); } endGame(); } } } function updateTimerDisplay() { const timerEle = docQuerySelectorStrict('#timer-display'); const time = Math.floor(gameState.timeRemaining); // <--- force integer const minutes = Math.floor(time / js_vars.seconds_per_minute); const seconds = time % js_vars.seconds_per_minute; // pad seconds always to 2 digits const secondsStr = String(seconds).padStart(2, '0'); timerEle.textContent = 'Time: ' + minutes + ':' + secondsStr; } //additional function revealBombResult() { const bombBox = docQuerySelectorStrict( `.grid-box[data-box-number="${gameState.bombPosition}"]` ); if (gameState.bombExploded) { bombBox.classList.add('bomb'); bombBox.textContent = '💣'; setTimeout(function () { if (gameState.variant === 'lucky') { showLuckyResultDialog(); } else { alert('BOOM! The bomb was in the boxes you collected.'); endGame(); } }, js_vars.bomb_reveal_delay_ms); } else { bombBox.classList.add('safe'); setTimeout(function () { showBonusDialog(true); }, js_vars.bomb_reveal_delay_ms); } } // OTAI SECTION: footer window.addEventListener('DOMContentLoaded', onDOMReady);