Update game UI, add 3D gear assets, backgrounds, icons, and audio enhancements

This commit is contained in:
Vishva
2026-09-26 15:38:52 +05:30
parent d68131d500
commit db6840b870
86 changed files with 497 additions and 570 deletions
+177 -104
View File
@@ -286,130 +286,89 @@ html, body {
} }
/* ========================================================================== /* ==========================================================================
SCREEN 1: START / HERO VIEW SCREEN 1: START / HERO VIEW WITH DEDICATED HQ HOME SCREEN GRAPHIC
========================================================================== */ ========================================================================== */
.intro-card { #screen-start {
max-width: 820px; position: relative;
margin: 30px auto; width: 100%;
background: linear-gradient(180deg, #131d2e 0%, #0d1524 100%); flex: 1;
border: 1px solid var(--panel-border);
border-radius: var(--radius-lg);
padding: 40px;
text-align: center;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.5);
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center;
padding: 4px 0;
} }
.hero-graphic { .home-screen-backdrop {
margin-bottom: 20px;
position: relative;
}
.safety-shield-animated {
width: 80px;
height: 80px;
position: relative; position: relative;
width: 100%;
max-width: 1440px;
margin: 0 auto;
aspect-ratio: 1024 / 458;
border-radius: var(--radius-lg);
overflow: hidden;
border: 1px solid var(--panel-border-light);
box-shadow: 0 16px 50px rgba(0, 0, 0, 0.7);
background: #070d18;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.shield-svg { .home-bg-image {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain;
object-position: center;
display: block;
user-select: none;
pointer-events: none;
image-rendering: -webkit-optimize-contrast;
image-rendering: high-quality;
transform: translateZ(0);
} }
.shield-outline { .btn-start-hero-overlay {
fill: #1e293b; position: absolute;
stroke: var(--safety-yellow); top: 78.5%;
stroke-width: 4; left: 50.8%;
} transform: translate(-50%, -50%);
width: 24.8%;
.shield-plus { height: 9.8%;
stroke: var(--safety-yellow); min-width: 180px;
stroke-width: 6; min-height: 40px;
stroke-linecap: round; background: transparent;
} border: none;
cursor: pointer;
.hero-title { border-radius: 14px;
font-family: var(--font-display); transition: all 0.25s cubic-bezier(0.16, 1, 0.3, 1);
font-size: 2.2rem; z-index: 10;
font-weight: 700;
letter-spacing: 1px;
color: #fff;
margin-bottom: 4px;
}
.hero-subtitle {
font-size: 1.1rem;
font-weight: 700;
color: var(--safety-yellow);
letter-spacing: 3px;
margin-bottom: 24px;
text-transform: uppercase;
}
.mission-briefing {
background: rgba(0, 0, 0, 0.35);
border: 1px solid var(--panel-border);
border-radius: var(--radius-md);
padding: 24px;
margin-bottom: 28px;
text-align: center;
max-width: 700px;
}
.mission-briefing h3 {
font-size: 1.1rem;
font-weight: 700;
color: #fff;
margin-bottom: 8px;
}
.mission-briefing p {
color: var(--text-muted);
font-size: 0.92rem;
margin-bottom: 18px;
line-height: 1.5;
}
.learning-flow-banner {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
flex-wrap: wrap; text-decoration: none;
gap: 10px; outline: none;
background: #090f19;
padding: 12px 18px;
border-radius: var(--radius-md);
border: 1px solid var(--panel-border);
} }
.flow-item { .btn-hero-hover-text {
font-family: var(--font-mono); opacity: 0;
font-size: 0.78rem; font-family: var(--font-display);
font-weight: 700; font-size: 1.15rem;
color: var(--safety-yellow); font-weight: 900;
color: #000;
letter-spacing: 1px;
transition: opacity 0.2s ease;
user-select: none;
} }
.flow-arrow { .btn-start-hero-overlay:hover {
color: var(--text-dim); background: rgba(250, 204, 21, 0.12);
font-size: 0.85rem; box-shadow: 0 0 40px rgba(250, 204, 21, 0.55);
transform: translate(-50%, -50%) scale(1.015);
} }
.start-controls { .btn-start-hero-overlay:active {
display: flex; transform: translate(-50%, -50%) scale(0.985);
flex-direction: column;
align-items: center;
gap: 16px;
}
.controls-hint {
font-size: 0.8rem;
color: var(--text-dim);
} }
/* Common Button Styles */ /* Common Button Styles */
@@ -714,16 +673,109 @@ html, body {
pointer-events: none; pointer-events: none;
} }
.scene-img-backdrop {
position: relative;
width: 100%;
height: 100%;
}
.env-stage-photo {
width: 100%;
height: 100%;
object-fit: cover;
object-position: center 60%;
display: block;
filter: contrast(1.03) brightness(0.98);
image-rendering: -webkit-optimize-contrast;
image-rendering: high-quality;
transform: translateZ(0);
backface-visibility: hidden;
transition: object-position 0.3s ease;
}
.stage-chemical .env-stage-photo {
object-position: center 48%;
}
.stage-grinding .env-stage-photo {
object-position: center 58%;
}
.env-stage-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: radial-gradient(circle at 50% 55%, rgba(0, 0, 0, 0.0) 0%, rgba(0, 0, 0, 0.35) 100%), linear-gradient(180deg, rgba(0, 0, 0, 0.2) 0%, transparent 35%, rgba(0, 0, 0, 0.35) 100%);
pointer-events: none;
}
.avatar-wrapper { .avatar-wrapper {
position: relative; position: relative;
z-index: 10; z-index: 10;
width: 280px; width: 290px;
height: 440px; height: 440px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
} }
.worker-svg {
width: 100%;
height: 100%;
overflow: visible;
animation: characterAuraPulse 3s ease-in-out infinite;
transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1);
}
.worker-svg image {
transition: opacity 0.2s ease-in-out;
}
@keyframes characterAuraPulse {
0% {
filter: drop-shadow(0 0 5px rgba(56, 189, 248, 0.45))
drop-shadow(0 0 12px rgba(56, 189, 248, 0.2))
drop-shadow(0 14px 24px rgba(0, 0, 0, 0.6));
}
50% {
filter: drop-shadow(0 0 10px rgba(56, 189, 248, 0.8))
drop-shadow(0 0 22px rgba(56, 189, 248, 0.45))
drop-shadow(0 0 34px rgba(56, 189, 248, 0.25))
drop-shadow(0 14px 24px rgba(0, 0, 0, 0.65));
}
100% {
filter: drop-shadow(0 0 5px rgba(56, 189, 248, 0.45))
drop-shadow(0 0 12px rgba(56, 189, 248, 0.2))
drop-shadow(0 14px 24px rgba(0, 0, 0, 0.6));
}
}
/* Specific Equipped PPE Item Electric Cyan/Blue Glow Pop */
.gear-pop-glow {
animation: ppeGearPopGlow 0.55s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}
@keyframes ppeGearPopGlow {
0% {
filter: drop-shadow(0 0 16px #38bdf8) drop-shadow(0 0 32px #0284c7);
opacity: 0.2;
transform: scale(1.04);
transform-origin: center center;
}
40% {
filter: drop-shadow(0 0 20px #38bdf8) drop-shadow(0 0 36px #38bdf8);
opacity: 1;
transform: scale(1.02);
}
100% {
filter: url(#gearShadow);
opacity: 1;
transform: scale(1);
}
}
.avatar-step-container { .avatar-step-container {
position: relative; position: relative;
width: 100%; width: 100%;
@@ -1048,13 +1100,34 @@ html, body {
color: var(--safety-cyan); color: var(--safety-cyan);
} }
/* 3x3 Grid Layout (Square Cards) */ /* 3-Column Grid Layout for 10 Locker Items */
.ppe-locker-grid { .ppe-locker-grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 10px; gap: 8px;
width: 100%; width: 100%;
margin-bottom: auto; margin-bottom: auto;
max-height: calc(100vh - 265px);
overflow-y: auto;
padding-right: 2px;
}
.ppe-locker-grid::-webkit-scrollbar {
width: 5px;
}
.ppe-locker-grid::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.25);
border-radius: 4px;
}
.ppe-locker-grid::-webkit-scrollbar-thumb {
background: #1e3556;
border-radius: 4px;
}
.ppe-locker-grid::-webkit-scrollbar-thumb:hover {
background: var(--safety-cyan);
} }
.ppe-item-card { .ppe-item-card {
@@ -1066,11 +1139,11 @@ html, body {
background: #09121f; background: #09121f;
border: 1.5px solid #162740; border: 1.5px solid #162740;
border-radius: var(--radius-md); border-radius: var(--radius-md);
padding: 8px 4px 6px 4px; padding: 6px 3px 5px 3px;
cursor: pointer; cursor: pointer;
transition: var(--transition-normal); transition: var(--transition-normal);
position: relative; position: relative;
aspect-ratio: 1 / 1.18; aspect-ratio: 1 / 1.15;
width: 100%; width: 100%;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35);
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 286 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 971 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 482 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 902 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 380 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 388 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 360 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 382 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 399 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 387 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 432 KiB

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 374 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

+18 -40
View File
@@ -8,6 +8,10 @@
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@600;700&family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@600;700&family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@500;700&display=swap" rel="stylesheet">
<link rel="preload" as="image" href="images/home_page_bg.png">
<link rel="preload" as="image" href="images/background_grinding.jpg">
<link rel="preload" as="image" href="images/background_chemical.jpg">
<link rel="preload" as="image" href="images/worker_base.png">
</head> </head>
<body> <body>
<!-- App Container --> <!-- App Container -->
@@ -69,44 +73,11 @@
<!-- SCREEN 1: START / INTRO SCREEN --> <!-- SCREEN 1: START / INTRO SCREEN -->
<section id="screen-start" class="screen-view active"> <section id="screen-start" class="screen-view active">
<div class="intro-card"> <div class="home-screen-backdrop">
<div class="hero-graphic"> <img src="images/home_page_bg.png" alt="PPE Safety Challenge" class="home-bg-image" />
<div class="safety-shield-animated"> <button id="btn-start-game" class="btn-start-hero-overlay" aria-label="Start Challenge" title="Click or Press Space to Start Challenge">
<svg viewBox="0 0 100 100" class="shield-svg"> <span class="btn-hero-hover-text">▶ START CHALLENGE</span>
<polygon points="50,5 92,20 92,55 50,95 8,55 8,20" class="shield-outline" /> </button>
<path d="M50 25 L50 75 M25 50 L75 50" class="shield-plus" />
</svg>
<div class="pulse-ring"></div>
</div>
</div>
<h1 class="hero-title">PPE SAFETY CHALLENGE</h1>
<p class="hero-subtitle">“Choose Right. Work Safe.”</p>
<div class="mission-briefing">
<h3>Interactive Workplace Safety Mission</h3>
<p>Select the correct Personal Protective Equipment (PPE) for critical industrial tasks. Learn how proper gear prevents severe workplace injuries before starting dangerous operations.</p>
<div class="learning-flow-banner">
<span class="flow-item">🔍 SEE SITUATION</span>
<span class="flow-arrow">➔</span>
<span class="flow-item">⚠️ IDENTIFY HAZARD</span>
<span class="flow-arrow">➔</span>
<span class="flow-item">🦺 SELECT PPE</span>
<span class="flow-arrow">➔</span>
<span class="flow-item">⚙️ START TASK</span>
</div>
</div>
<div class="start-controls">
<button id="btn-start-game" class="btn btn-primary btn-large glow-effect">
<span class="btn-icon-large">▶</span>
<span class="btn-text">START CHALLENGE</span>
</button>
<div class="controls-hint">
<span>⌨️ Mouse Click or Number Keys (1-9) to equip • Spacebar to Start Task</span>
</div>
</div>
</div> </div>
</section> </section>
@@ -207,9 +178,16 @@
<!-- Scene canvas for particles/sparks/chemicals --> <!-- Scene canvas for particles/sparks/chemicals -->
<canvas id="stage-fx-canvas" class="stage-fx-canvas"></canvas> <canvas id="stage-fx-canvas" class="stage-fx-canvas"></canvas>
<!-- Dynamic Background Scenery Layer --> <!-- Pre-rendered Dynamic Background Scenery Layer -->
<div class="scene-backdrop" id="scene-backdrop"> <div class="scene-backdrop" id="scene-backdrop">
<!-- Dynamically populated based on scenario --> <div class="scene-img-backdrop backdrop-grinding" id="backdrop-grinding">
<img src="images/background_grinding.jpg" alt="Angle Grinding Workshop" class="env-stage-photo" fetchpriority="high" />
<div class="env-stage-overlay"></div>
</div>
<div class="scene-img-backdrop backdrop-chemical" id="backdrop-chemical" style="display: none;">
<img src="images/background_chemical.jpg" alt="Chemical Dispensing Station" class="env-stage-photo" />
<div class="env-stage-overlay"></div>
</div>
</div> </div>
<!-- Worker Avatar Interactive SVG Layer --> <!-- Worker Avatar Interactive SVG Layer -->
+81 -18
View File
@@ -77,10 +77,19 @@ class SoundEngine {
// VOLUME & MUTE CONTROLS // VOLUME & MUTE CONTROLS
// ========================================== // ==========================================
get isMasterMuted() {
return this.isMuted;
}
toggleMasterMute() {
this.setMasterMute(!this.isMuted);
return this.isMuted;
}
setMasterMute(muted) { setMasterMute(muted) {
this.isMuted = muted; this.isMuted = !!muted;
if (this.masterGain && this.ctx) { 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; let step = 0;
this.musicInterval = setInterval(() => { const playStep = () => {
if (!this.isMusicPlaying || !this.ctx || this.isMuted) return; if (!this.isMusicPlaying || !this.ctx || this.isMuted) return;
const current = progression[step % progression.length]; const current = progression[step % progression.length];
@@ -247,7 +256,27 @@ class SoundEngine {
}); });
step++; 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() { stopBackgroundMusic() {
@@ -265,6 +294,7 @@ class SoundEngine {
setAmbientScenario(scenarioId) { setAmbientScenario(scenarioId) {
this.ensureContext(); this.ensureContext();
this.stopAmbient(); this.stopAmbient();
this.stopAllTaskSFX();
if (!this.ctx || !this.isAmbientEnabled) return; if (!this.ctx || !this.isAmbientEnabled) return;
if (scenarioId === 'grinding') { if (scenarioId === 'grinding') {
@@ -609,19 +639,37 @@ class SoundEngine {
stopGrinderSFX() { stopGrinderSFX() {
if (this.activeGrinderNode && this.ctx) { if (this.activeGrinderNode && this.ctx) {
const t = this.ctx.currentTime; const node = this.activeGrinderNode;
this.activeGrinderNode = null;
try { try {
this.activeGrinderNode.gain.gain.linearRampToValueAtTime(0.001, t + 0.1); const t = this.ctx.currentTime;
this.activeGrinderNode.noiseGain.gain.linearRampToValueAtTime(0.001, t + 0.1); 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(() => { setTimeout(() => {
try { try {
this.activeGrinderNode.osc.stop(); if (node.osc) {
this.activeGrinderNode.noise.stop(); node.osc.stop();
node.osc.disconnect();
}
if (node.noise) {
node.noise.stop();
node.noise.disconnect();
}
} catch (e) {} } catch (e) {}
this.activeGrinderNode = null; }, 60);
}, 120);
} catch (e) { } 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() { stopLiquidTransferSFX() {
if (this.activeLiquidNode && this.ctx) { if (this.activeLiquidNode && this.ctx) {
const t = this.ctx.currentTime; const node = this.activeLiquidNode;
this.activeLiquidNode = null;
try { 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(() => { setTimeout(() => {
try { try {
this.activeLiquidNode.noise.stop(); if (node.noise) {
node.noise.stop();
node.noise.disconnect();
}
} catch (e) {} } catch (e) {}
this.activeLiquidNode = null; }, 60);
}, 120);
} catch (e) { } catch (e) {
this.activeLiquidNode = null; try {
if (node.noise) { node.noise.stop(); node.noise.disconnect(); }
} catch (err) {}
} }
} }
} }
stopAllTaskSFX() {
this.stopGrinderSFX();
this.stopLiquidTransferSFX();
}
playChemicalSplashSFX() { playChemicalSplashSFX() {
this.ensureContext(); this.ensureContext();
if (!this.ctx || this.isMuted) return; if (!this.ctx || this.isMuted) return;
+148 -90
View File
@@ -1,7 +1,7 @@
/** /**
* PPE SAFETY CHALLENGE - DYNAMIC 2D WORKER AVATAR SYSTEM * PPE SAFETY CHALLENGE - REALISTIC 3D WORKER AVATAR SYSTEM
* Uses the step-by-step transparent character images from ppe_character_steps_transparent_png! * Uses the exact 3D realistic character image provided by the user (images/worker_base.png),
* Dynamically switches character states as safety gear is equipped. * with precision anatomical PPE overlays for both Grinding and Chemical scenarios.
*/ */
class WorkerAvatar { class WorkerAvatar {
@@ -18,7 +18,7 @@ class WorkerAvatar {
equip(itemId) { equip(itemId) {
this.equippedItems.add(itemId); this.equippedItems.add(itemId);
this.updateLayers(); this.updateLayers(itemId);
} }
unequip(itemId) { unequip(itemId) {
@@ -40,107 +40,165 @@ class WorkerAvatar {
x: relX, x: relX,
y: relY, y: relY,
eyeX: relX, eyeX: relX,
eyeY: relY - 130, eyeY: relY - 135,
handX: relX + 55, handX: relX + 65,
handY: relY + 30 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() { render() {
if (!this.container) return; if (!this.container) return;
this.container.innerHTML = ` this.container.innerHTML = `
<div class="avatar-step-container" id="avatar-step-container"> <svg viewBox="0 0 682 1024" class="worker-svg" id="worker-avatar-svg" xmlns="http://www.w3.org/2000/svg">
<!-- Floor Contact Shadow --> <defs>
<div class="avatar-floor-shadow"></div> <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>
<!-- Dynamic Character Step Image --> <!-- Realistic Soft Floor Contact & Ambient Occlusion Shadow -->
<img src="images/steps/01_no_ppe.png" alt="Worker Character" class="avatar-step-img" id="avatar-step-img" /> <g id="layer-floor-shadow" filter="url(#softFloorBlur)">
</div> <!-- 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(); this.updateLayers();
} }
updateLayers() { updateLayers(newlyEquippedId = null) {
if (!this.container) return; if (!this.container) return;
const imgEl = document.getElementById('avatar-step-img'); const allGearIds = [
if (imgEl) { 'safety_helmet_chem', 'safety_helmet_grind',
const targetSrc = this.getStepImagePath(); 'chem_splash_goggles', 'safety_goggles_grind', 'face_shield_chem', 'face_shield_grind', 'tinted_sunglasses', 'dust_mask_grind',
if (imgEl.getAttribute('src') !== targetSrc) { 'hearing_protection_chem', 'hearing_protection_grind',
imgEl.src = targetSrc; '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
View File
@@ -94,6 +94,9 @@ class SafetyGame {
this.bindEvents(); this.bindEvents();
this.updateHUD(); this.updateHUD();
// Enable background music starting from home page on first gesture
window.soundEngine.enableAudioOnFirstGesture();
// Listen for freeze frame event from particle collision // Listen for freeze frame event from particle collision
window.addEventListener('hazard-freeze-triggered', (e) => { window.addEventListener('hazard-freeze-triggered', (e) => {
this.handleFreezeFrameAlert(e.detail.projectile); this.handleFreezeFrameAlert(e.detail.projectile);
@@ -101,17 +104,14 @@ class SafetyGame {
} }
bindEvents() { bindEvents() {
// Start Game from Intro // Start Game from Intro (Instantaneous Transition, Zero Delay)
if (this.dom.btnStartGame) { if (this.dom.btnStartGame) {
this.dom.btnStartGame.addEventListener('click', () => { this.dom.btnStartGame.addEventListener('click', () => {
window.soundEngine.ensureContext(); window.soundEngine.ensureContext();
window.soundEngine.playStartIntroSting(); window.soundEngine.playStartIntroSting();
window.soundEngine.startBackgroundMusic();
this.loadScenario(0);
this.switchScreen('game'); this.switchScreen('game');
setTimeout(() => {
window.soundEngine.startBackgroundMusic();
this.loadScenario(0);
}, 800);
}); });
} }
@@ -141,6 +141,7 @@ class SafetyGame {
if (this.dom.btnNextScenario) { if (this.dom.btnNextScenario) {
this.dom.btnNextScenario.addEventListener('click', () => { this.dom.btnNextScenario.addEventListener('click', () => {
window.soundEngine.playButtonClick(); window.soundEngine.playButtonClick();
window.soundEngine.stopAllTaskSFX();
this.dom.successOverlay.classList.remove('active'); this.dom.successOverlay.classList.remove('active');
this.particles.stopMode(); 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) { if (this.dom.btnSoundToggle) {
this.dom.btnSoundToggle.addEventListener('click', () => { this.dom.btnSoundToggle.addEventListener('click', () => {
window.soundEngine.ensureContext(); window.soundEngine.ensureContext();
const isMuted = !window.soundEngine.isMasterMuted; const isMuted = window.soundEngine.toggleMasterMute();
window.soundEngine.setMasterMute(isMuted); const iconSpan = this.dom.btnSoundToggle.querySelector('span') || this.dom.btnSoundToggle;
this.dom.btnSoundToggle.querySelector('span').textContent = isMuted ? '🔇' : '🔊'; iconSpan.textContent = isMuted ? '🔇' : '🔊';
if (this.dom.toggleMasterMute) this.dom.toggleMasterMute.checked = !isMuted; if (this.dom.toggleMasterMute) this.dom.toggleMasterMute.checked = !isMuted;
}); });
} }
@@ -192,7 +193,8 @@ class SafetyGame {
const currentVol = window.soundEngine.musicVolume; const currentVol = window.soundEngine.musicVolume;
const newVol = currentVol > 0.01 ? 0 : 0.25; const newVol = currentVol > 0.01 ? 0 : 0.25;
window.soundEngine.setMusicVolume(newVol); 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.sliderMusicVol) this.dom.sliderMusicVol.value = Math.round(newVol * 100);
if (this.dom.musicVolNum) this.dom.musicVolNum.textContent = `${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) => { window.addEventListener('keydown', (e) => {
const num = parseInt(e.key); const num = parseInt(e.key);
if (!isNaN(num) && num >= 1 && num <= 9) { if (!isNaN(num)) {
const cards = document.querySelectorAll('.ppe-item-card'); const cards = document.querySelectorAll('.ppe-item-card');
if (cards[num - 1]) { const cardIndex = num === 0 ? 9 : num - 1;
cards[num - 1].click(); 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 (e.code === 'Space' && this.dom.screenGame.classList.contains('active')) {
if (!this.dom.consequenceOverlay.classList.contains('active') && !this.dom.successOverlay.classList.contains('active')) { if (!this.dom.consequenceOverlay.classList.contains('active') && !this.dom.successOverlay.classList.contains('active')) {
e.preventDefault(); e.preventDefault();
@@ -285,6 +294,7 @@ class SafetyGame {
} }
loadScenario(index) { loadScenario(index) {
window.soundEngine.stopAllTaskSFX();
this.currentScenarioIndex = index; this.currentScenarioIndex = index;
this.currentScenario = SCENARIO_DATA[index]; this.currentScenario = SCENARIO_DATA[index];
this.scenarioFirstAttempt = true; this.scenarioFirstAttempt = true;
@@ -305,6 +315,11 @@ class SafetyGame {
if (this.dom.sceneDesc) this.dom.sceneDesc.textContent = this.currentScenario.environmentDesc; if (this.dom.sceneDesc) this.dom.sceneDesc.textContent = this.currentScenario.environmentDesc;
if (this.dom.lockerGuideText) this.dom.lockerGuideText.textContent = this.currentScenario.guideTip; 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 // Render Scenario Background
this.renderScenarioEnvironment(this.currentScenario.id); this.renderScenarioEnvironment(this.currentScenario.id);
@@ -326,298 +341,16 @@ class SafetyGame {
} }
renderScenarioEnvironment(scenarioId) { renderScenarioEnvironment(scenarioId) {
if (!this.dom.sceneBackdrop) return; const bgGrind = document.getElementById('backdrop-grinding');
const bgChem = document.getElementById('backdrop-chemical');
if (scenarioId === 'grinding') { if (bgGrind && bgChem) {
// SCENARIO 1: Vector Industrial Fabrication & Grinding Workshop (Matching Reference Image) if (scenarioId === 'grinding') {
this.dom.sceneBackdrop.innerHTML = ` bgGrind.style.display = 'block';
<svg viewBox="0 0 1000 600" preserveAspectRatio="xMidYMid slice" class="env-svg-stage"> bgChem.style.display = 'none';
<defs> } else {
<linearGradient id="grindSkyGrad" x1="0" y1="0" x2="0" y2="1"> bgGrind.style.display = 'none';
<stop offset="0%" stop-color="#3b6998"/> bgChem.style.display = 'block';
<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>
`;
} }
} }
@@ -888,6 +621,9 @@ class SafetyGame {
// Play Success Chime & Star sound after short operational run // Play Success Chime & Star sound after short operational run
setTimeout(() => { setTimeout(() => {
window.soundEngine.stopAllTaskSFX();
this.particles.stopMode();
window.soundEngine.playSuccessChime(); window.soundEngine.playSuccessChime();
window.soundEngine.playSafetyStarSound(); window.soundEngine.playSafetyStarSound();
@@ -1054,6 +790,7 @@ class SafetyGame {
} }
dismissConsequenceModal() { dismissConsequenceModal() {
window.soundEngine.stopAllTaskSFX();
if (this.dom.consequenceOverlay) this.dom.consequenceOverlay.classList.remove('active'); if (this.dom.consequenceOverlay) this.dom.consequenceOverlay.classList.remove('active');
this.particles.clear(); this.particles.clear();
this.particles.setFrozen(false); this.particles.setFrozen(false);
@@ -1071,6 +808,7 @@ class SafetyGame {
// ========================================================================= // =========================================================================
showFinalSummary() { showFinalSummary() {
window.soundEngine.stopAllTaskSFX();
this.switchScreen('summary'); this.switchScreen('summary');
window.soundEngine.playVictorySting(); window.soundEngine.playVictorySting();
this.particles.spawnConfetti(); this.particles.spawnConfetti();
+27 -10
View File
@@ -28,6 +28,7 @@ const SCENARIO_DATA = [
"safety_helmet_grind", "safety_helmet_grind",
"face_shield_grind", "face_shield_grind",
"hearing_protection_grind", "hearing_protection_grind",
"fire_resistant_apron_grind",
"leather_grinding_gloves", "leather_grinding_gloves",
"steel_toe_boots_grind" "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.", 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)" 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", slot: "hands",
requiredItem: "leather_grinding_gloves", requiredItem: "leather_grinding_gloves",
@@ -128,14 +136,13 @@ const SCENARIO_DATA = [
spec: "Heavy split cowhide heat/spark resistant" spec: "Heavy split cowhide heat/spark resistant"
}, },
{ {
id: "reflective_vest_grind", id: "fire_resistant_apron_grind",
name: "Reflective Vest", name: "Leather Apron",
slot: "body", slot: "body",
iconType: "leather_jacket", iconType: "leather_jacket",
iconImg: "grinding_ppe_icons_png/07_reflective_vest.png", iconImg: "grinding_ppe_icons_png/07_fire_resistant_apron.png",
isCorrect: false, isCorrect: true,
rejectReason: "Synthetic high-visibility vests melt or ignite from dense grinder spark streams.", spec: "Heavy split cowhide heat/spark & flame resistant apron"
spec: "Synthetic mesh hi-vis vest"
}, },
{ {
id: "steel_toe_boots_grind", id: "steel_toe_boots_grind",
@@ -149,12 +156,22 @@ const SCENARIO_DATA = [
{ {
id: "dust_mask_grind", id: "dust_mask_grind",
name: "Dust Mask", name: "Dust Mask",
slot: "body", slot: "mouth",
iconType: "cotton_coat", iconType: "respirator",
iconImg: "grinding_ppe_icons_png/09_dust_mask.png", iconImg: "grinding_ppe_icons_png/09_dust_mask.png",
isCorrect: false, isCorrect: false,
rejectReason: "A simple dust mask does not protect the face or body from high-speed thermal sparks.", 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: "N95 particulate filter mask" 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)"
} }
], ],
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 42 KiB