Initial commit: Safety Game
This commit is contained in:
+629
@@ -0,0 +1,629 @@
|
||||
/**
|
||||
* 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;
|
||||
|
||||
// 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'),
|
||||
btnClearPpe: document.getElementById('btn-clear-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'),
|
||||
stepPill1: document.getElementById('step-pill-1'),
|
||||
stepPill2: document.getElementById('step-pill-2'),
|
||||
|
||||
// Scene
|
||||
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'),
|
||||
statusLight: document.getElementById('status-light'),
|
||||
statusText: document.getElementById('status-text'),
|
||||
audioCaption: document.getElementById('audio-caption'),
|
||||
|
||||
// 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
|
||||
summaryScore: document.getElementById('summary-score'),
|
||||
summaryPercentage: document.getElementById('summary-percentage'),
|
||||
summaryStars: document.getElementById('summary-stars'),
|
||||
summaryRank: document.getElementById('summary-rank'),
|
||||
|
||||
// Audio Modal
|
||||
btnAudioSettings: document.getElementById('btn-audio-settings'),
|
||||
btnRestart: document.getElementById('btn-restart'),
|
||||
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();
|
||||
|
||||
// Listen for freeze frame event from particle collision
|
||||
window.addEventListener('hazard-freeze-triggered', (e) => {
|
||||
this.handleFreezeFrameAlert(e.detail.projectile);
|
||||
});
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Start Game
|
||||
this.dom.btnStartGame.addEventListener('click', () => {
|
||||
window.soundEngine.ensureContext();
|
||||
window.soundEngine.playStartIntroSting();
|
||||
this.switchScreen('game');
|
||||
|
||||
setTimeout(() => {
|
||||
window.soundEngine.startBackgroundMusic();
|
||||
this.loadScenario(0);
|
||||
}, 900);
|
||||
});
|
||||
|
||||
// Test Worker Safety
|
||||
this.dom.btnTestSafety.addEventListener('click', () => {
|
||||
this.executeSafetyTest();
|
||||
});
|
||||
|
||||
// Clear PPE
|
||||
this.dom.btnClearPpe.addEventListener('click', () => {
|
||||
window.soundEngine.playDeselectSFX();
|
||||
this.equippedItems.clear();
|
||||
this.avatar.setEquipped([]);
|
||||
this.renderLockerItems();
|
||||
this.updateCoverageIndicators();
|
||||
});
|
||||
|
||||
// Fix PPE (From Consequence Modal)
|
||||
this.dom.btnFixPpe.addEventListener('click', () => {
|
||||
window.soundEngine.playButtonClick();
|
||||
this.dismissConsequenceModal();
|
||||
});
|
||||
|
||||
// Next Scenario
|
||||
this.dom.btnNextScenario.addEventListener('click', () => {
|
||||
window.soundEngine.playButtonClick();
|
||||
this.dom.successOverlay.classList.remove('active');
|
||||
this.particles.stopMode();
|
||||
|
||||
if (this.currentScenarioIndex < SCENARIO_DATA.length - 1) {
|
||||
this.loadScenario(this.currentScenarioIndex + 1);
|
||||
} else {
|
||||
this.showFinalSummary();
|
||||
}
|
||||
});
|
||||
|
||||
// Replay Challenge
|
||||
this.dom.btnReplay.addEventListener('click', () => {
|
||||
window.soundEngine.playButtonClick();
|
||||
this.score = 0;
|
||||
this.scenarioCompleted = [false, false];
|
||||
this.switchScreen('game');
|
||||
window.soundEngine.startBackgroundMusic();
|
||||
this.loadScenario(0);
|
||||
});
|
||||
|
||||
// Print Certificate
|
||||
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.print();
|
||||
});
|
||||
|
||||
// Restart Button (Top Bar)
|
||||
this.dom.btnRestart.addEventListener('click', () => {
|
||||
if (confirm("Restart the PPE Safety Challenge from the beginning?")) {
|
||||
window.soundEngine.playButtonClick();
|
||||
this.score = 0;
|
||||
this.scenarioCompleted = [false, false];
|
||||
this.switchScreen('start');
|
||||
window.soundEngine.stopBackgroundMusic();
|
||||
window.soundEngine.stopAmbient();
|
||||
window.soundEngine.stopGrinderSFX();
|
||||
window.soundEngine.stopLiquidTransferSFX();
|
||||
}
|
||||
});
|
||||
|
||||
// Audio Modal Controls
|
||||
this.dom.btnAudioSettings.addEventListener('click', () => {
|
||||
this.dom.modalAudio.classList.add('active');
|
||||
});
|
||||
|
||||
this.dom.btnCloseAudioModal.addEventListener('click', () => {
|
||||
this.dom.modalAudio.classList.remove('active');
|
||||
});
|
||||
|
||||
this.dom.btnSaveAudio.addEventListener('click', () => {
|
||||
this.dom.modalAudio.classList.remove('active');
|
||||
});
|
||||
|
||||
this.dom.toggleMasterMute.addEventListener('change', (e) => {
|
||||
window.soundEngine.setMasterMute(!e.target.checked);
|
||||
document.getElementById('audio-icon').textContent = e.target.checked ? '🔊' : '🔇';
|
||||
});
|
||||
|
||||
this.dom.sliderMusicVol.addEventListener('input', (e) => {
|
||||
const val = parseInt(e.target.value) / 100;
|
||||
this.dom.musicVolNum.textContent = `${e.target.value}%`;
|
||||
window.soundEngine.setMusicVolume(val);
|
||||
});
|
||||
|
||||
this.dom.sliderSfxVol.addEventListener('input', (e) => {
|
||||
const val = parseInt(e.target.value) / 100;
|
||||
this.dom.sfxVolNum.textContent = `${e.target.value}%`;
|
||||
window.soundEngine.setSfxVolume(val);
|
||||
});
|
||||
|
||||
this.dom.toggleAmbientHum.addEventListener('change', (e) => {
|
||||
window.soundEngine.setAmbientEnabled(e.target.checked);
|
||||
});
|
||||
|
||||
// Keyboard Shortcuts
|
||||
window.addEventListener('keydown', (e) => {
|
||||
// Numbers 1-9 to toggle PPE
|
||||
const num = parseInt(e.key);
|
||||
if (!isNaN(num) && num >= 1 && num <= 9) {
|
||||
const cards = document.querySelectorAll('.ppe-item-card');
|
||||
if (cards[num - 1]) {
|
||||
cards[num - 1].click();
|
||||
}
|
||||
}
|
||||
|
||||
// Spacebar to Test Safety
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
// Esc to close overlays/modals
|
||||
if (e.key === 'Escape') {
|
||||
if (this.dom.modalAudio.classList.contains('active')) {
|
||||
this.dom.modalAudio.classList.remove('active');
|
||||
} else if (this.dom.consequenceOverlay.classList.contains('active')) {
|
||||
this.dismissConsequenceModal();
|
||||
}
|
||||
}
|
||||
|
||||
// 'M' to toggle Audio Settings
|
||||
if (e.key === 'm' || e.key === 'M') {
|
||||
this.dom.modalAudio.classList.toggle('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 === 'game') this.dom.screenGame.classList.add('active');
|
||||
if (screenName === 'summary') this.dom.screenSummary.classList.add('active');
|
||||
}
|
||||
|
||||
loadScenario(index) {
|
||||
this.currentScenarioIndex = index;
|
||||
this.currentScenario = SCENARIO_DATA[index];
|
||||
this.scenarioFirstAttempt = true;
|
||||
this.equippedItems.clear();
|
||||
this.avatar.setEquipped([]);
|
||||
this.particles.clear();
|
||||
this.particles.stopMode();
|
||||
|
||||
// Update Stepper HUD
|
||||
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');
|
||||
|
||||
// Scene Styling
|
||||
this.dom.sceneStage.className = `scene-environment-container ${this.currentScenario.sceneClass}`;
|
||||
this.dom.sceneBadge.textContent = `SCENARIO ${index + 1} OF ${SCENARIO_DATA.length}`;
|
||||
this.dom.sceneTitle.textContent = this.currentScenario.title;
|
||||
this.dom.sceneDesc.textContent = this.currentScenario.environmentDesc;
|
||||
this.dom.lockerGuideText.textContent = this.currentScenario.guideTip;
|
||||
this.dom.audioCaption.textContent = `[Audio: ${this.currentScenario.audioCaption}]`;
|
||||
|
||||
// Status Bar
|
||||
this.dom.statusLight.className = 'status-indicator-light';
|
||||
this.dom.statusText.textContent = `TASK IDLE - EQUIP WORKER BEFORE STARTING ${this.currentScenario.taskName}`;
|
||||
|
||||
// Populate Hazard List
|
||||
this.dom.hazardList.innerHTML = this.currentScenario.hazards.map(h => `
|
||||
<li class="hazard-list-item">
|
||||
<span>${h.label}</span>
|
||||
</li>
|
||||
`).join('');
|
||||
|
||||
// Populate Locker Grid
|
||||
this.renderLockerItems();
|
||||
this.updateCoverageIndicators();
|
||||
|
||||
// Start Ambient Sound for this scenario
|
||||
window.soundEngine.setAmbientScenario(this.currentScenario.id);
|
||||
}
|
||||
|
||||
renderLockerItems() {
|
||||
this.dom.lockerGrid.innerHTML = this.currentScenario.lockerItems.map((item, idx) => {
|
||||
const isEquipped = this.equippedItems.has(item.id);
|
||||
return `
|
||||
<div class="ppe-item-card ${isEquipped ? 'equipped' : ''}" data-item-id="${item.id}" data-slot="${item.slot}" title="${item.spec}">
|
||||
<span class="item-key-badge">${idx + 1}</span>
|
||||
<div class="item-toggle-indicator">${isEquipped ? '✓' : ''}</div>
|
||||
<div class="item-icon-box">${item.icon}</div>
|
||||
<span class="item-name">${item.name}</span>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
// Bind click events on cards
|
||||
this.dom.lockerGrid.querySelectorAll('.ppe-item-card').forEach(card => {
|
||||
card.addEventListener('click', () => {
|
||||
const itemId = card.dataset.itemId;
|
||||
const slot = card.dataset.slot;
|
||||
this.toggleItem(itemId, slot);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
toggleItem(itemId, slot) {
|
||||
if (this.equippedItems.has(itemId)) {
|
||||
// Unequip
|
||||
this.equippedItems.delete(itemId);
|
||||
this.avatar.unequip(itemId);
|
||||
window.soundEngine.playDeselectSFX();
|
||||
} else {
|
||||
// If equipping, replace any existing item occupying the same slot
|
||||
const existingInSlot = this.currentScenario.lockerItems.find(
|
||||
it => it.slot === slot && this.equippedItems.has(it.id)
|
||||
);
|
||||
if (existingInSlot) {
|
||||
this.equippedItems.delete(existingInSlot.id);
|
||||
this.avatar.unequip(existingInSlot.id);
|
||||
}
|
||||
|
||||
this.equippedItems.add(itemId);
|
||||
this.avatar.equip(itemId);
|
||||
window.soundEngine.playEquipSFX();
|
||||
}
|
||||
|
||||
this.renderLockerItems();
|
||||
this.updateCoverageIndicators();
|
||||
}
|
||||
|
||||
updateCoverageIndicators() {
|
||||
const coverageItems = this.dom.coverageGrid.querySelectorAll('.coverage-item');
|
||||
coverageItems.forEach(itemEl => {
|
||||
const slot = itemEl.dataset.slot;
|
||||
const statusEl = itemEl.querySelector('.cov-status');
|
||||
|
||||
// Find if any item is equipped in this slot
|
||||
const isSlotEquipped = Array.from(this.equippedItems).some(itemId => {
|
||||
const itemObj = this.currentScenario.lockerItems.find(it => it.id === itemId);
|
||||
return itemObj && itemObj.slot === slot;
|
||||
});
|
||||
|
||||
if (isSlotEquipped) {
|
||||
statusEl.className = 'cov-status protected';
|
||||
statusEl.textContent = 'Equipped';
|
||||
} else {
|
||||
statusEl.className = 'cov-status missing';
|
||||
statusEl.textContent = 'Missing';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// SAFETY CHECK EXECUTION & CONSEQUENCE DETECTION
|
||||
// =========================================================================
|
||||
|
||||
executeSafetyTest() {
|
||||
window.soundEngine.playButtonClick();
|
||||
|
||||
// 1. Identify ALL equipped distractors / incorrect items
|
||||
const allEquippedDistractors = this.currentScenario.lockerItems.filter(
|
||||
it => this.equippedItems.has(it.id) && !it.isCorrect
|
||||
);
|
||||
|
||||
// 2. Identify ALL missing required critical gear checks
|
||||
const allMissingChecks = this.currentScenario.criticalChecks.filter(
|
||||
chk => !this.equippedItems.has(chk.requiredItem)
|
||||
);
|
||||
|
||||
const isSafe = allEquippedDistractors.length === 0 && allMissingChecks.length === 0;
|
||||
|
||||
if (isSafe) {
|
||||
// 100% SAFE SUCCESS PATH
|
||||
this.handleSafeSuccess();
|
||||
} else {
|
||||
// UNSAFE FAILURE / CONSEQUENCE NEAR-MISS PATH (Collect all issues)
|
||||
this.handleUnsafeConsequence(allMissingChecks, allEquippedDistractors);
|
||||
}
|
||||
}
|
||||
|
||||
handleSafeSuccess() {
|
||||
// 1. Update visual state
|
||||
this.dom.statusLight.className = 'status-indicator-light running';
|
||||
this.dom.statusText.textContent = `OPERATION RUNNING SAFELY - ALL HAZARDS MITIGATED`;
|
||||
|
||||
// 2. 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');
|
||||
}
|
||||
|
||||
// 3. Play Success Chime & Star sound after short operational run
|
||||
setTimeout(() => {
|
||||
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
|
||||
this.dom.successTitle.textContent = this.currentScenario.successMessage.title;
|
||||
this.dom.successDesc.textContent = this.currentScenario.successMessage.body;
|
||||
this.dom.awardedPointsText.textContent = `+${points} Safety Points ⭐`;
|
||||
|
||||
if (this.currentScenarioIndex === SCENARIO_DATA.length - 1) {
|
||||
this.dom.btnNextScenarioText.textContent = 'VIEW FINAL RESULTS ➔';
|
||||
} else {
|
||||
this.dom.btnNextScenarioText.textContent = 'NEXT SCENARIO ➔';
|
||||
}
|
||||
|
||||
this.dom.successOverlay.classList.add('active');
|
||||
}, 1200);
|
||||
}
|
||||
|
||||
handleUnsafeConsequence(allMissingChecks, allEquippedDistractors) {
|
||||
this.scenarioFirstAttempt = false;
|
||||
|
||||
const totalIssues = allMissingChecks.length + allEquippedDistractors.length;
|
||||
|
||||
// 1. Start task machine momentarily
|
||||
this.dom.statusLight.className = 'status-indicator-light danger';
|
||||
this.dom.statusText.textContent = `CRITICAL HAZARD DETECTED (${totalIssues} ISSUES) - FREEZING OPERATION!`;
|
||||
|
||||
if (this.currentScenario.id === 'grinding') {
|
||||
window.soundEngine.startGrinderSFX();
|
||||
this.particles.startMode('sparks');
|
||||
} else {
|
||||
window.soundEngine.startLiquidTransferSFX();
|
||||
this.particles.startMode('chemical');
|
||||
}
|
||||
|
||||
// 2. Determine target position for incoming projectile (prioritize eye/face if missing, otherwise hand/body)
|
||||
const avatarCoords = this.avatar.getAvatarCenterCoordinates();
|
||||
const stageWidth = this.dom.sceneStage.clientWidth;
|
||||
const stageHeight = this.dom.sceneStage.clientHeight;
|
||||
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 };
|
||||
}
|
||||
|
||||
handleFreezeFrameAlert(projectile) {
|
||||
// 1. Freeze particle engine & stop machine motors
|
||||
this.particles.setFrozen(true);
|
||||
window.soundEngine.stopGrinderSFX();
|
||||
window.soundEngine.stopLiquidTransferSFX();
|
||||
|
||||
// 2. Duck background music
|
||||
window.soundEngine.duckMusic(true);
|
||||
|
||||
// 3. Play warning tones
|
||||
window.soundEngine.playWarningTone();
|
||||
window.soundEngine.playCriticalStopAlert();
|
||||
|
||||
const { allMissingChecks, allEquippedDistractors } = this.activeConsequenceData;
|
||||
const totalIssues = allMissingChecks.length + allEquippedDistractors.length;
|
||||
|
||||
// Slot icons mapping
|
||||
const slotIcons = {
|
||||
head: '🧢',
|
||||
eyes: '👓',
|
||||
ears: '🎧',
|
||||
hands: '🧤',
|
||||
body: '🦺',
|
||||
feet: '🥾'
|
||||
};
|
||||
|
||||
// 4. Update Header & Badges
|
||||
this.dom.hazardFlasherBadge.textContent = `⚠️ STOP! ${totalIssues} SAFETY ${totalIssues === 1 ? 'HAZARD' : 'HAZARDS'} DETECTED`;
|
||||
this.dom.missingCountNum.textContent = totalIssues;
|
||||
|
||||
if (totalIssues === 1) {
|
||||
if (allEquippedDistractors.length > 0) {
|
||||
const d = allEquippedDistractors[0];
|
||||
this.dom.consequenceTitle.textContent = "Incorrect / Ineffective PPE Detected";
|
||||
this.dom.consequenceDesc.textContent = d.rejectReason;
|
||||
} else {
|
||||
const m = allMissingChecks[0];
|
||||
this.dom.consequenceTitle.textContent = m.consequenceTitle;
|
||||
this.dom.consequenceDesc.textContent = m.consequenceDesc;
|
||||
}
|
||||
} else {
|
||||
this.dom.consequenceTitle.textContent = "Multiple Critical Protection Gaps Detected";
|
||||
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.";
|
||||
}
|
||||
|
||||
// 5. Build HTML for ALL Missing & Incorrect Items Simultaneously
|
||||
let cardsHtml = '';
|
||||
|
||||
// Render all missing critical checks
|
||||
allMissingChecks.forEach(chk => {
|
||||
const icon = slotIcons[chk.slot] || '⚠️';
|
||||
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>
|
||||
<span class="card-hazard-text">${chk.consequenceDesc}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
// Render all equipped distractors that need to be removed
|
||||
allEquippedDistractors.forEach(dist => {
|
||||
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>
|
||||
<span class="card-hazard-text">${dist.rejectReason}</span>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
this.dom.consequenceHazardList.innerHTML = cardsHtml;
|
||||
|
||||
// 6. Highlight missing slots in Coverage Grid with visual alert animation
|
||||
const coverageItems = this.dom.coverageGrid.querySelectorAll('.coverage-item');
|
||||
coverageItems.forEach(itemEl => {
|
||||
const slot = itemEl.dataset.slot;
|
||||
const isSlotMissing = allMissingChecks.some(chk => chk.slot === slot);
|
||||
if (isSlotMissing) {
|
||||
itemEl.classList.add('alert-missing');
|
||||
} else {
|
||||
itemEl.classList.remove('alert-missing');
|
||||
}
|
||||
});
|
||||
|
||||
// 7. Display freeze overlay
|
||||
this.dom.consequenceOverlay.classList.add('active');
|
||||
}
|
||||
|
||||
dismissConsequenceModal() {
|
||||
// Unfreeze and restore normal background music
|
||||
this.dom.consequenceOverlay.classList.remove('active');
|
||||
this.particles.clear();
|
||||
this.particles.setFrozen(false);
|
||||
this.particles.stopMode();
|
||||
window.soundEngine.duckMusic(false);
|
||||
|
||||
// Clear alert classes on coverage items
|
||||
const coverageItems = this.dom.coverageGrid.querySelectorAll('.coverage-item');
|
||||
coverageItems.forEach(itemEl => itemEl.classList.remove('alert-missing'));
|
||||
|
||||
this.dom.statusLight.className = 'status-indicator-light';
|
||||
this.dom.statusText.textContent = `TASK IDLE - ADJUST PPE SELECTION`;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// SUMMARY & COMPLETION SCREEN
|
||||
// =========================================================================
|
||||
|
||||
showFinalSummary() {
|
||||
this.switchScreen('summary');
|
||||
window.soundEngine.playVictorySting();
|
||||
this.particles.spawnConfetti();
|
||||
|
||||
// Update metrics
|
||||
const totalMax = SCENARIO_DATA.length * 100;
|
||||
const pct = Math.round((this.score / totalMax) * 100);
|
||||
this.dom.summaryScore.textContent = `${this.score} / ${totalMax}`;
|
||||
this.dom.summaryPercentage.textContent = `${pct}% Safety Compliance Rating`;
|
||||
|
||||
if (this.score >= 180) {
|
||||
this.dom.summaryStars.textContent = "⭐⭐";
|
||||
this.dom.summaryRank.textContent = "Master Safety Practitioner (Zero Accidents)";
|
||||
} else {
|
||||
this.dom.summaryStars.textContent = "⭐";
|
||||
this.dom.summaryRank.textContent = "Safety Certified (Refresher Recommended)";
|
||||
}
|
||||
}
|
||||
|
||||
updateHUD() {
|
||||
this.dom.hudScore.textContent = this.score;
|
||||
const starCount = Math.floor(this.score / 100);
|
||||
this.dom.hudStars.textContent = "⭐".repeat(starCount) + "☆".repeat(Math.max(0, 2 - starCount));
|
||||
}
|
||||
}
|
||||
|
||||
// Instantiate game on load
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
window.safetyGame = new SafetyGame();
|
||||
});
|
||||
Reference in New Issue
Block a user