Update game UI, add 3D gear assets, backgrounds, icons, and audio enhancements
This commit is contained in:
+81
-18
@@ -77,10 +77,19 @@ class SoundEngine {
|
||||
// VOLUME & MUTE CONTROLS
|
||||
// ==========================================
|
||||
|
||||
get isMasterMuted() {
|
||||
return this.isMuted;
|
||||
}
|
||||
|
||||
toggleMasterMute() {
|
||||
this.setMasterMute(!this.isMuted);
|
||||
return this.isMuted;
|
||||
}
|
||||
|
||||
setMasterMute(muted) {
|
||||
this.isMuted = muted;
|
||||
this.isMuted = !!muted;
|
||||
if (this.masterGain && this.ctx) {
|
||||
this.masterGain.gain.setTargetAtTime(muted ? 0 : 1.0, this.ctx.currentTime, 0.05);
|
||||
this.masterGain.gain.setTargetAtTime(this.isMuted ? 0 : 1.0, this.ctx.currentTime, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +182,7 @@ class SoundEngine {
|
||||
];
|
||||
|
||||
let step = 0;
|
||||
this.musicInterval = setInterval(() => {
|
||||
const playStep = () => {
|
||||
if (!this.isMusicPlaying || !this.ctx || this.isMuted) return;
|
||||
|
||||
const current = progression[step % progression.length];
|
||||
@@ -247,7 +256,27 @@ class SoundEngine {
|
||||
});
|
||||
|
||||
step++;
|
||||
}, 1800);
|
||||
};
|
||||
|
||||
// Play step 0 immediately with zero delay
|
||||
playStep();
|
||||
this.musicInterval = setInterval(playStep, 1800);
|
||||
}
|
||||
|
||||
enableAudioOnFirstGesture() {
|
||||
const unlockAndPlay = () => {
|
||||
this.ensureContext();
|
||||
if (!this.isMusicPlaying) {
|
||||
this.startBackgroundMusic();
|
||||
}
|
||||
['click', 'keydown', 'pointerdown', 'touchstart'].forEach(evt => {
|
||||
window.removeEventListener(evt, unlockAndPlay);
|
||||
});
|
||||
};
|
||||
|
||||
['click', 'keydown', 'pointerdown', 'touchstart'].forEach(evt => {
|
||||
window.addEventListener(evt, unlockAndPlay, { once: false, passive: true });
|
||||
});
|
||||
}
|
||||
|
||||
stopBackgroundMusic() {
|
||||
@@ -265,6 +294,7 @@ class SoundEngine {
|
||||
setAmbientScenario(scenarioId) {
|
||||
this.ensureContext();
|
||||
this.stopAmbient();
|
||||
this.stopAllTaskSFX();
|
||||
if (!this.ctx || !this.isAmbientEnabled) return;
|
||||
|
||||
if (scenarioId === 'grinding') {
|
||||
@@ -609,19 +639,37 @@ class SoundEngine {
|
||||
|
||||
stopGrinderSFX() {
|
||||
if (this.activeGrinderNode && this.ctx) {
|
||||
const t = this.ctx.currentTime;
|
||||
const node = this.activeGrinderNode;
|
||||
this.activeGrinderNode = null;
|
||||
try {
|
||||
this.activeGrinderNode.gain.gain.linearRampToValueAtTime(0.001, t + 0.1);
|
||||
this.activeGrinderNode.noiseGain.gain.linearRampToValueAtTime(0.001, t + 0.1);
|
||||
const t = this.ctx.currentTime;
|
||||
if (node.gain && node.gain.gain) {
|
||||
node.gain.gain.cancelScheduledValues(t);
|
||||
node.gain.gain.setValueAtTime(node.gain.gain.value, t);
|
||||
node.gain.gain.linearRampToValueAtTime(0.0001, t + 0.05);
|
||||
}
|
||||
if (node.noiseGain && node.noiseGain.gain) {
|
||||
node.noiseGain.gain.cancelScheduledValues(t);
|
||||
node.noiseGain.gain.setValueAtTime(node.noiseGain.gain.value, t);
|
||||
node.noiseGain.gain.linearRampToValueAtTime(0.0001, t + 0.05);
|
||||
}
|
||||
setTimeout(() => {
|
||||
try {
|
||||
this.activeGrinderNode.osc.stop();
|
||||
this.activeGrinderNode.noise.stop();
|
||||
if (node.osc) {
|
||||
node.osc.stop();
|
||||
node.osc.disconnect();
|
||||
}
|
||||
if (node.noise) {
|
||||
node.noise.stop();
|
||||
node.noise.disconnect();
|
||||
}
|
||||
} catch (e) {}
|
||||
this.activeGrinderNode = null;
|
||||
}, 120);
|
||||
}, 60);
|
||||
} catch (e) {
|
||||
this.activeGrinderNode = null;
|
||||
try {
|
||||
if (node.osc) { node.osc.stop(); node.osc.disconnect(); }
|
||||
if (node.noise) { node.noise.stop(); node.noise.disconnect(); }
|
||||
} catch (err) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -662,21 +710,36 @@ class SoundEngine {
|
||||
|
||||
stopLiquidTransferSFX() {
|
||||
if (this.activeLiquidNode && this.ctx) {
|
||||
const t = this.ctx.currentTime;
|
||||
const node = this.activeLiquidNode;
|
||||
this.activeLiquidNode = null;
|
||||
try {
|
||||
this.activeLiquidNode.gain.gain.linearRampToValueAtTime(0.001, t + 0.1);
|
||||
const t = this.ctx.currentTime;
|
||||
if (node.gain && node.gain.gain) {
|
||||
node.gain.gain.cancelScheduledValues(t);
|
||||
node.gain.gain.setValueAtTime(node.gain.gain.value, t);
|
||||
node.gain.gain.linearRampToValueAtTime(0.0001, t + 0.05);
|
||||
}
|
||||
setTimeout(() => {
|
||||
try {
|
||||
this.activeLiquidNode.noise.stop();
|
||||
if (node.noise) {
|
||||
node.noise.stop();
|
||||
node.noise.disconnect();
|
||||
}
|
||||
} catch (e) {}
|
||||
this.activeLiquidNode = null;
|
||||
}, 120);
|
||||
}, 60);
|
||||
} catch (e) {
|
||||
this.activeLiquidNode = null;
|
||||
try {
|
||||
if (node.noise) { node.noise.stop(); node.noise.disconnect(); }
|
||||
} catch (err) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stopAllTaskSFX() {
|
||||
this.stopGrinderSFX();
|
||||
this.stopLiquidTransferSFX();
|
||||
}
|
||||
|
||||
playChemicalSplashSFX() {
|
||||
this.ensureContext();
|
||||
if (!this.ctx || this.isMuted) return;
|
||||
|
||||
+149
-91
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* PPE SAFETY CHALLENGE - DYNAMIC 2D WORKER AVATAR SYSTEM
|
||||
* Uses the step-by-step transparent character images from ppe_character_steps_transparent_png!
|
||||
* Dynamically switches character states as safety gear is equipped.
|
||||
* PPE SAFETY CHALLENGE - REALISTIC 3D WORKER AVATAR SYSTEM
|
||||
* Uses the exact 3D realistic character image provided by the user (images/worker_base.png),
|
||||
* with precision anatomical PPE overlays for both Grinding and Chemical scenarios.
|
||||
*/
|
||||
|
||||
class WorkerAvatar {
|
||||
@@ -18,7 +18,7 @@ class WorkerAvatar {
|
||||
|
||||
equip(itemId) {
|
||||
this.equippedItems.add(itemId);
|
||||
this.updateLayers();
|
||||
this.updateLayers(itemId);
|
||||
}
|
||||
|
||||
unequip(itemId) {
|
||||
@@ -40,107 +40,165 @@ class WorkerAvatar {
|
||||
x: relX,
|
||||
y: relY,
|
||||
eyeX: relX,
|
||||
eyeY: relY - 130,
|
||||
handX: relX + 55,
|
||||
handY: relY + 30
|
||||
eyeY: relY - 135,
|
||||
handX: relX + 65,
|
||||
handY: relY + 25
|
||||
};
|
||||
}
|
||||
|
||||
getStepImagePath() {
|
||||
const hasHelmet = this.equippedItems.has('safety_helmet_grind') || this.equippedItems.has('safety_helmet_chem');
|
||||
const hasGoggles = this.equippedItems.has('chem_splash_goggles') || this.equippedItems.has('safety_goggles_grind');
|
||||
const hasFaceShield = this.equippedItems.has('face_shield_grind') || this.equippedItems.has('face_shield_chem');
|
||||
const hasHearing = this.equippedItems.has('hearing_protection_grind') || this.equippedItems.has('hearing_protection_chem');
|
||||
const hasLeatherGloves = this.equippedItems.has('leather_grinding_gloves');
|
||||
const hasWorkGloves = this.equippedItems.has('work_gloves_grind') || this.equippedItems.has('standard_work_gloves');
|
||||
const hasChemGloves = this.equippedItems.has('chem_resistant_gloves');
|
||||
const hasVest = this.equippedItems.has('reflective_vest_grind');
|
||||
const hasApron = this.equippedItems.has('chem_resistant_apron');
|
||||
const hasCottonCoat = this.equippedItems.has('cotton_coat_chem');
|
||||
const hasBoots = this.equippedItems.has('steel_toe_boots_grind') || this.equippedItems.has('chem_safety_shoes');
|
||||
const hasMask = this.equippedItems.has('dust_mask_grind');
|
||||
|
||||
// 10. Full PPE with Mask
|
||||
if (hasMask) {
|
||||
return 'images/steps/10_full_ppe_with_mask.png';
|
||||
}
|
||||
|
||||
// 9. Full Grinding Set with Boots
|
||||
if (hasHelmet && (hasGoggles || hasFaceShield) && hasHearing && (hasLeatherGloves || hasChemGloves) && (hasVest || hasApron || hasBoots)) {
|
||||
return 'images/steps/09_helmet_goggles_hearing_leather_gloves_vest_boots.png';
|
||||
}
|
||||
|
||||
// 8. With Vest / Body Wear
|
||||
if (hasHelmet && (hasGoggles || hasFaceShield) && hasHearing && (hasLeatherGloves || hasChemGloves) && (hasVest || hasApron || hasCottonCoat)) {
|
||||
return 'images/steps/08_helmet_goggles_hearing_leather_gloves_vest.png';
|
||||
}
|
||||
|
||||
// 7. With Leather / Chem Gloves
|
||||
if (hasHelmet && (hasGoggles || hasFaceShield) && hasHearing && (hasLeatherGloves || hasChemGloves)) {
|
||||
return 'images/steps/07_helmet_goggles_hearing_leather_gloves.png';
|
||||
}
|
||||
|
||||
// 6. With Work Gloves
|
||||
if (hasHelmet && (hasGoggles || hasFaceShield) && hasHearing && hasWorkGloves) {
|
||||
return 'images/steps/06_helmet_goggles_hearing_work_gloves.png';
|
||||
}
|
||||
|
||||
// 5. With Earmuffs / Hearing Protection
|
||||
if (hasHelmet && (hasGoggles || hasFaceShield) && hasHearing) {
|
||||
return 'images/steps/05_helmet_goggles_hearing.png';
|
||||
}
|
||||
|
||||
// 4. With Face Shield
|
||||
if (hasFaceShield || (hasHelmet && hasFaceShield)) {
|
||||
return 'images/steps/04_helmet_goggles_face_shield.png';
|
||||
}
|
||||
|
||||
// 3. With Goggles & Helmet
|
||||
if (hasHelmet && hasGoggles) {
|
||||
return 'images/steps/03_helmet_goggles.png';
|
||||
}
|
||||
|
||||
// 2. Helmet Only
|
||||
if (hasHelmet) {
|
||||
return 'images/steps/02_helmet.png';
|
||||
}
|
||||
|
||||
// Individual items
|
||||
if (hasFaceShield) return 'images/steps/04_helmet_goggles_face_shield.png';
|
||||
if (hasGoggles) return 'images/steps/03_helmet_goggles.png';
|
||||
if (hasHearing) return 'images/steps/05_helmet_goggles_hearing.png';
|
||||
if (hasLeatherGloves) return 'images/steps/07_helmet_goggles_hearing_leather_gloves.png';
|
||||
if (hasWorkGloves) return 'images/steps/06_helmet_goggles_hearing_work_gloves.png';
|
||||
|
||||
// 1. Base Worker (No PPE)
|
||||
return 'images/steps/01_no_ppe.png';
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.container) return;
|
||||
|
||||
this.container.innerHTML = `
|
||||
<div class="avatar-step-container" id="avatar-step-container">
|
||||
<!-- Floor Contact Shadow -->
|
||||
<div class="avatar-floor-shadow"></div>
|
||||
|
||||
<!-- Dynamic Character Step Image -->
|
||||
<img src="images/steps/01_no_ppe.png" alt="Worker Character" class="avatar-step-img" id="avatar-step-img" />
|
||||
</div>
|
||||
<svg viewBox="0 0 682 1024" class="worker-svg" id="worker-avatar-svg" xmlns="http://www.w3.org/2000/svg">
|
||||
<defs>
|
||||
<filter id="gearShadow" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feDropShadow dx="0" dy="4" stdDeviation="5" flood-color="#000000" flood-opacity="0.6"/>
|
||||
</filter>
|
||||
<radialGradient id="softFloorShadowGrad" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#000000" stop-opacity="0.65"/>
|
||||
<stop offset="35%" stop-color="#000000" stop-opacity="0.4"/>
|
||||
<stop offset="65%" stop-color="#000000" stop-opacity="0.18"/>
|
||||
<stop offset="85%" stop-color="#000000" stop-opacity="0.05"/>
|
||||
<stop offset="100%" stop-color="#000000" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="footContactShadowGrad" cx="50%" cy="50%" r="50%">
|
||||
<stop offset="0%" stop-color="#000000" stop-opacity="0.75"/>
|
||||
<stop offset="40%" stop-color="#000000" stop-opacity="0.45"/>
|
||||
<stop offset="75%" stop-color="#000000" stop-opacity="0.12"/>
|
||||
<stop offset="100%" stop-color="#000000" stop-opacity="0"/>
|
||||
</radialGradient>
|
||||
<filter id="softFloorBlur" x="-30%" y="-30%" width="160%" height="160%">
|
||||
<feGaussianBlur stdDeviation="5"/>
|
||||
</filter>
|
||||
</defs>
|
||||
|
||||
<!-- Realistic Soft Floor Contact & Ambient Occlusion Shadow -->
|
||||
<g id="layer-floor-shadow" filter="url(#softFloorBlur)">
|
||||
<!-- Broad Ambient Floor Shadow -->
|
||||
<ellipse cx="347" cy="1008" rx="205" ry="22" fill="url(#softFloorShadowGrad)"/>
|
||||
<!-- Left Foot Direct Contact Shadow -->
|
||||
<ellipse cx="238" cy="1008" rx="72" ry="14" fill="url(#footContactShadowGrad)"/>
|
||||
<!-- Right Foot Direct Contact Shadow -->
|
||||
<ellipse cx="452" cy="1008" rx="72" ry="14" fill="url(#footContactShadowGrad)"/>
|
||||
</g>
|
||||
|
||||
<!-- ====================================================
|
||||
LAYER 1: REALISTIC 3D WORKER BASE CHARACTER IMAGE
|
||||
==================================================== -->
|
||||
<g id="layer-base-character">
|
||||
<image href="images/worker_base.png" x="0" y="0" width="682" height="1024" preserveAspectRatio="xMidYMid meet" />
|
||||
</g>
|
||||
|
||||
<!-- ====================================================
|
||||
LAYER 2: MODULAR PPE EQUIPMENT (TOGGLED DYNAMICALLY)
|
||||
==================================================== -->
|
||||
|
||||
<!-- 1. FEET: Safety Shoes (Steel-Toe Boots covering bare feet) -->
|
||||
<g id="layer-gear-steel_toe_boots_grind" style="display: none;" filter="url(#gearShadow)">
|
||||
<image href="images/gear/gear_3d_safety_boots.png" x="0" y="0" width="682" height="1024" preserveAspectRatio="xMidYMid meet" />
|
||||
</g>
|
||||
<g id="layer-gear-chem_safety_shoes" style="display: none;" filter="url(#gearShadow)">
|
||||
<image href="images/gear/gear_3d_safety_boots.png" x="0" y="0" width="682" height="1024" preserveAspectRatio="xMidYMid meet" />
|
||||
</g>
|
||||
|
||||
<!-- 2. HANDS: Leather Grinding / Heavy Work Gloves & Chemical Gloves -->
|
||||
<g id="layer-gear-leather_grinding_gloves" style="display: none;" filter="url(#gearShadow)">
|
||||
<image href="images/gear/gear_3d_leather_gloves.png" x="0" y="0" width="682" height="1024" preserveAspectRatio="xMidYMid meet" />
|
||||
</g>
|
||||
<g id="layer-gear-work_gloves_grind" style="display: none;" filter="url(#gearShadow)">
|
||||
<image href="images/gear/gear_3d_work_gloves.png" x="0" y="0" width="682" height="1024" preserveAspectRatio="xMidYMid meet" />
|
||||
</g>
|
||||
<g id="layer-gear-standard_work_gloves" style="display: none;" filter="url(#gearShadow)">
|
||||
<image href="images/gear/gear_3d_work_gloves.png" x="0" y="0" width="682" height="1024" preserveAspectRatio="xMidYMid meet" />
|
||||
</g>
|
||||
<g id="layer-gear-chem_resistant_gloves" style="display: none;" filter="url(#gearShadow)">
|
||||
<image href="images/gear/gear_3d_chem_gloves.png" x="0" y="0" width="682" height="1024" preserveAspectRatio="xMidYMid meet" />
|
||||
</g>
|
||||
|
||||
<!-- 3. BODY: Lab Coat, Aprons, Reflective Vest (Overlaps glove cuffs) -->
|
||||
<g id="layer-gear-cotton_coat_chem" style="display: none;" filter="url(#gearShadow)">
|
||||
<image href="images/gear/gear_3d_cotton_coat.png" x="0" y="0" width="682" height="1024" preserveAspectRatio="xMidYMid meet" />
|
||||
</g>
|
||||
<g id="layer-gear-chem_resistant_apron" style="display: none;" filter="url(#gearShadow)">
|
||||
<image href="images/gear/gear_3d_chem_apron.png" x="0" y="0" width="682" height="1024" preserveAspectRatio="xMidYMid meet" />
|
||||
</g>
|
||||
<g id="layer-gear-fire_resistant_apron_grind" style="display: none;" filter="url(#gearShadow)">
|
||||
<image href="images/gear/gear_3d_fire_resistant_apron.png" x="0" y="0" width="682" height="1024" preserveAspectRatio="xMidYMid meet" />
|
||||
</g>
|
||||
<g id="layer-gear-fire_resistant_apron" style="display: none;" filter="url(#gearShadow)">
|
||||
<image href="images/gear/gear_3d_fire_resistant_apron.png" x="0" y="0" width="682" height="1024" preserveAspectRatio="xMidYMid meet" />
|
||||
</g>
|
||||
<g id="layer-gear-reflective_vest_grind" style="display: none;" filter="url(#gearShadow)">
|
||||
<image href="images/gear/gear_3d_reflective_vest.png" x="0" y="0" width="682" height="1024" preserveAspectRatio="xMidYMid meet" />
|
||||
</g>
|
||||
|
||||
<!-- 4. MASK: Dust Mask (Grinding Distractor) -->
|
||||
<g id="layer-gear-dust_mask_grind" style="display: none;" filter="url(#gearShadow)">
|
||||
<image href="images/gear/gear_3d_dust_mask.png" x="0" y="0" width="682" height="1024" preserveAspectRatio="xMidYMid meet" />
|
||||
</g>
|
||||
|
||||
<!-- 5. EYES: Safety Goggles (Clear Grinding Protective Glasses) -->
|
||||
<g id="layer-gear-safety_goggles_grind" style="display: none;" filter="url(#gearShadow)">
|
||||
<image href="images/gear/gear_3d_safety_glasses.png" x="0" y="0" width="682" height="1024" preserveAspectRatio="xMidYMid meet" />
|
||||
</g>
|
||||
<g id="layer-gear-chem_splash_goggles" style="display: none;" filter="url(#gearShadow)">
|
||||
<image href="images/gear/gear_3d_chem_splash_goggles.png" x="0" y="0" width="682" height="1024" preserveAspectRatio="xMidYMid meet" />
|
||||
</g>
|
||||
|
||||
<!-- 6. HEARING: Acoustic Earmuffs (Grinding & Chemical) -->
|
||||
<g id="layer-gear-hearing_protection_grind" style="display: none;" filter="url(#gearShadow)">
|
||||
<image href="images/gear/gear_3d_earmuffs.png" x="0" y="0" width="682" height="1024" preserveAspectRatio="xMidYMid meet" />
|
||||
</g>
|
||||
<g id="layer-gear-hearing_protection_chem" style="display: none;" filter="url(#gearShadow)">
|
||||
<image href="images/gear/gear_3d_earmuffs.png" x="0" y="0" width="682" height="1024" preserveAspectRatio="xMidYMid meet" />
|
||||
</g>
|
||||
|
||||
<!-- 7. HEAD: Safety Helmet (Yellow Hard Hat) -->
|
||||
<g id="layer-gear-safety_helmet_grind" style="display: none;" filter="url(#gearShadow)">
|
||||
<image href="images/gear/gear_3d_safety_helmet.png" x="0" y="0" width="682" height="1024" preserveAspectRatio="xMidYMid meet" />
|
||||
</g>
|
||||
<g id="layer-gear-safety_helmet_chem" style="display: none;" filter="url(#gearShadow)">
|
||||
<image href="images/gear/gear_3d_safety_helmet.png" x="0" y="0" width="682" height="1024" preserveAspectRatio="xMidYMid meet" />
|
||||
</g>
|
||||
|
||||
<!-- 8. FACE: Face Shield (Grinding & Chemical Visor) -->
|
||||
<g id="layer-gear-face_shield_grind" style="display: none;" filter="url(#gearShadow)">
|
||||
<image href="images/gear/gear_3d_face_shield.png" x="0" y="0" width="682" height="1024" preserveAspectRatio="xMidYMid meet" />
|
||||
</g>
|
||||
<g id="layer-gear-face_shield_chem" style="display: none;" filter="url(#gearShadow)">
|
||||
<image href="images/gear/gear_3d_face_shield.png" x="0" y="0" width="682" height="1024" preserveAspectRatio="xMidYMid meet" />
|
||||
</g>
|
||||
|
||||
</svg>
|
||||
`;
|
||||
|
||||
this.updateLayers();
|
||||
}
|
||||
|
||||
updateLayers() {
|
||||
updateLayers(newlyEquippedId = null) {
|
||||
if (!this.container) return;
|
||||
|
||||
const imgEl = document.getElementById('avatar-step-img');
|
||||
if (imgEl) {
|
||||
const targetSrc = this.getStepImagePath();
|
||||
if (imgEl.getAttribute('src') !== targetSrc) {
|
||||
imgEl.src = targetSrc;
|
||||
const allGearIds = [
|
||||
'safety_helmet_chem', 'safety_helmet_grind',
|
||||
'chem_splash_goggles', 'safety_goggles_grind', 'face_shield_chem', 'face_shield_grind', 'tinted_sunglasses', 'dust_mask_grind',
|
||||
'hearing_protection_chem', 'hearing_protection_grind',
|
||||
'chem_resistant_gloves', 'standard_work_gloves', 'work_gloves_grind', 'leather_grinding_gloves', 'disposable_latex_gloves',
|
||||
'chem_resistant_apron', 'cotton_coat_chem', 'flame_resistant_jacket', 'reflective_vest_grind', 'fire_resistant_apron_grind', 'fire_resistant_apron',
|
||||
'chem_safety_shoes', 'steel_toe_boots_grind', 'running_sneakers'
|
||||
];
|
||||
|
||||
allGearIds.forEach(gearId => {
|
||||
const el = document.getElementById(`layer-gear-${gearId}`);
|
||||
if (el) {
|
||||
const isEquipped = this.equippedItems.has(gearId);
|
||||
el.style.display = isEquipped ? 'block' : 'none';
|
||||
if (isEquipped && gearId === newlyEquippedId) {
|
||||
el.classList.remove('gear-pop-glow');
|
||||
void el.offsetWidth;
|
||||
el.classList.add('gear-pop-glow');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+45
-307
@@ -94,6 +94,9 @@ class SafetyGame {
|
||||
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);
|
||||
@@ -101,17 +104,14 @@ class SafetyGame {
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Start Game from Intro
|
||||
// Start Game from Intro (Instantaneous Transition, Zero Delay)
|
||||
if (this.dom.btnStartGame) {
|
||||
this.dom.btnStartGame.addEventListener('click', () => {
|
||||
window.soundEngine.ensureContext();
|
||||
window.soundEngine.playStartIntroSting();
|
||||
window.soundEngine.startBackgroundMusic();
|
||||
this.loadScenario(0);
|
||||
this.switchScreen('game');
|
||||
|
||||
setTimeout(() => {
|
||||
window.soundEngine.startBackgroundMusic();
|
||||
this.loadScenario(0);
|
||||
}, 800);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -141,6 +141,7 @@ class SafetyGame {
|
||||
if (this.dom.btnNextScenario) {
|
||||
this.dom.btnNextScenario.addEventListener('click', () => {
|
||||
window.soundEngine.playButtonClick();
|
||||
window.soundEngine.stopAllTaskSFX();
|
||||
this.dom.successOverlay.classList.remove('active');
|
||||
this.particles.stopMode();
|
||||
|
||||
@@ -175,13 +176,13 @@ class SafetyGame {
|
||||
});
|
||||
}
|
||||
|
||||
// Audio & Settings Header Controls
|
||||
// Audio & Settings Header Controls (Toggle Mute / Unmute continuously)
|
||||
if (this.dom.btnSoundToggle) {
|
||||
this.dom.btnSoundToggle.addEventListener('click', () => {
|
||||
window.soundEngine.ensureContext();
|
||||
const isMuted = !window.soundEngine.isMasterMuted;
|
||||
window.soundEngine.setMasterMute(isMuted);
|
||||
this.dom.btnSoundToggle.querySelector('span').textContent = isMuted ? '🔇' : '🔊';
|
||||
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;
|
||||
});
|
||||
}
|
||||
@@ -192,7 +193,8 @@ class SafetyGame {
|
||||
const currentVol = window.soundEngine.musicVolume;
|
||||
const newVol = currentVol > 0.01 ? 0 : 0.25;
|
||||
window.soundEngine.setMusicVolume(newVol);
|
||||
this.dom.btnMusicToggle.querySelector('span').textContent = newVol > 0 ? '🎵' : '🔇';
|
||||
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)}%`;
|
||||
});
|
||||
@@ -247,16 +249,23 @@ class SafetyGame {
|
||||
});
|
||||
}
|
||||
|
||||
// Keyboard Shortcuts (1-9 for Locker, Space for Start Task, Esc to close modals)
|
||||
// Keyboard Shortcuts (1-9 and 0 for 10 Locker items, Space for Start Task, Esc to close modals)
|
||||
window.addEventListener('keydown', (e) => {
|
||||
const num = parseInt(e.key);
|
||||
if (!isNaN(num) && num >= 1 && num <= 9) {
|
||||
if (!isNaN(num)) {
|
||||
const cards = document.querySelectorAll('.ppe-item-card');
|
||||
if (cards[num - 1]) {
|
||||
cards[num - 1].click();
|
||||
const cardIndex = num === 0 ? 9 : num - 1;
|
||||
if (cards[cardIndex]) {
|
||||
cards[cardIndex].click();
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
@@ -285,6 +294,7 @@ class SafetyGame {
|
||||
}
|
||||
|
||||
loadScenario(index) {
|
||||
window.soundEngine.stopAllTaskSFX();
|
||||
this.currentScenarioIndex = index;
|
||||
this.currentScenario = SCENARIO_DATA[index];
|
||||
this.scenarioFirstAttempt = true;
|
||||
@@ -305,6 +315,11 @@ class SafetyGame {
|
||||
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);
|
||||
|
||||
@@ -326,298 +341,16 @@ class SafetyGame {
|
||||
}
|
||||
|
||||
renderScenarioEnvironment(scenarioId) {
|
||||
if (!this.dom.sceneBackdrop) return;
|
||||
|
||||
if (scenarioId === 'grinding') {
|
||||
// SCENARIO 1: Vector Industrial Fabrication & Grinding Workshop (Matching Reference Image)
|
||||
this.dom.sceneBackdrop.innerHTML = `
|
||||
<svg viewBox="0 0 1000 600" preserveAspectRatio="xMidYMid slice" class="env-svg-stage">
|
||||
<defs>
|
||||
<linearGradient id="grindSkyGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#3b6998"/>
|
||||
<stop offset="60%" stop-color="#5486b8"/>
|
||||
<stop offset="100%" stop-color="#72a1d2"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="grindLampGlow" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="rgba(254, 240, 138, 0.55)"/>
|
||||
<stop offset="60%" stop-color="rgba(254, 240, 138, 0.22)"/>
|
||||
<stop offset="100%" stop-color="rgba(254, 240, 138, 0.0)"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="grindBenchGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#94a3b8"/>
|
||||
<stop offset="100%" stop-color="#64748b"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="grindFloorGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#475569"/>
|
||||
<stop offset="50%" stop-color="#334155"/>
|
||||
<stop offset="100%" stop-color="#1e293b"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- 1. BLUE FACTORY WALL & TALL MULTI-PANE WINDOWS -->
|
||||
<rect width="1000" height="380" fill="url(#grindSkyGrad)"/>
|
||||
|
||||
<!-- Distant Sky & Structure Silhouettes Through Windows -->
|
||||
<g fill="#2c527c" opacity="0.4">
|
||||
<rect x="80" y="120" width="140" height="260"/>
|
||||
<rect x="280" y="90" width="180" height="290"/>
|
||||
<rect x="520" y="140" width="120" height="240"/>
|
||||
<rect x="700" y="100" width="220" height="280"/>
|
||||
</g>
|
||||
|
||||
<!-- Window Framework Mullions -->
|
||||
<g stroke="#1e3a5f" stroke-width="4" opacity="0.8">
|
||||
<line x1="0" y1="70" x2="1000" y2="70"/>
|
||||
<line x1="0" y1="180" x2="1000" y2="180"/>
|
||||
<line x1="0" y1="280" x2="1000" y2="280"/>
|
||||
<line x1="160" y1="0" x2="160" y2="380"/>
|
||||
<line x1="320" y1="0" x2="320" y2="380"/>
|
||||
<line x1="610" y1="0" x2="610" y2="380"/>
|
||||
<line x1="860" y1="0" x2="860" y2="380"/>
|
||||
</g>
|
||||
|
||||
<!-- Overhead Gantry & Ceiling Beams -->
|
||||
<rect x="0" y="24" width="1000" height="30" fill="#1e293b"/>
|
||||
<rect x="0" y="54" width="1000" height="8" fill="#0f172a"/>
|
||||
|
||||
<!-- 2. STRUCTURAL PILLARS WITH HAZARD CHEVRONS & PPE SIGN -->
|
||||
<!-- Left Column with Hazard Stripes -->
|
||||
<rect x="240" y="30" width="56" height="350" fill="#334155"/>
|
||||
<g transform="translate(240, 260)">
|
||||
<rect width="56" height="120" fill="#18181b"/>
|
||||
<!-- Yellow/Black Chevrons -->
|
||||
<polygon points="0,0 28,0 56,28 56,56 0,0" fill="#facc15"/>
|
||||
<polygon points="0,35 56,91 56,119 0,63" fill="#facc15"/>
|
||||
<polygon points="0,95 25,120 0,120" fill="#facc15"/>
|
||||
</g>
|
||||
|
||||
<!-- Green "WEAR PPE" Sign on Pillar (From Reference) -->
|
||||
<g transform="translate(155, 170)">
|
||||
<rect width="90" height="90" rx="8" fill="#059669" stroke="#ffffff" stroke-width="3"/>
|
||||
<!-- PPE Helmet/Goggles Symbol -->
|
||||
<circle cx="45" cy="38" r="22" fill="#ffffff" opacity="0.2"/>
|
||||
<path d="M 28 36 C 28 22 62 22 62 36 Z" fill="#ffffff"/>
|
||||
<rect x="25" y="36" width="40" height="4" rx="2" fill="#ffffff"/>
|
||||
<ellipse cx="37" cy="45" rx="6" ry="4" fill="none" stroke="#ffffff" stroke-width="2"/>
|
||||
<ellipse cx="53" cy="45" rx="6" ry="4" fill="none" stroke="#ffffff" stroke-width="2"/>
|
||||
<text x="45" y="66" fill="#ffffff" font-size="11" font-weight="900" font-family="sans-serif" text-anchor="middle">WEAR</text>
|
||||
<text x="45" y="78" fill="#ffffff" font-size="11" font-weight="900" font-family="sans-serif" text-anchor="middle">PPE</text>
|
||||
</g>
|
||||
|
||||
<!-- Right Background Storage Racks with Hazard Stripes -->
|
||||
<g transform="translate(875, 140)">
|
||||
<rect width="110" height="240" fill="#1e293b" stroke="#334155" stroke-width="3"/>
|
||||
<line x1="0" y1="60" x2="110" y2="60" stroke="#475569" stroke-width="4"/>
|
||||
<line x1="0" y1="120" x2="110" y2="120" stroke="#475569" stroke-width="4"/>
|
||||
<line x1="0" y1="180" x2="110" y2="180" stroke="#475569" stroke-width="4"/>
|
||||
<!-- Column hazard chevrons -->
|
||||
<g fill="#facc15">
|
||||
<polygon points="0,0 20,0 0,20"/>
|
||||
<polygon points="0,40 20,20 20,40 0,60"/>
|
||||
<polygon points="0,80 20,60 20,80 0,100"/>
|
||||
<polygon points="0,120 20,100 20,120 0,140"/>
|
||||
</g>
|
||||
</g>
|
||||
|
||||
<!-- Red Tool Cabinet (Left) -->
|
||||
<g transform="translate(0, 255)">
|
||||
<rect width="150" height="125" rx="4" fill="#dc2626" stroke="#991b1b" stroke-width="3"/>
|
||||
<!-- Drawers with Silver Handles -->
|
||||
<rect x="10" y="15" width="130" height="22" rx="2" fill="#b91c1c"/>
|
||||
<rect x="45" y="24" width="60" height="4" rx="2" fill="#e2e8f0"/>
|
||||
<rect x="10" y="44" width="130" height="22" rx="2" fill="#b91c1c"/>
|
||||
<rect x="45" y="53" width="60" height="4" rx="2" fill="#e2e8f0"/>
|
||||
<rect x="10" y="73" width="130" height="22" rx="2" fill="#b91c1c"/>
|
||||
<rect x="45" y="82" width="60" height="4" rx="2" fill="#e2e8f0"/>
|
||||
<!-- Top Tool Box -->
|
||||
<rect x="15" y="-22" width="55" height="22" rx="3" fill="#eab308" stroke="#ca8a04" stroke-width="2"/>
|
||||
<rect x="80" y="-26" width="40" height="26" rx="3" fill="#0284c7" stroke="#0369a1" stroke-width="2"/>
|
||||
</g>
|
||||
|
||||
<!-- 3. PENDANT CONICAL LAMPS & BRIGHT LIGHT CONES (From Reference) -->
|
||||
<!-- Left Lamp & Glow Cone -->
|
||||
<g transform="translate(190, 50)">
|
||||
<line x1="0" y1="0" x2="0" y2="40" stroke="#0f172a" stroke-width="3"/>
|
||||
<polygon points="0,35 -32,60 32,60" fill="#3b82f6" stroke="#1d4ed8" stroke-width="1.5"/>
|
||||
<!-- Spotlight Cone -->
|
||||
<polygon points="-32,60 32,60 100,380 -100,380" fill="url(#grindLampGlow)"/>
|
||||
<ellipse cx="0" cy="60" rx="30" ry="7" fill="#ffffff"/>
|
||||
</g>
|
||||
|
||||
<!-- Right Lamp & Glow Cone -->
|
||||
<g transform="translate(790, 50)">
|
||||
<line x1="0" y1="0" x2="0" y2="40" stroke="#0f172a" stroke-width="3"/>
|
||||
<polygon points="0,35 -32,60 32,60" fill="#3b82f6" stroke="#1d4ed8" stroke-width="1.5"/>
|
||||
<!-- Spotlight Cone -->
|
||||
<polygon points="-32,60 32,60 100,380 -100,380" fill="url(#grindLampGlow)"/>
|
||||
<ellipse cx="0" cy="60" rx="30" ry="7" fill="#ffffff"/>
|
||||
</g>
|
||||
|
||||
<!-- 4. WORKSHOP FLOOR & YELLOW HAZARD BOUNDARY LINES -->
|
||||
<rect x="0" y="380" width="1000" height="220" fill="url(#grindFloorGrad)"/>
|
||||
<line x1="0" y1="380" x2="1000" y2="380" stroke="#334155" stroke-width="3"/>
|
||||
|
||||
<!-- Yellow Floor Safety Line Markings (From Reference) -->
|
||||
<g fill="none" stroke="#facc15" stroke-width="12" opacity="0.95">
|
||||
<line x1="0" y1="560" x2="300" y2="430"/>
|
||||
<line x1="280" y1="430" x2="550" y2="600"/>
|
||||
<line x1="550" y1="600" x2="550" y2="380"/>
|
||||
</g>
|
||||
|
||||
<!-- Stacks of Rectangular Steel Hollow Tubing on Left Floor -->
|
||||
<g transform="translate(0, 420)">
|
||||
<rect x="0" y="0" width="110" height="40" fill="#94a3b8" stroke="#475569" stroke-width="3"/>
|
||||
<rect x="10" y="8" width="90" height="24" fill="#334155"/>
|
||||
<rect x="90" y="42" width="55" height="40" fill="#94a3b8" stroke="#475569" stroke-width="3"/>
|
||||
<rect x="98" y="50" width="39" height="24" fill="#334155"/>
|
||||
</g>
|
||||
|
||||
<!-- 5. HEAVY INDUSTRIAL WORKBENCH, BLUE VISE & ANGLE GRINDER (Right) -->
|
||||
<!-- Anti-slip Textured Floor Mat under Table -->
|
||||
<rect x="490" y="430" width="450" height="85" rx="4" fill="#1e293b" stroke="#334155" stroke-width="2"/>
|
||||
<g fill="#0f172a" opacity="0.7">
|
||||
<circle cx="530" cy="450" r="4"/><circle cx="570" cy="450" r="4"/><circle cx="610" cy="450" r="4"/>
|
||||
<circle cx="650" cy="450" r="4"/><circle cx="690" cy="450" r="4"/><circle cx="730" cy="450" r="4"/>
|
||||
<circle cx="770" cy="450" r="4"/><circle cx="810" cy="450" r="4"/><circle cx="850" cy="450" r="4"/>
|
||||
<circle cx="530" cy="480" r="4"/><circle cx="570" cy="480" r="4"/><circle cx="610" cy="480" r="4"/>
|
||||
<circle cx="650" cy="480" r="4"/><circle cx="690" cy="480" r="4"/><circle cx="730" cy="480" r="4"/>
|
||||
<circle cx="770" cy="480" r="4"/><circle cx="810" cy="480" r="4"/><circle cx="850" cy="480" r="4"/>
|
||||
</g>
|
||||
|
||||
<!-- Workbench Legs & Frame -->
|
||||
<g transform="translate(560, 310)">
|
||||
<!-- Heavy Steel Frame -->
|
||||
<rect x="20" y="60" width="400" height="200" fill="none" stroke="#1e293b" stroke-width="16"/>
|
||||
<!-- Metal Bench Top -->
|
||||
<rect x="0" y="52" width="440" height="20" rx="3" fill="url(#grindBenchGrad)" stroke="#334155" stroke-width="2"/>
|
||||
|
||||
<!-- Steel I-Beam Clamped for Grinding (From Reference) -->
|
||||
<g transform="translate(90, -10)">
|
||||
<rect x="0" y="0" width="350" height="62" fill="#cbd5e1" stroke="#475569" stroke-width="2"/>
|
||||
<rect x="0" y="0" width="350" height="12" fill="#e2e8f0"/>
|
||||
<rect x="0" y="50" width="350" height="12" fill="#94a3b8"/>
|
||||
</g>
|
||||
|
||||
<!-- Blue Industrial Bench Vise Clamp -->
|
||||
<g transform="translate(5, 0)">
|
||||
<!-- Base clamped to bench -->
|
||||
<rect x="55" y="50" width="40" height="45" rx="3" fill="#1d4ed8" stroke="#1e40af" stroke-width="2"/>
|
||||
<!-- Vise Body -->
|
||||
<path d="M 25 15 L 85 15 L 85 55 L 45 55 Z" fill="#2563eb" stroke="#1d4ed8" stroke-width="2"/>
|
||||
<!-- Clamp Jaws holding I-beam -->
|
||||
<rect x="85" y="8" width="18" height="42" fill="#94a3b8" stroke="#475569" stroke-width="1.5"/>
|
||||
<rect x="10" y="24" width="20" height="12" rx="2" fill="#cbd5e1"/>
|
||||
</g>
|
||||
|
||||
<!-- Yellow Angle Grinder on Tabletop (From Reference) -->
|
||||
<g transform="translate(190, 18)">
|
||||
<!-- Black Grinding Disc -->
|
||||
<ellipse cx="40" cy="22" rx="24" ry="24" fill="#27272a" stroke="#09090b" stroke-width="2"/>
|
||||
<ellipse cx="40" cy="22" rx="8" ry="8" fill="#94a3b8"/>
|
||||
<!-- Metal Spindle Flange -->
|
||||
<circle cx="40" cy="22" r="3" fill="#ffffff"/>
|
||||
<!-- Yellow Motor Housing -->
|
||||
<rect x="60" y="10" width="75" height="24" rx="6" fill="#facc15" stroke="#ca8a04" stroke-width="2"/>
|
||||
<rect x="110" y="13" width="20" height="18" fill="#eab308"/>
|
||||
<!-- Black Rear Handle -->
|
||||
<path d="M 135 15 L 165 18 L 165 26 L 135 29 Z" fill="#18181b"/>
|
||||
<!-- Black Power Cord trailing down -->
|
||||
<path d="M 165 22 Q 210 30 200 90 Q 190 150 240 240" fill="none" stroke="#09090b" stroke-width="4"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
} else {
|
||||
// SCENARIO 2: Vector Industrial Chemical Transfer Station
|
||||
this.dom.sceneBackdrop.innerHTML = `
|
||||
<svg viewBox="0 0 1000 600" preserveAspectRatio="xMidYMid slice" class="env-svg-stage">
|
||||
<defs>
|
||||
<linearGradient id="chemSkyGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#bae6fd"/>
|
||||
<stop offset="60%" stop-color="#e0f2fe"/>
|
||||
<stop offset="100%" stop-color="#f0fdf4"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="chemTableGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#ffffff"/>
|
||||
<stop offset="50%" stop-color="#f1f5f9"/>
|
||||
<stop offset="100%" stop-color="#cbd5e1"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="chemFloorGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#334155"/>
|
||||
<stop offset="100%" stop-color="#1e293b"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="liquidCyanGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#38bdf8"/>
|
||||
<stop offset="100%" stop-color="#0284c7"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
|
||||
<!-- Clean Laboratory Tile Wall -->
|
||||
<rect width="1000" height="380" fill="url(#chemSkyGrad)"/>
|
||||
<g stroke="#cbd5e1" stroke-width="1.5" opacity="0.6">
|
||||
<line x1="0" y1="80" x2="1000" y2="80"/>
|
||||
<line x1="0" y1="160" x2="1000" y2="160"/>
|
||||
<line x1="0" y1="240" x2="1000" y2="240"/>
|
||||
<line x1="0" y1="320" x2="1000" y2="320"/>
|
||||
<line x1="120" y1="0" x2="120" y2="380"/>
|
||||
<line x1="240" y1="0" x2="240" y2="380"/>
|
||||
<line x1="360" y1="0" x2="360" y2="380"/>
|
||||
<line x1="480" y1="0" x2="480" y2="380"/>
|
||||
<line x1="600" y1="0" x2="600" y2="380"/>
|
||||
<line x1="720" y1="0" x2="720" y2="380"/>
|
||||
<line x1="840" y1="0" x2="840" y2="380"/>
|
||||
</g>
|
||||
|
||||
<!-- Fume Extraction Hood Canopy -->
|
||||
<rect x="180" y="0" width="640" height="40" fill="#475569"/>
|
||||
<polygon points="180,40 240,110 760,110 820,40" fill="#64748b"/>
|
||||
<rect x="240" y="110" width="520" height="8" fill="#facc15"/>
|
||||
|
||||
<!-- Emergency Eye Wash Station (Left Wall) -->
|
||||
<g transform="translate(45, 180)">
|
||||
<rect width="40" height="60" rx="4" fill="#059669"/>
|
||||
<text x="20" y="24" fill="#fff" font-size="9" font-weight="bold" text-anchor="middle">EYE</text>
|
||||
<text x="20" y="36" fill="#fff" font-size="9" font-weight="bold" text-anchor="middle">WASH</text>
|
||||
<circle cx="20" cy="48" r="6" fill="#38bdf8"/>
|
||||
<line x1="20" y1="60" x2="20" y2="120" stroke="#94a3b8" stroke-width="4"/>
|
||||
</g>
|
||||
|
||||
<!-- Industrial Lab Chemical Floor -->
|
||||
<rect x="0" y="380" width="1000" height="220" fill="url(#chemFloorGrad)"/>
|
||||
|
||||
<!-- Heavy Chemical Dispensing Workbench -->
|
||||
<g transform="translate(380, 320)">
|
||||
<rect x="0" y="60" width="540" height="150" fill="#e2e8f0" stroke="#94a3b8" stroke-width="2"/>
|
||||
<rect x="0" y="60" width="540" height="22" rx="3" fill="url(#chemTableGrad)" stroke="#64748b" stroke-width="1.5"/>
|
||||
|
||||
<!-- Chemical Glassware on Bench -->
|
||||
<!-- Round Bottom Flask -->
|
||||
<g transform="translate(70, -10)">
|
||||
<circle cx="30" cy="46" r="26" fill="rgba(255,255,255,0.2)" stroke="#fff" stroke-width="2"/>
|
||||
<circle cx="30" cy="48" r="22" fill="url(#liquidCyanGrad)"/>
|
||||
<rect x="26" y="6" width="8" height="20" fill="none" stroke="#fff" stroke-width="2"/>
|
||||
</g>
|
||||
|
||||
<!-- Erlenmeyer Conical Flask -->
|
||||
<g transform="translate(140, 10)">
|
||||
<polygon points="14,0 22,0 34,48 2,48" fill="rgba(255,255,255,0.2)" stroke="#fff" stroke-width="2"/>
|
||||
<polygon points="5,48 31,48 26,24 10,24" fill="url(#liquidCyanGrad)"/>
|
||||
</g>
|
||||
|
||||
<!-- 50L Chemical Drum -->
|
||||
<g transform="translate(230, -30)">
|
||||
<rect width="65" height="90" rx="8" fill="#0284c7" stroke="#0369a1" stroke-width="2"/>
|
||||
<line x1="0" y1="30" x2="65" y2="30" stroke="#38bdf8" stroke-width="3"/>
|
||||
<line x1="0" y1="60" x2="65" y2="60" stroke="#38bdf8" stroke-width="3"/>
|
||||
<!-- Hazard Diamond -->
|
||||
<g transform="translate(24, 38) rotate(45)">
|
||||
<rect width="18" height="18" fill="#fff" stroke="#ef4444" stroke-width="2"/>
|
||||
<text x="9" y="13" font-size="9" text-anchor="middle" transform="rotate(-45 9 9)">🧪</text>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
`;
|
||||
const bgGrind = document.getElementById('backdrop-grinding');
|
||||
const bgChem = document.getElementById('backdrop-chemical');
|
||||
if (bgGrind && bgChem) {
|
||||
if (scenarioId === 'grinding') {
|
||||
bgGrind.style.display = 'block';
|
||||
bgChem.style.display = 'none';
|
||||
} else {
|
||||
bgGrind.style.display = 'none';
|
||||
bgChem.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -888,6 +621,9 @@ class SafetyGame {
|
||||
|
||||
// Play Success Chime & Star sound after short operational run
|
||||
setTimeout(() => {
|
||||
window.soundEngine.stopAllTaskSFX();
|
||||
this.particles.stopMode();
|
||||
|
||||
window.soundEngine.playSuccessChime();
|
||||
window.soundEngine.playSafetyStarSound();
|
||||
|
||||
@@ -1054,6 +790,7 @@ class SafetyGame {
|
||||
}
|
||||
|
||||
dismissConsequenceModal() {
|
||||
window.soundEngine.stopAllTaskSFX();
|
||||
if (this.dom.consequenceOverlay) this.dom.consequenceOverlay.classList.remove('active');
|
||||
this.particles.clear();
|
||||
this.particles.setFrozen(false);
|
||||
@@ -1071,6 +808,7 @@ class SafetyGame {
|
||||
// =========================================================================
|
||||
|
||||
showFinalSummary() {
|
||||
window.soundEngine.stopAllTaskSFX();
|
||||
this.switchScreen('summary');
|
||||
window.soundEngine.playVictorySting();
|
||||
this.particles.spawnConfetti();
|
||||
|
||||
+27
-10
@@ -28,6 +28,7 @@ const SCENARIO_DATA = [
|
||||
"safety_helmet_grind",
|
||||
"face_shield_grind",
|
||||
"hearing_protection_grind",
|
||||
"fire_resistant_apron_grind",
|
||||
"leather_grinding_gloves",
|
||||
"steel_toe_boots_grind"
|
||||
],
|
||||
@@ -47,6 +48,13 @@ const SCENARIO_DATA = [
|
||||
consequenceDesc: "Angle grinding routinely exceeds 100 dB. Operating without acoustic earmuffs causes irreversible inner-ear cochlear damage and permanent tinnitus.",
|
||||
missingLabel: "Hearing Protection (NRR 30 Earmuffs)"
|
||||
},
|
||||
{
|
||||
slot: "body",
|
||||
requiredItem: "fire_resistant_apron_grind",
|
||||
consequenceTitle: "Thermal Spark Clothing Burn Hazard",
|
||||
consequenceDesc: "Without a heavy split cowhide flame-resistant apron, 1000°C angle grinder spark streams ignite clothing and cause severe upper-body thermal burns.",
|
||||
missingLabel: "Fire Resistant Leather Apron"
|
||||
},
|
||||
{
|
||||
slot: "hands",
|
||||
requiredItem: "leather_grinding_gloves",
|
||||
@@ -128,14 +136,13 @@ const SCENARIO_DATA = [
|
||||
spec: "Heavy split cowhide heat/spark resistant"
|
||||
},
|
||||
{
|
||||
id: "reflective_vest_grind",
|
||||
name: "Reflective Vest",
|
||||
id: "fire_resistant_apron_grind",
|
||||
name: "Leather Apron",
|
||||
slot: "body",
|
||||
iconType: "leather_jacket",
|
||||
iconImg: "grinding_ppe_icons_png/07_reflective_vest.png",
|
||||
isCorrect: false,
|
||||
rejectReason: "Synthetic high-visibility vests melt or ignite from dense grinder spark streams.",
|
||||
spec: "Synthetic mesh hi-vis vest"
|
||||
iconImg: "grinding_ppe_icons_png/07_fire_resistant_apron.png",
|
||||
isCorrect: true,
|
||||
spec: "Heavy split cowhide heat/spark & flame resistant apron"
|
||||
},
|
||||
{
|
||||
id: "steel_toe_boots_grind",
|
||||
@@ -149,12 +156,22 @@ const SCENARIO_DATA = [
|
||||
{
|
||||
id: "dust_mask_grind",
|
||||
name: "Dust Mask",
|
||||
slot: "body",
|
||||
iconType: "cotton_coat",
|
||||
slot: "mouth",
|
||||
iconType: "respirator",
|
||||
iconImg: "grinding_ppe_icons_png/09_dust_mask.png",
|
||||
isCorrect: false,
|
||||
rejectReason: "A simple dust mask does not protect the face or body from high-speed thermal sparks.",
|
||||
spec: "N95 particulate filter mask"
|
||||
rejectReason: "A basic nuisance dust mask provides no impact, eye, or face protection against high-speed 1000°C abrasive sparks and disc shattering debris.",
|
||||
spec: "Particulate nuisance dust mask (Non-impact)"
|
||||
},
|
||||
{
|
||||
id: "reflective_vest_grind",
|
||||
name: "Reflective Vest",
|
||||
slot: "body",
|
||||
iconType: "leather_jacket",
|
||||
iconImg: "grinding_ppe_icons_png/09_reflective_vest.png",
|
||||
isCorrect: false,
|
||||
rejectReason: "Synthetic high-visibility vests melt and catch fire instantly from dense 1000°C angle grinder spark streams. Heavy leather apron is required.",
|
||||
spec: "Synthetic mesh hi-vis vest (Non-FR)"
|
||||
}
|
||||
],
|
||||
|
||||
|
||||
Reference in New Issue
Block a user