Add certificate template, shield logo, audio tracks, and enhance game UI/UX
This commit is contained in:
+352
-107
@@ -15,6 +15,10 @@ class SafetyGame {
|
||||
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'),
|
||||
@@ -34,10 +38,16 @@ class SafetyGame {
|
||||
// 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
|
||||
@@ -104,14 +114,15 @@ class SafetyGame {
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Start Game from Intro (Instantaneous Transition, Zero Delay)
|
||||
// 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);
|
||||
this.loadScenario(0, false);
|
||||
this.switchScreen('game');
|
||||
this.triggerStartCountdown(() => this.startTimer());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -146,7 +157,8 @@ class SafetyGame {
|
||||
this.particles.stopMode();
|
||||
|
||||
if (this.currentScenarioIndex < SCENARIO_DATA.length - 1) {
|
||||
this.loadScenario(this.currentScenarioIndex + 1);
|
||||
this.loadScenario(this.currentScenarioIndex + 1, false);
|
||||
this.triggerStartCountdown(() => this.startTimer());
|
||||
} else {
|
||||
this.showFinalSummary();
|
||||
}
|
||||
@@ -161,17 +173,31 @@ class SafetyGame {
|
||||
this.scenarioCompleted = [false, false];
|
||||
this.switchScreen('game');
|
||||
window.soundEngine.startBackgroundMusic();
|
||||
this.loadScenario(0);
|
||||
this.loadScenario(0, false);
|
||||
this.triggerStartCountdown(() => this.startTimer());
|
||||
});
|
||||
}
|
||||
|
||||
// Print Certificate
|
||||
// Certificate Viewer Modal & Print Handlers
|
||||
if (this.dom.btnPrintCertificate) {
|
||||
this.dom.btnPrintCertificate.addEventListener('click', () => {
|
||||
const dateEl = document.getElementById('cert-date-val');
|
||||
if (dateEl) {
|
||||
dateEl.textContent = new Date().toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
}
|
||||
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();
|
||||
});
|
||||
}
|
||||
@@ -243,14 +269,31 @@ class SafetyGame {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.dom.toggleAmbientHum) {
|
||||
this.dom.toggleAmbientHum.addEventListener('change', (e) => {
|
||||
window.soundEngine.setAmbientEnabled(e.target.checked);
|
||||
// Fullscreen Mode Toggle
|
||||
if (this.dom.btnFullscreenToggle) {
|
||||
this.dom.btnFullscreenToggle.addEventListener('click', () => {
|
||||
this.toggleFullscreen();
|
||||
});
|
||||
}
|
||||
|
||||
// Keyboard Shortcuts (1-9 and 0 for 10 Locker items, Space for Start Task, Esc to close modals)
|
||||
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');
|
||||
@@ -260,6 +303,12 @@ class SafetyGame {
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -276,6 +325,8 @@ class SafetyGame {
|
||||
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();
|
||||
}
|
||||
@@ -283,17 +334,206 @@ class SafetyGame {
|
||||
});
|
||||
}
|
||||
|
||||
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.dom.screenStart.classList.add('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.dom.screenSummary.classList.add('active');
|
||||
if (screenName === 'summary') {
|
||||
this.stopTimer();
|
||||
this.dom.screenSummary.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
loadScenario(index) {
|
||||
// =========================================================================
|
||||
// 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];
|
||||
@@ -303,6 +543,14 @@ class SafetyGame {
|
||||
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');
|
||||
@@ -483,9 +731,6 @@ class SafetyGame {
|
||||
|
||||
return `
|
||||
<div class="ppe-item-card ${isEquipped ? 'equipped' : ''}" data-item-id="${item.id}" data-slot="${item.slot}" title="${item.spec}">
|
||||
<div class="item-toggle-indicator">
|
||||
<span class="radio-check-mark">${isEquipped ? '✓' : ''}</span>
|
||||
</div>
|
||||
<div class="item-icon-box">${iconContent}</div>
|
||||
<span class="item-name">${item.name}</span>
|
||||
</div>
|
||||
@@ -610,91 +855,63 @@ class SafetyGame {
|
||||
}
|
||||
|
||||
handleSafeSuccess() {
|
||||
// Start Task Simulation SFX & Particles
|
||||
if (this.currentScenario.id === 'grinding') {
|
||||
window.soundEngine.startGrinderSFX();
|
||||
this.particles.startMode('sparks');
|
||||
} else {
|
||||
window.soundEngine.startLiquidTransferSFX();
|
||||
this.particles.startMode('chemical');
|
||||
this.stopTimer();
|
||||
this.particles.clear();
|
||||
this.particles.stopMode();
|
||||
window.soundEngine.stopAllTaskSFX();
|
||||
|
||||
window.soundEngine.playSuccessChime();
|
||||
window.soundEngine.playSafetyStarSound();
|
||||
|
||||
// Award points (100 on first try, 80 on retry)
|
||||
const points = this.scenarioFirstAttempt ? 100 : 80;
|
||||
this.score += points;
|
||||
this.scenarioCompleted[this.currentScenarioIndex] = true;
|
||||
this.updateHUD();
|
||||
|
||||
// Show Success Modal Overlay
|
||||
if (this.dom.successTitle) {
|
||||
if (this.currentScenario.id === 'grinding') {
|
||||
this.dom.successTitle.innerHTML = `<span class="title-green">SAFE</span> <span class="title-white">TO START</span>`;
|
||||
} else {
|
||||
this.dom.successTitle.innerHTML = `<span class="title-green">SAFE</span> <span class="title-white">TO PROCEED</span>`;
|
||||
}
|
||||
}
|
||||
if (this.dom.successDesc) this.dom.successDesc.textContent = this.currentScenario.successMessage.body;
|
||||
if (this.dom.awardedPointsText) this.dom.awardedPointsText.textContent = `+${points} Safety Points`;
|
||||
|
||||
if (this.dom.btnNextScenarioText) {
|
||||
if (this.currentScenarioIndex === SCENARIO_DATA.length - 1) {
|
||||
this.dom.btnNextScenarioText.textContent = 'VIEW FINAL RESULTS ➔';
|
||||
} else {
|
||||
this.dom.btnNextScenarioText.textContent = 'NEXT SCENARIO ➔';
|
||||
}
|
||||
}
|
||||
|
||||
// Play Success Chime & Star sound after short operational run
|
||||
setTimeout(() => {
|
||||
window.soundEngine.stopAllTaskSFX();
|
||||
this.particles.stopMode();
|
||||
|
||||
window.soundEngine.playSuccessChime();
|
||||
window.soundEngine.playSafetyStarSound();
|
||||
|
||||
// Award points (100 on first try, 80 on retry)
|
||||
const points = this.scenarioFirstAttempt ? 100 : 80;
|
||||
this.score += points;
|
||||
this.scenarioCompleted[this.currentScenarioIndex] = true;
|
||||
this.updateHUD();
|
||||
|
||||
// Show Success Modal Overlay
|
||||
if (this.dom.successTitle) this.dom.successTitle.textContent = this.currentScenario.successMessage.title;
|
||||
if (this.dom.successDesc) this.dom.successDesc.textContent = this.currentScenario.successMessage.body;
|
||||
if (this.dom.awardedPointsText) this.dom.awardedPointsText.textContent = `+${points} Safety Points ⭐`;
|
||||
|
||||
if (this.dom.btnNextScenarioText) {
|
||||
if (this.currentScenarioIndex === SCENARIO_DATA.length - 1) {
|
||||
this.dom.btnNextScenarioText.textContent = 'VIEW FINAL RESULTS ➔';
|
||||
} else {
|
||||
this.dom.btnNextScenarioText.textContent = 'NEXT SCENARIO ➔';
|
||||
}
|
||||
}
|
||||
|
||||
if (this.dom.successOverlay) this.dom.successOverlay.classList.add('active');
|
||||
}, 1200);
|
||||
if (this.dom.successOverlay) this.dom.successOverlay.classList.add('active');
|
||||
}
|
||||
|
||||
handleUnsafeConsequence(allMissingChecks, allEquippedDistractors) {
|
||||
this.stopTimer();
|
||||
this.scenarioFirstAttempt = false;
|
||||
this.particles.clear();
|
||||
this.particles.stopMode();
|
||||
|
||||
// Start task machine momentarily
|
||||
if (this.currentScenario.id === 'grinding') {
|
||||
window.soundEngine.startGrinderSFX();
|
||||
this.particles.startMode('sparks');
|
||||
} else {
|
||||
window.soundEngine.startLiquidTransferSFX();
|
||||
this.particles.startMode('chemical');
|
||||
}
|
||||
|
||||
// Determine target position for incoming projectile
|
||||
const avatarCoords = this.avatar.getAvatarCenterCoordinates();
|
||||
const stageWidth = this.dom.sceneStage ? this.dom.sceneStage.clientWidth : 600;
|
||||
const stageHeight = this.dom.sceneStage ? this.dom.sceneStage.clientHeight : 450;
|
||||
const originX = stageWidth * 0.72;
|
||||
const originY = stageHeight * 0.65;
|
||||
|
||||
const hasEyeFaceIssue = allMissingChecks.some(chk => chk.slot === 'eyes' || chk.slot === 'head');
|
||||
const targetX = hasEyeFaceIssue ? avatarCoords.eyeX : avatarCoords.handX;
|
||||
const targetY = hasEyeFaceIssue ? avatarCoords.eyeY : avatarCoords.handY;
|
||||
|
||||
setTimeout(() => {
|
||||
window.soundEngine.playWhooshImpact();
|
||||
this.particles.spawnHazardProjectile(
|
||||
this.currentScenario.id === 'grinding' ? 'spark' : 'chemical_drop',
|
||||
originX, originY, targetX, targetY
|
||||
);
|
||||
}, 400);
|
||||
|
||||
// Store consequence context for freeze overlay
|
||||
this.activeConsequenceData = { allMissingChecks, allEquippedDistractors };
|
||||
this.handleFreezeFrameAlert();
|
||||
}
|
||||
|
||||
handleFreezeFrameAlert(projectile) {
|
||||
// 1. Freeze particle engine & stop machine motors
|
||||
this.particles.setFrozen(true);
|
||||
this.particles.clear();
|
||||
this.particles.setFrozen(false);
|
||||
this.particles.stopMode();
|
||||
window.soundEngine.stopGrinderSFX();
|
||||
window.soundEngine.stopLiquidTransferSFX();
|
||||
|
||||
// 2. Duck background music
|
||||
// Duck background music
|
||||
window.soundEngine.duckMusic(true);
|
||||
|
||||
// 3. Play warning tones
|
||||
// Play warning tones
|
||||
window.soundEngine.playWarningTone();
|
||||
window.soundEngine.playCriticalStopAlert();
|
||||
|
||||
@@ -712,22 +929,25 @@ class SafetyGame {
|
||||
|
||||
// 4. Update Header & Badges
|
||||
if (this.dom.hazardFlasherBadge) {
|
||||
this.dom.hazardFlasherBadge.textContent = `⚠️ STOP! ${totalIssues} SAFETY ${totalIssues === 1 ? 'HAZARD' : 'HAZARDS'} DETECTED`;
|
||||
this.dom.hazardFlasherBadge.innerHTML = `
|
||||
<span class="flasher-icon">⚠️</span>
|
||||
<span class="flasher-text">STOP! ${totalIssues} SAFETY ${totalIssues === 1 ? 'HAZARD' : 'HAZARDS'} DETECTED</span>
|
||||
`;
|
||||
}
|
||||
if (this.dom.missingCountNum) this.dom.missingCountNum.textContent = totalIssues;
|
||||
|
||||
if (totalIssues === 1) {
|
||||
if (allEquippedDistractors.length > 0) {
|
||||
const d = allEquippedDistractors[0];
|
||||
if (this.dom.consequenceTitle) this.dom.consequenceTitle.textContent = "Incorrect / Ineffective PPE Detected";
|
||||
if (this.dom.consequenceTitle) this.dom.consequenceTitle.innerHTML = `<span class="title-red">INCORRECT</span> <span class="title-white">PPE DETECTED</span>`;
|
||||
if (this.dom.consequenceDesc) this.dom.consequenceDesc.textContent = d.rejectReason;
|
||||
} else {
|
||||
const m = allMissingChecks[0];
|
||||
if (this.dom.consequenceTitle) this.dom.consequenceTitle.textContent = m.consequenceTitle;
|
||||
if (this.dom.consequenceTitle) this.dom.consequenceTitle.innerHTML = `<span class="title-red">CRITICAL</span> <span class="title-white">HAZARD DETECTED</span>`;
|
||||
if (this.dom.consequenceDesc) this.dom.consequenceDesc.textContent = m.consequenceDesc;
|
||||
}
|
||||
} else {
|
||||
if (this.dom.consequenceTitle) this.dom.consequenceTitle.textContent = "Critical Protection Gaps Detected";
|
||||
if (this.dom.consequenceTitle) this.dom.consequenceTitle.innerHTML = `<span class="title-red">CRITICAL</span> <span class="title-white">PROTECTION GAPS DETECTED</span>`;
|
||||
if (this.dom.consequenceDesc) this.dom.consequenceDesc.textContent = "Operating without complete PPE exposes multiple body zones to direct, severe workplace injuries. All required safety equipment must be equipped before starting.";
|
||||
}
|
||||
|
||||
@@ -735,31 +955,49 @@ class SafetyGame {
|
||||
let cardsHtml = '';
|
||||
|
||||
allMissingChecks.forEach(chk => {
|
||||
const icon = slotIcons[chk.slot] || '⚠️';
|
||||
const reqItem = this.currentScenario.lockerItems.find(it => it.id === chk.requiredItem);
|
||||
const iconImgHtml = reqItem && reqItem.iconImg
|
||||
? `<img src="${reqItem.iconImg}" alt="${reqItem.name}" class="hazard-card-ppe-img">`
|
||||
: `<span class="hazard-card-fallback-icon">${slotIcons[chk.slot] || '⚠️'}</span>`;
|
||||
|
||||
const slotCap = chk.slot ? chk.slot.charAt(0).toUpperCase() + chk.slot.slice(1) : 'Body';
|
||||
|
||||
cardsHtml += `
|
||||
<div class="hazard-card-item">
|
||||
<div class="card-icon">${icon}</div>
|
||||
<div class="card-body">
|
||||
<div class="card-header-line">
|
||||
<span class="card-item-name">${chk.missingLabel}</span>
|
||||
<span class="card-slot-badge">${chk.slot} - Missing</span>
|
||||
<div class="cyber-hazard-card">
|
||||
<div class="hazard-card-icon-box">
|
||||
${iconImgHtml}
|
||||
</div>
|
||||
<div class="hazard-card-content">
|
||||
<div class="hazard-card-top-row">
|
||||
<span class="hazard-card-name">${chk.missingLabel || (reqItem ? reqItem.name : 'Missing PPE')}</span>
|
||||
<span class="hazard-card-badge missing-badge">
|
||||
<span class="badge-exclamation">!</span> ${slotCap} - Missing
|
||||
</span>
|
||||
</div>
|
||||
<span class="card-hazard-text">${chk.consequenceDesc}</span>
|
||||
<p class="hazard-card-desc">${chk.consequenceDesc}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
allEquippedDistractors.forEach(dist => {
|
||||
const iconImgHtml = dist.iconImg
|
||||
? `<img src="${dist.iconImg}" alt="${dist.name}" class="hazard-card-ppe-img">`
|
||||
: `<span class="hazard-card-fallback-icon">🚫</span>`;
|
||||
|
||||
cardsHtml += `
|
||||
<div class="hazard-card-item distractor-card">
|
||||
<div class="card-icon">🚫</div>
|
||||
<div class="card-body">
|
||||
<div class="card-header-line">
|
||||
<span class="card-item-name">REMOVE: ${dist.name}</span>
|
||||
<span class="card-slot-badge">Unsafe Gear</span>
|
||||
<div class="cyber-hazard-card distractor-card">
|
||||
<div class="hazard-card-icon-box">
|
||||
${iconImgHtml}
|
||||
</div>
|
||||
<div class="hazard-card-content">
|
||||
<div class="hazard-card-top-row">
|
||||
<span class="hazard-card-name">REMOVE: ${dist.name}</span>
|
||||
<span class="hazard-card-badge distractor-badge">
|
||||
<span class="badge-exclamation">!</span> Unsafe Gear
|
||||
</span>
|
||||
</div>
|
||||
<span class="card-hazard-text">${dist.rejectReason}</span>
|
||||
<p class="hazard-card-desc">${dist.rejectReason}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -801,6 +1039,13 @@ class SafetyGame {
|
||||
const rows = this.dom.coverageGrid.querySelectorAll('.status-row-item');
|
||||
rows.forEach(row => row.classList.remove('alert-missing'));
|
||||
}
|
||||
|
||||
if (this.timerSeconds <= 0) {
|
||||
this.timerSeconds = 15; // Extra 15s to fix PPE
|
||||
this.resumeTimer();
|
||||
} else {
|
||||
this.resumeTimer();
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user