/** * PPE SAFETY CHALLENGE - CORE GAME ENGINE & STATE CONTROLLER * "Choose Right. Work Safe." */ class SafetyGame { constructor() { this.currentScenarioIndex = 0; // 0 for Grinding, 1 for Chemical this.currentScenario = null; this.score = 0; this.scenarioFirstAttempt = true; this.scenarioCompleted = [false, false]; this.equippedItems = new Set(); this.avatar = null; this.particles = null; // 30-Second Countdown Timer State this.timerSeconds = 30; this.timerInterval = null; // DOM Elements Cache this.dom = { screenStart: document.getElementById('screen-start'), screenGame: document.getElementById('screen-game'), screenSummary: document.getElementById('screen-summary'), btnStartGame: document.getElementById('btn-start-game'), btnTestSafety: document.getElementById('btn-test-safety'), btnTestSafetyText: document.getElementById('btn-test-safety-text'), btnCheckPpe: document.getElementById('btn-check-ppe'), btnFixPpe: document.getElementById('btn-fix-ppe'), btnNextScenario: document.getElementById('btn-next-scenario'), btnNextScenarioText: document.getElementById('btn-next-scenario-text'), btnReplay: document.getElementById('btn-replay'), btnPrintCertificate: document.getElementById('btn-print-certificate'), // HUD hudScore: document.getElementById('hud-score'), hudStars: document.getElementById('hud-stars'), hudTimerBox: document.getElementById('hud-timer-box'), hudTimerVal: document.getElementById('hud-timer-val'), countdownOverlay: document.getElementById('countdown-overlay'), countdownDigit: document.getElementById('countdown-digit'), countdownSubTip: document.getElementById('countdown-sub-tip'), stepPill1: document.getElementById('step-pill-1'), stepPill2: document.getElementById('step-pill-2'), btnSoundToggle: document.getElementById('btn-sound-toggle'), btnMusicToggle: document.getElementById('btn-music-toggle'), btnFullscreenToggle: document.getElementById('btn-fullscreen-toggle'), btnAudioSettings: document.getElementById('btn-audio-settings'), // Scene Info & Hazards sceneBadge: document.getElementById('scene-badge'), sceneTitle: document.getElementById('scene-title'), sceneDesc: document.getElementById('scene-desc'), hazardList: document.getElementById('hazard-list'), coverageGrid: document.getElementById('coverage-grid'), sceneStage: document.getElementById('scene-stage'), sceneBackdrop: document.getElementById('scene-backdrop'), // Locker lockerGrid: document.getElementById('ppe-locker-grid'), lockerGuideText: document.getElementById('locker-guide-text'), // Overlays consequenceOverlay: document.getElementById('consequence-overlay'), hazardFlasherBadge: document.getElementById('hazard-flasher-badge'), consequenceTitle: document.getElementById('consequence-title'), consequenceDesc: document.getElementById('consequence-desc'), missingCountNum: document.getElementById('missing-count-num'), consequenceHazardList: document.getElementById('consequence-hazard-list'), successOverlay: document.getElementById('success-overlay'), successTitle: document.getElementById('success-title'), successDesc: document.getElementById('success-desc'), awardedPointsText: document.getElementById('awarded-points-text'), // Summary Screen summaryScore: document.getElementById('summary-score'), summaryPercentage: document.getElementById('summary-percentage'), summaryStars: document.getElementById('summary-stars'), summaryRank: document.getElementById('summary-rank'), // Audio Modal modalAudio: document.getElementById('modal-audio'), btnCloseAudioModal: document.getElementById('btn-close-audio-modal'), btnSaveAudio: document.getElementById('btn-save-audio'), toggleMasterMute: document.getElementById('toggle-master-mute'), sliderMusicVol: document.getElementById('slider-music-vol'), sliderSfxVol: document.getElementById('slider-sfx-vol'), toggleAmbientHum: document.getElementById('toggle-ambient-hum'), musicVolNum: document.getElementById('music-vol-num'), sfxVolNum: document.getElementById('sfx-vol-num') }; this.init(); } init() { this.avatar = new WorkerAvatar('avatar-container'); this.particles = new ParticleEngine('stage-fx-canvas'); this.bindEvents(); this.updateHUD(); // Enable background music starting from home page on first gesture window.soundEngine.enableAudioOnFirstGesture(); // Listen for freeze frame event from particle collision window.addEventListener('hazard-freeze-triggered', (e) => { this.handleFreezeFrameAlert(e.detail.projectile); }); } bindEvents() { // Start Game from Intro (Instantaneous Transition with 3,2,1,GO Central Countdown) if (this.dom.btnStartGame) { this.dom.btnStartGame.addEventListener('click', () => { window.soundEngine.ensureContext(); window.soundEngine.playStartIntroSting(); window.soundEngine.startBackgroundMusic(); this.loadScenario(0, false); this.switchScreen('game'); this.triggerStartCountdown(() => this.startTimer()); }); } // Check My PPE Button (Safe Preview & Validation Feedback) if (this.dom.btnCheckPpe) { this.dom.btnCheckPpe.addEventListener('click', () => { this.checkPpeStatus(); }); } // Start Task Safety Test Button if (this.dom.btnTestSafety) { this.dom.btnTestSafety.addEventListener('click', () => { this.executeSafetyTest(); }); } // Fix PPE (Dismiss Consequence Modal & Return to Edit) if (this.dom.btnFixPpe) { this.dom.btnFixPpe.addEventListener('click', () => { window.soundEngine.playButtonClick(); this.dismissConsequenceModal(); }); } // Next Scenario / View Summary Button if (this.dom.btnNextScenario) { this.dom.btnNextScenario.addEventListener('click', () => { window.soundEngine.playButtonClick(); window.soundEngine.stopAllTaskSFX(); this.dom.successOverlay.classList.remove('active'); this.particles.stopMode(); if (this.currentScenarioIndex < SCENARIO_DATA.length - 1) { this.loadScenario(this.currentScenarioIndex + 1, false); this.triggerStartCountdown(() => this.startTimer()); } else { this.showFinalSummary(); } }); } // Replay Challenge if (this.dom.btnReplay) { this.dom.btnReplay.addEventListener('click', () => { window.soundEngine.playButtonClick(); this.score = 0; this.scenarioCompleted = [false, false]; this.switchScreen('game'); window.soundEngine.startBackgroundMusic(); this.loadScenario(0, false); this.triggerStartCountdown(() => this.startTimer()); }); } // Certificate Viewer Modal & Print Handlers if (this.dom.btnPrintCertificate) { this.dom.btnPrintCertificate.addEventListener('click', () => { window.soundEngine.playButtonClick(); const certModal = document.getElementById('modal-certificate'); if (certModal) certModal.classList.add('active'); }); } const btnCloseCert = document.getElementById('btn-close-cert-modal'); if (btnCloseCert) { btnCloseCert.addEventListener('click', () => { const certModal = document.getElementById('modal-certificate'); if (certModal) certModal.classList.remove('active'); }); } const btnPrintAction = document.getElementById('btn-print-cert-action'); if (btnPrintAction) { btnPrintAction.addEventListener('click', () => { window.print(); }); } // Audio & Settings Header Controls (Toggle Mute / Unmute continuously) if (this.dom.btnSoundToggle) { this.dom.btnSoundToggle.addEventListener('click', () => { window.soundEngine.ensureContext(); const isMuted = window.soundEngine.toggleMasterMute(); const iconSpan = this.dom.btnSoundToggle.querySelector('span') || this.dom.btnSoundToggle; iconSpan.textContent = isMuted ? '๐' : '๐'; if (this.dom.toggleMasterMute) this.dom.toggleMasterMute.checked = !isMuted; }); } if (this.dom.btnMusicToggle) { this.dom.btnMusicToggle.addEventListener('click', () => { window.soundEngine.ensureContext(); const currentVol = window.soundEngine.musicVolume; const newVol = currentVol > 0.01 ? 0 : 0.25; window.soundEngine.setMusicVolume(newVol); const iconSpan = this.dom.btnMusicToggle.querySelector('span') || this.dom.btnMusicToggle; iconSpan.textContent = newVol > 0 ? '๐ต' : '๐'; if (this.dom.sliderMusicVol) this.dom.sliderMusicVol.value = Math.round(newVol * 100); if (this.dom.musicVolNum) this.dom.musicVolNum.textContent = `${Math.round(newVol * 100)}%`; }); } if (this.dom.btnAudioSettings) { this.dom.btnAudioSettings.addEventListener('click', () => { if (this.dom.modalAudio) this.dom.modalAudio.classList.add('active'); }); } if (this.dom.btnCloseAudioModal) { this.dom.btnCloseAudioModal.addEventListener('click', () => { if (this.dom.modalAudio) this.dom.modalAudio.classList.remove('active'); }); } if (this.dom.btnSaveAudio) { this.dom.btnSaveAudio.addEventListener('click', () => { if (this.dom.modalAudio) this.dom.modalAudio.classList.remove('active'); }); } if (this.dom.toggleMasterMute) { this.dom.toggleMasterMute.addEventListener('change', (e) => { window.soundEngine.setMasterMute(!e.target.checked); if (this.dom.btnSoundToggle) { this.dom.btnSoundToggle.querySelector('span').textContent = e.target.checked ? '๐' : '๐'; } }); } if (this.dom.sliderMusicVol) { this.dom.sliderMusicVol.addEventListener('input', (e) => { const val = parseInt(e.target.value) / 100; if (this.dom.musicVolNum) this.dom.musicVolNum.textContent = `${e.target.value}%`; window.soundEngine.setMusicVolume(val); }); } if (this.dom.sliderSfxVol) { this.dom.sliderSfxVol.addEventListener('input', (e) => { const val = parseInt(e.target.value) / 100; if (this.dom.sfxVolNum) this.dom.sfxVolNum.textContent = `${e.target.value}%`; window.soundEngine.setSfxVolume(val); }); } // Fullscreen Mode Toggle if (this.dom.btnFullscreenToggle) { this.dom.btnFullscreenToggle.addEventListener('click', () => { this.toggleFullscreen(); }); } const onFullscreenChange = () => { const isFull = !!(document.fullscreenElement || document.webkitFullscreenElement); document.body.classList.toggle('fullscreen-mode', isFull); document.documentElement.classList.toggle('fullscreen-mode', isFull); this.updateFullscreenIcon(); }; document.addEventListener('fullscreenchange', onFullscreenChange); document.addEventListener('webkitfullscreenchange', onFullscreenChange); document.addEventListener('mozfullscreenchange', onFullscreenChange); document.addEventListener('MSFullscreenChange', onFullscreenChange); // Keyboard Shortcuts (1-9 and 0 for 10 Locker items, Space for Start Task, F for Fullscreen, Esc to close modals) window.addEventListener('keydown', (e) => { // Don't trigger game shortcuts if typing in an editable field if (e.target && (e.target.tagName === 'INPUT' || e.target.isContentEditable)) { return; } const num = parseInt(e.key); if (!isNaN(num)) { const cards = document.querySelectorAll('.ppe-item-card'); const cardIndex = num === 0 ? 9 : num - 1; if (cards[cardIndex]) { cards[cardIndex].click(); } } if ((e.key === 'f' || e.key === 'F') && !e.ctrlKey && !e.altKey && !e.metaKey) { e.preventDefault(); this.toggleFullscreen(); return; } if ((e.code === 'Space' || e.code === 'Enter') && this.dom.screenStart.classList.contains('active')) { e.preventDefault(); if (this.dom.btnStartGame) this.dom.btnStartGame.click(); return; } if (e.code === 'Space' && this.dom.screenGame.classList.contains('active')) { if (!this.dom.consequenceOverlay.classList.contains('active') && !this.dom.successOverlay.classList.contains('active')) { e.preventDefault(); this.executeSafetyTest(); } } if (e.key === 'Escape') { if (this.dom.modalAudio && this.dom.modalAudio.classList.contains('active')) { this.dom.modalAudio.classList.remove('active'); } else if (document.getElementById('modal-certificate')?.classList.contains('active')) { document.getElementById('modal-certificate').classList.remove('active'); } else if (this.dom.consequenceOverlay.classList.contains('active')) { this.dismissConsequenceModal(); } } }); } toggleFullscreen() { window.soundEngine.playButtonClick(); if (!document.fullscreenElement && !document.webkitFullscreenElement) { const elem = document.documentElement; if (elem.requestFullscreen) { elem.requestFullscreen().catch(() => {}); } else if (elem.webkitRequestFullscreen) { elem.webkitRequestFullscreen(); } } else { if (document.exitFullscreen) { document.exitFullscreen().catch(() => {}); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); } } } updateFullscreenIcon() { const isFull = !!(document.fullscreenElement || document.webkitFullscreenElement); const iconSpan = document.getElementById('fullscreen-icon'); if (iconSpan) { iconSpan.textContent = isFull ? 'โคก' : 'โถ'; } if (this.dom.btnFullscreenToggle) { this.dom.btnFullscreenToggle.title = isFull ? 'Exit Fullscreen (F)' : 'Toggle Fullscreen (F)'; if (isFull) { this.dom.btnFullscreenToggle.classList.add('fullscreen-active'); } else { this.dom.btnFullscreenToggle.classList.remove('fullscreen-active'); } } } switchScreen(screenName) { this.dom.screenStart.classList.remove('active'); this.dom.screenGame.classList.remove('active'); this.dom.screenSummary.classList.remove('active'); if (screenName === 'start') { this.stopTimer(); this.dom.screenStart.classList.add('active'); } if (screenName === 'game') this.dom.screenGame.classList.add('active'); if (screenName === 'summary') { this.stopTimer(); this.dom.screenSummary.classList.add('active'); } } // ========================================================================= // 30-SECOND COUNTDOWN TIMER CONTROLLER // ========================================================================= startTimer() { this.stopTimer(); this.timerSeconds = 30; this.updateTimerDisplay(); this.timerInterval = setInterval(() => { if (this.timerSeconds > 0) { this.timerSeconds--; this.updateTimerDisplay(); if (this.timerSeconds > 10) { // Normal clock tick sound from 30s to 11s (first 20 seconds) window.soundEngine.playTimerTick(); } else if (this.timerSeconds === 10) { // Trigger dedicated audio track for the final 10 seconds window.soundEngine.playTimerLast10sAudio(); } } else { this.stopTimer(); // Time expired: execute safety check if (this.dom.screenGame.classList.contains('active') && !this.dom.consequenceOverlay.classList.contains('active') && !this.dom.successOverlay.classList.contains('active')) { this.executeSafetyTest(); } } }, 1000); } stopTimer() { if (this.timerInterval) { clearInterval(this.timerInterval); this.timerInterval = null; } window.soundEngine.stopTimerLast10sAudio(); } pauseTimer() { this.stopTimer(); } resumeTimer() { if (this.timerSeconds > 0 && !this.timerInterval) { this.timerInterval = setInterval(() => { if (this.timerSeconds > 0) { this.timerSeconds--; this.updateTimerDisplay(); if (this.timerSeconds > 10) { window.soundEngine.playTimerTick(); } else if (this.timerSeconds === 10) { window.soundEngine.playTimerLast10sAudio(); } } else { this.stopTimer(); if (this.dom.screenGame.classList.contains('active') && !this.dom.consequenceOverlay.classList.contains('active') && !this.dom.successOverlay.classList.contains('active')) { this.executeSafetyTest(); } } }, 1000); } } updateTimerDisplay() { if (!this.dom.hudTimerVal) return; const formatted = `00:${this.timerSeconds.toString().padStart(2, '0')}`; this.dom.hudTimerVal.textContent = formatted; if (this.dom.hudTimerBox) { this.dom.hudTimerBox.classList.remove('timer-warning', 'timer-danger'); if (this.timerSeconds <= 5) { this.dom.hudTimerBox.classList.add('timer-danger'); } else if (this.timerSeconds <= 10) { this.dom.hudTimerBox.classList.add('timer-warning'); } } } // ========================================================================= // CENTRAL SCREEN START COUNTDOWN (3, 2, 1, GO!) // ========================================================================= triggerStartCountdown(onComplete) { if (!this.dom.countdownOverlay) { if (onComplete) onComplete(); return; } this.stopTimer(); this.timerSeconds = 30; this.updateTimerDisplay(); this.dom.countdownOverlay.classList.add('active'); const digitEl = this.dom.countdownDigit; const subTipEl = this.dom.countdownSubTip; const steps = [ { text: '3', tip: 'PREPARE YOUR PPE', isGo: false, duration: 1000 }, { text: '2', tip: 'IDENTIFY HAZARDS', isGo: false, duration: 1000 }, { text: '1', tip: 'EQUIP WORKER', isGo: false, duration: 1000 }, { text: 'GO!', tip: '30s TIMER STARTED', isGo: true, duration: 750 } ]; let currentStep = 0; const playNextStep = () => { if (currentStep < steps.length) { const item = steps[currentStep]; if (digitEl) { digitEl.textContent = item.text; digitEl.className = 'countdown-digit'; if (item.isGo) { digitEl.classList.add('go-text'); } // Force CSS reflow to re-trigger pop animation void digitEl.offsetWidth; digitEl.classList.add('pop'); } if (subTipEl) { subTipEl.textContent = item.tip; } // Play uploaded countdown tick audio for 3, 2, 1 every second if (item.isGo) { window.soundEngine.playCountdownGo(); } else { window.soundEngine.playCountdownTickAudio(); } currentStep++; setTimeout(playNextStep, item.duration); } else { // Complete countdown & fade out this.dom.countdownOverlay.classList.remove('active'); if (onComplete) onComplete(); } }; playNextStep(); } loadScenario(index, autoStartTimer = true) { window.soundEngine.stopAllTaskSFX(); this.currentScenarioIndex = index; this.currentScenario = SCENARIO_DATA[index]; this.scenarioFirstAttempt = true; this.equippedItems.clear(); this.avatar.setEquipped([]); this.particles.clear(); this.particles.stopMode(); if (autoStartTimer) { this.startTimer(); } else { this.stopTimer(); this.timerSeconds = 30; this.updateTimerDisplay(); } // Update Stepper HUD if (this.dom.stepPill1 && this.dom.stepPill2) { this.dom.stepPill1.className = index === 0 ? 'step-pill active' : (this.scenarioCompleted[0] ? 'step-pill completed' : 'step-pill'); this.dom.stepPill2.className = index === 1 ? 'step-pill active' : (this.scenarioCompleted[1] ? 'step-pill completed' : 'step-pill'); } // Scenario Details if (this.dom.sceneBadge) this.dom.sceneBadge.textContent = `SCENARIO ${index + 1} OF ${SCENARIO_DATA.length}`; if (this.dom.sceneTitle) this.dom.sceneTitle.textContent = this.currentScenario.title; if (this.dom.sceneDesc) this.dom.sceneDesc.textContent = this.currentScenario.environmentDesc; if (this.dom.lockerGuideText) this.dom.lockerGuideText.textContent = this.currentScenario.guideTip; // Apply stage scene class for background framing if (this.dom.sceneStage) { this.dom.sceneStage.className = 'scene-environment-container ' + (this.currentScenario.sceneClass || ''); } // Render Scenario Background this.renderScenarioEnvironment(this.currentScenario.id); // Render Hazards List if (this.dom.hazardList) { this.dom.hazardList.innerHTML = this.currentScenario.hazards.map(h => `
${chk.consequenceDesc}
${dist.rejectReason}