Add certificate template, shield logo, audio tracks, and enhance game UI/UX
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1642
-496
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 397 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 971 KiB After Width: | Height: | Size: 2.2 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 39 KiB |
+335
-124
@@ -20,12 +20,8 @@
|
||||
<!-- Top Navigation / Header Bar -->
|
||||
<header class="top-bar">
|
||||
<div class="brand-badge">
|
||||
<div class="safety-helmet-icon">
|
||||
<svg viewBox="0 0 36 36" class="helmet-svg">
|
||||
<path d="M 6 22 C 6 12 30 12 30 22 L 34 24 C 34 26 2 26 2 24 Z" fill="#facc15" stroke="#ca8a04" stroke-width="1.5"/>
|
||||
<path d="M 14 11 L 22 11 L 21 23 L 15 23 Z" fill="#eab308"/>
|
||||
<rect x="3" y="23" width="30" height="3" rx="1.5" fill="#ca8a04"/>
|
||||
</svg>
|
||||
<div class="safety-shield-logo-box">
|
||||
<img src="images/safety_shield_logo.png" alt="Safety Shield Logo" class="brand-shield-img" />
|
||||
</div>
|
||||
<div class="brand-text">
|
||||
<span class="game-title">PPE SAFETY CHALLENGE</span>
|
||||
@@ -48,6 +44,13 @@
|
||||
</div>
|
||||
|
||||
<div class="hud-right">
|
||||
<!-- 30-Second Countdown Timer -->
|
||||
<div class="timer-display" id="hud-timer-box">
|
||||
<span class="timer-icon">⏱️</span>
|
||||
<span class="timer-label">TIME</span>
|
||||
<span class="timer-val" id="hud-timer-val">00:30</span>
|
||||
</div>
|
||||
|
||||
<div class="score-display">
|
||||
<span class="score-label">SAFETY SCORE</span>
|
||||
<span class="score-val" id="hud-score">0</span>
|
||||
@@ -55,13 +58,16 @@
|
||||
<span class="score-stars-sub" id="hud-stars">☆</span>
|
||||
</div>
|
||||
|
||||
<!-- Audio & Settings Controls -->
|
||||
<!-- Audio, Fullscreen & Settings Controls -->
|
||||
<button id="btn-sound-toggle" class="btn-icon-hud" title="Sound Effects (S)" aria-label="Sound Toggle">
|
||||
<span>🔊</span>
|
||||
</button>
|
||||
<button id="btn-music-toggle" class="btn-icon-hud" title="Music Toggle (M)" aria-label="Music Toggle">
|
||||
<span>🎵</span>
|
||||
</button>
|
||||
<button id="btn-fullscreen-toggle" class="btn-icon-hud" title="Toggle Fullscreen (F)" aria-label="Toggle Fullscreen">
|
||||
<span id="fullscreen-icon">⛶</span>
|
||||
</button>
|
||||
<button id="btn-audio-settings" class="btn-icon-hud" title="Audio Settings (O)" aria-label="Audio Settings">
|
||||
<span>⚙️</span>
|
||||
</button>
|
||||
@@ -107,24 +113,9 @@
|
||||
<div class="worker-protection-card">
|
||||
<div class="card-subtitle-small">WORKER PROTECTION STATUS</div>
|
||||
<div class="protection-status-content">
|
||||
<!-- Blue Wireframe Silhouette -->
|
||||
<!-- Blue 3D Hologram Silhouette (Exact Match to 2nd Ref) -->
|
||||
<div class="body-silhouette-box">
|
||||
<svg viewBox="0 0 100 200" class="wireframe-body-svg">
|
||||
<!-- Head -->
|
||||
<circle cx="50" cy="24" r="14" fill="none" stroke="#38bdf8" stroke-width="2" opacity="0.85"/>
|
||||
<!-- Neck -->
|
||||
<line x1="50" y1="38" x2="50" y2="46" stroke="#38bdf8" stroke-width="2" opacity="0.85"/>
|
||||
<!-- Shoulders & Torso -->
|
||||
<path d="M 24 55 L 76 55 L 68 115 L 32 115 Z" fill="none" stroke="#38bdf8" stroke-width="2" opacity="0.85"/>
|
||||
<!-- Left Arm -->
|
||||
<path d="M 24 55 L 12 105 L 10 140" fill="none" stroke="#38bdf8" stroke-width="2" opacity="0.85"/>
|
||||
<!-- Right Arm -->
|
||||
<path d="M 76 55 L 88 105 L 90 140" fill="none" stroke="#38bdf8" stroke-width="2" opacity="0.85"/>
|
||||
<!-- Left Leg -->
|
||||
<path d="M 38 115 L 34 165 L 32 195" fill="none" stroke="#38bdf8" stroke-width="2" opacity="0.85"/>
|
||||
<!-- Right Leg -->
|
||||
<path d="M 62 115 L 66 165 L 68 195" fill="none" stroke="#38bdf8" stroke-width="2" opacity="0.85"/>
|
||||
</svg>
|
||||
<img src="images/worker_hologram_silhouette.png" alt="Worker Hologram Silhouette" class="hologram-silhouette-img" />
|
||||
</div>
|
||||
|
||||
<!-- Status Rows -->
|
||||
@@ -195,53 +186,139 @@
|
||||
<!-- Injected by avatar.js -->
|
||||
</div>
|
||||
|
||||
<!-- Freeze Frame / Critical Warning Alert Overlay -->
|
||||
<!-- Freeze Frame / Critical Warning Alert Cyber HUD Modal (Exact Match to Reference UI) -->
|
||||
<div class="consequence-freeze-overlay" id="consequence-overlay">
|
||||
<div class="freeze-banner">
|
||||
<div class="freeze-header">
|
||||
<span class="hazard-flasher" id="hazard-flasher-badge">⚠️ STOP! CRITICAL HAZARDS DETECTED</span>
|
||||
<div class="cyber-consequence-modal">
|
||||
<!-- Decorative Hazard Side Vents -->
|
||||
<div class="cyber-flange flange-left hazard-flange">
|
||||
<span class="hazard-vent-bar"></span>
|
||||
<span class="hazard-vent-bar"></span>
|
||||
<span class="hazard-vent-bar"></span>
|
||||
<span class="hazard-vent-bar"></span>
|
||||
</div>
|
||||
<h3 class="consequence-title" id="consequence-title">Critical Protection Gaps Detected</h3>
|
||||
<p class="consequence-desc" id="consequence-desc">
|
||||
Multiple required protective gear items are missing or improper for this task. Unprotected body zones are exposed to severe workplace trauma.
|
||||
<div class="cyber-flange flange-right hazard-flange">
|
||||
<span class="hazard-vent-bar"></span>
|
||||
<span class="hazard-vent-bar"></span>
|
||||
<span class="hazard-vent-bar"></span>
|
||||
<span class="hazard-vent-bar"></span>
|
||||
</div>
|
||||
|
||||
<!-- Red Corner Accent Notches -->
|
||||
<div class="cyber-corner-notch red-notch-tl"></div>
|
||||
<div class="cyber-corner-notch red-notch-tr"></div>
|
||||
<div class="cyber-corner-notch red-notch-bl"></div>
|
||||
<div class="cyber-corner-notch red-notch-br"></div>
|
||||
|
||||
<!-- Top Triangle Hazard Badge -->
|
||||
<div class="hud-hazard-badge-wrapper">
|
||||
<div class="hud-hazard-triangle">
|
||||
<svg viewBox="0 0 60 52" class="hazard-triangle-svg">
|
||||
<path d="M 30 4 L 56 48 L 4 48 Z" fill="#180404" stroke="#ff2244" stroke-width="3.5" stroke-linejoin="round" />
|
||||
<path d="M 30 10 L 51 45 L 9 45 Z" fill="#ff2244" opacity="0.18" />
|
||||
<path d="M 30 18 L 30 32" stroke="#ffffff" stroke-width="4.2" stroke-linecap="round" />
|
||||
<circle cx="30" cy="39" r="2.4" fill="#ffffff" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Inner Content -->
|
||||
<div class="cyber-consequence-body">
|
||||
<!-- Stop Flasher Pill -->
|
||||
<div class="cyber-hazard-flasher" id="hazard-flasher-badge">
|
||||
<span class="flasher-icon">⚠️</span>
|
||||
<span class="flasher-text" id="hazard-flasher-text">STOP! 5 SAFETY HAZARDS DETECTED</span>
|
||||
</div>
|
||||
|
||||
<h2 class="cyber-consequence-title" id="consequence-title">
|
||||
<span class="title-red">CRITICAL</span> <span class="title-white">PROTECTION GAPS DETECTED</span>
|
||||
</h2>
|
||||
|
||||
<p class="cyber-consequence-desc" id="consequence-desc">
|
||||
Operating without complete PPE exposes multiple body zones to direct, severe workplace injuries. All required safety equipment must be equipped before starting.
|
||||
</p>
|
||||
|
||||
<div class="missing-items-box">
|
||||
<div class="missing-box-header">
|
||||
<span class="box-label">⚠️ ALL REQUIRED CORRECTIONS (<span id="missing-count-num">0</span> ISSUES):</span>
|
||||
<div class="cyber-missing-box">
|
||||
<div class="cyber-missing-header">
|
||||
<span class="missing-label-icon">⚠️</span>
|
||||
<span class="missing-label-text">ALL REQUIRED CORRECTIONS (<span id="missing-count-num">0</span> ISSUES):</span>
|
||||
</div>
|
||||
<div class="missing-hazard-cards" id="consequence-hazard-list">
|
||||
<!-- Dynamically rendered list of all missing & incorrect items -->
|
||||
<div class="cyber-hazard-list" id="consequence-hazard-list">
|
||||
<!-- Dynamically populated hazard items -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="btn-fix-ppe" class="btn btn-warning glow-effect">
|
||||
<span>🔧 FIX ALL WORKER PPE & RETRY</span>
|
||||
<button id="btn-fix-ppe" class="btn-cyber-fix">
|
||||
<span class="btn-fix-icon">🔧</span>
|
||||
<span>FIX ALL WORKER PPE & RETRY ➔</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success Overlay Banner -->
|
||||
<!-- Success Overlay Cyber HUD Modal (Exact Match to Reference UI) -->
|
||||
<div class="success-overlay" id="success-overlay">
|
||||
<div class="success-banner">
|
||||
<div class="success-badge">
|
||||
<span class="badge-icon">✅</span>
|
||||
<span class="badge-title" id="success-title">SAFE TO START</span>
|
||||
<div class="cyber-success-modal">
|
||||
<!-- Decorative Side Vents -->
|
||||
<div class="cyber-flange flange-left">
|
||||
<span class="vent-bar"></span>
|
||||
<span class="vent-bar"></span>
|
||||
<span class="vent-bar"></span>
|
||||
<span class="vent-bar"></span>
|
||||
</div>
|
||||
<p class="success-desc" id="success-desc">
|
||||
Correct PPE selected. Your eyes, face, hands, hearing and feet are fully protected against all task hazards.
|
||||
<div class="cyber-flange flange-right">
|
||||
<span class="vent-bar"></span>
|
||||
<span class="vent-bar"></span>
|
||||
<span class="vent-bar"></span>
|
||||
<span class="vent-bar"></span>
|
||||
</div>
|
||||
|
||||
<!-- Corner Accent Notches -->
|
||||
<div class="cyber-corner-notch notch-tl"></div>
|
||||
<div class="cyber-corner-notch notch-tr"></div>
|
||||
<div class="cyber-corner-notch notch-bl"></div>
|
||||
<div class="cyber-corner-notch notch-br"></div>
|
||||
|
||||
<!-- Top Checkmark HUD Badge with Segmented Radar Rings -->
|
||||
<div class="hud-check-badge-wrapper">
|
||||
<div class="hud-radar-ring">
|
||||
<svg viewBox="0 0 100 100" class="radar-svg">
|
||||
<circle cx="50" cy="50" r="46" fill="none" stroke="rgba(0, 255, 136, 0.4)" stroke-width="1.5" stroke-dasharray="4, 4" />
|
||||
<circle cx="50" cy="50" r="41" fill="none" stroke="rgba(0, 255, 136, 0.65)" stroke-width="1.5" stroke-dasharray="14, 7, 7, 7" />
|
||||
<circle cx="50" cy="50" r="36" fill="none" stroke="rgba(0, 255, 136, 0.35)" stroke-width="1" />
|
||||
</svg>
|
||||
</div>
|
||||
<div class="hud-check-circle">
|
||||
<svg viewBox="0 0 32 32" class="hud-check-svg">
|
||||
<path d="M 8 16 L 14 22 L 24 10" fill="none" stroke="#ffffff" stroke-width="3.8" stroke-linecap="round" stroke-linejoin="round" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Inner Content -->
|
||||
<div class="cyber-success-body">
|
||||
<h2 class="cyber-success-title" id="success-title">
|
||||
<span class="title-green">SAFE</span> <span class="title-white">TO START</span>
|
||||
</h2>
|
||||
<div class="cyber-title-dash"></div>
|
||||
|
||||
<p class="cyber-success-desc" id="success-desc">
|
||||
Your eyes, face, hands, hearing and feet are fully protected from all hazards of this grinding task.
|
||||
</p>
|
||||
<div class="points-awarded-pill">
|
||||
<span class="star-icon">⭐</span>
|
||||
<span class="points-text" id="awarded-points-text">+100 Safety Points</span>
|
||||
|
||||
<div class="cyber-points-pill">
|
||||
<span class="points-star">★</span>
|
||||
<span class="points-value" id="awarded-points-text">+100 Safety Points</span>
|
||||
<span class="points-star">★</span>
|
||||
</div>
|
||||
<button id="btn-next-scenario" class="btn btn-success glow-effect">
|
||||
|
||||
<button id="btn-next-scenario" class="btn-cyber-next">
|
||||
<span id="btn-next-scenario-text">NEXT SCENARIO ➔</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: PPE Selection Locker (3x3 Grid) & Action Buttons -->
|
||||
<aside class="sidebar-locker-panel card-panel">
|
||||
@@ -276,76 +353,184 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- SCREEN 3: COMPLETION / FINAL SCORE SCREEN -->
|
||||
<!-- SCREEN 3: COMPLETION / FINAL SCORE SCREEN (EXACT MATCH TO REFERENCE UI) -->
|
||||
<section id="screen-summary" class="screen-view">
|
||||
<div class="summary-card">
|
||||
<div class="summary-header">
|
||||
<div class="victory-trophy">🏆</div>
|
||||
<h1 class="summary-title">PPE SAFETY CHALLENGE COMPLETE</h1>
|
||||
<p class="summary-subtitle">“Choose Right. Work Safe.”</p>
|
||||
<div class="cyber-summary-container">
|
||||
<!-- Decorative Hazard Caution Vents on Left and Right -->
|
||||
<div class="cyber-flange flange-left summary-flange">
|
||||
<span class="gold-vent-bar"></span>
|
||||
<span class="gold-vent-bar"></span>
|
||||
<span class="gold-vent-bar"></span>
|
||||
<span class="gold-vent-bar"></span>
|
||||
</div>
|
||||
<div class="cyber-flange flange-right summary-flange">
|
||||
<span class="gold-vent-bar"></span>
|
||||
<span class="gold-vent-bar"></span>
|
||||
<span class="gold-vent-bar"></span>
|
||||
<span class="gold-vent-bar"></span>
|
||||
</div>
|
||||
|
||||
<div class="metrics-dashboard">
|
||||
<div class="metric-box">
|
||||
<span class="metric-label">FINAL SAFETY SCORE</span>
|
||||
<span class="metric-val" id="summary-score">200 / 200</span>
|
||||
<span class="metric-sub" id="summary-percentage">100% Safety Compliance</span>
|
||||
<!-- Gold Cyber Corner Accents -->
|
||||
<div class="cyber-corner-notch gold-notch-tl"></div>
|
||||
<div class="cyber-corner-notch gold-notch-tr"></div>
|
||||
<div class="cyber-corner-notch gold-notch-bl"></div>
|
||||
<div class="cyber-corner-notch gold-notch-br"></div>
|
||||
|
||||
<!-- Trophy Header Section -->
|
||||
<div class="summary-trophy-header">
|
||||
<div class="trophy-artwork-box">
|
||||
<svg viewBox="0 0 120 80" class="trophy-laurel-svg">
|
||||
<!-- Golden Laurels Left -->
|
||||
<path d="M 38 52 C 28 42, 28 26, 38 16 C 36 28, 38 40, 48 48 Z" fill="#f59e0b" opacity="0.85"/>
|
||||
<path d="M 28 42 C 20 34, 20 22, 28 14 C 27 24, 29 32, 36 38 Z" fill="#fbbf24" opacity="0.75"/>
|
||||
<!-- Golden Laurels Right -->
|
||||
<path d="M 82 52 C 92 42, 92 26, 82 16 C 84 28, 82 40, 72 48 Z" fill="#f59e0b" opacity="0.85"/>
|
||||
<path d="M 92 42 C 100 34, 100 22, 92 14 C 93 24, 91 32, 84 38 Z" fill="#fbbf24" opacity="0.75"/>
|
||||
<!-- Confetti Flakes -->
|
||||
<polygon points="20,12 24,8 26,14 22,16" fill="#facc15" opacity="0.8"/>
|
||||
<polygon points="98,10 102,6 104,12 100,14" fill="#facc15" opacity="0.8"/>
|
||||
<polygon points="14,30 18,28 17,34 13,33" fill="#fbbf24" opacity="0.7"/>
|
||||
<polygon points="106,28 110,26 109,32 105,31" fill="#fbbf24" opacity="0.7"/>
|
||||
<!-- Golden Trophy Center -->
|
||||
<path d="M 44 20 L 76 20 L 72 42 C 70 48, 50 48, 48 42 Z" fill="url(#trophyGoldGrad)"/>
|
||||
<path d="M 44 24 C 36 24, 34 34, 46 36 L 46 32 C 38 32, 38 26, 44 26 Z" fill="#f59e0b"/>
|
||||
<path d="M 76 24 C 84 24, 86 34, 74 36 L 74 32 C 82 32, 82 26, 76 26 Z" fill="#f59e0b"/>
|
||||
<rect x="56" y="44" width="8" height="12" fill="#d97706"/>
|
||||
<path d="M 48 56 L 72 56 L 76 64 L 44 64 Z" fill="url(#trophyGoldGrad)"/>
|
||||
<polygon points="60,25 62,31 68,31 63,35 65,41 60,37 55,41 57,35 52,31 58,31" fill="#ffffff" opacity="0.9"/>
|
||||
<defs>
|
||||
<linearGradient id="trophyGoldGrad" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stop-color="#fde047"/>
|
||||
<stop offset="50%" stop-color="#f59e0b"/>
|
||||
<stop offset="100%" stop-color="#b45309"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<span class="metric-label">SCENARIOS COMPLETED</span>
|
||||
<span class="metric-val">2 / 2</span>
|
||||
<span class="metric-sub">Zero Workplace Accidents</span>
|
||||
<h1 class="summary-champion-title">
|
||||
<span class="title-white">PPE SAFETY CHALLENGE</span> <span class="title-gold">COMPLETE</span>
|
||||
</h1>
|
||||
<p class="summary-champion-subtitle">“Choose Right. Work Safe.”</p>
|
||||
</div>
|
||||
<div class="metric-box">
|
||||
<span class="metric-label">SAFETY STARS</span>
|
||||
<span class="metric-val star-gold" id="summary-stars">⭐⭐</span>
|
||||
<span class="metric-sub" id="summary-rank">Master Safety Practitioner</span>
|
||||
|
||||
<!-- 3 Highlight Metric Cards -->
|
||||
<div class="summary-metrics-grid">
|
||||
<!-- Score Card (Green Accent) -->
|
||||
<div class="summary-stat-card stat-green">
|
||||
<div class="stat-icon-badge icon-shield-green">
|
||||
<svg viewBox="0 0 24 24" class="stat-svg">
|
||||
<path d="M 12 2 L 20 5 L 20 11 C 20 16.5 16.5 21 12 22 C 7.5 21 4 16.5 4 11 L 4 5 Z" fill="rgba(16, 185, 129, 0.2)" stroke="#10b981" stroke-width="2"/>
|
||||
<path d="M 8 11.5 L 11 14.5 L 16 9.5" fill="none" stroke="#10b981" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-text-group">
|
||||
<span class="stat-label">FINAL SAFETY SCORE</span>
|
||||
<span class="stat-val" id="summary-score">200 / 200</span>
|
||||
<span class="stat-sub" id="summary-percentage">100% Safety Compliance Rating</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key Takeaway Mantra -->
|
||||
<div class="takeaway-banner">
|
||||
<div class="takeaway-heading">CORE SAFETY WORKFLOW</div>
|
||||
<div class="mantra-chain">
|
||||
<div class="mantra-step">🛑 <strong>STOP</strong></div>
|
||||
<div class="mantra-arrow">➔</div>
|
||||
<div class="mantra-step">👁️ <strong>LOOK</strong></div>
|
||||
<div class="mantra-arrow">➔</div>
|
||||
<div class="mantra-step">⚠️ <strong>IDENTIFY HAZARDS</strong></div>
|
||||
<div class="mantra-arrow">➔</div>
|
||||
<div class="mantra-step">🦺 <strong>SELECT PPE</strong></div>
|
||||
<div class="mantra-arrow">➔</div>
|
||||
<div class="mantra-step">🛡️ <strong>WORK SAFELY</strong></div>
|
||||
<!-- Scenarios Completed Card (Cyan Accent) -->
|
||||
<div class="summary-stat-card stat-cyan">
|
||||
<div class="stat-icon-badge icon-clipboard-cyan">
|
||||
<svg viewBox="0 0 24 24" class="stat-svg">
|
||||
<path d="M 9 2 L 15 2 L 15 5 L 9 5 Z" fill="#0ea5e9"/>
|
||||
<rect x="5" y="4" width="14" height="18" rx="2" fill="none" stroke="#0ea5e9" stroke-width="2"/>
|
||||
<line x1="9" y1="9" x2="15" y2="9" stroke="#38bdf8" stroke-width="1.8" stroke-linecap="round"/>
|
||||
<line x1="9" y1="13" x2="15" y2="13" stroke="#38bdf8" stroke-width="1.8" stroke-linecap="round"/>
|
||||
<line x1="9" y1="17" x2="13" y2="17" stroke="#38bdf8" stroke-width="1.8" stroke-linecap="round"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="stat-text-group">
|
||||
<span class="stat-label">SCENARIOS COMPLETED</span>
|
||||
<span class="stat-val">2 / 2</span>
|
||||
<span class="stat-sub">Zero Workplace Accidents</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scenarios Review Summary Accordion -->
|
||||
<div class="scenarios-review-box">
|
||||
<h4>Safety Assessment Summary</h4>
|
||||
<div class="review-row">
|
||||
<div class="rev-icon">⚡</div>
|
||||
<div class="rev-info">
|
||||
<strong>Scenario 1: Angle Grinding Operation</strong>
|
||||
<p>Protected against flying hot sparks, high decibel noise (95+ dB), and sharp metal burrs using Full Face Shield + Glasses, Leather Grinding Gloves, Hearing Protection & Safety Boots.</p>
|
||||
<!-- Stars Card (Gold Accent) -->
|
||||
<div class="summary-stat-card stat-gold">
|
||||
<div class="stat-icon-badge icon-star-gold">
|
||||
<svg viewBox="0 0 24 24" class="stat-svg">
|
||||
<circle cx="12" cy="12" r="10" fill="none" stroke="#f59e0b" stroke-width="2"/>
|
||||
<polygon points="12,6 14,10 18.5,10.5 15,14 16,18.5 12,16 8,18.5 9,14 5.5,10.5 10,10" fill="#fbbf24"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="rev-status">✅ PASSED</div>
|
||||
<div class="stat-text-group">
|
||||
<span class="stat-label">SAFETY STARS</span>
|
||||
<span class="stat-val stat-stars-text" id="summary-stars">★★</span>
|
||||
<span class="stat-sub" id="summary-rank">Master Safety Practitioner (Zero Accidents)</span>
|
||||
</div>
|
||||
<div class="review-row">
|
||||
<div class="rev-icon">🧪</div>
|
||||
<div class="rev-info">
|
||||
<strong>Scenario 2: Industrial Chemical Transfer</strong>
|
||||
<p>Prevented chemical burn & splash injury using Chemical Splash Goggles, Heavy Nitrile Gauntlet Gloves, Chemical Resistant Apron & Rubber Safety Boots.</p>
|
||||
</div>
|
||||
<div class="rev-status">✅ PASSED</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="summary-actions">
|
||||
<button id="btn-replay" class="btn btn-primary btn-large">
|
||||
<span>🔄 REPLAY SAFETY CHALLENGE</span>
|
||||
<!-- Core Safety Workflow Banner -->
|
||||
<div class="summary-workflow-dock">
|
||||
<div class="workflow-dock-title">
|
||||
<span class="dock-title-line"></span>
|
||||
<span class="dock-title-text">CORE SAFETY WORKFLOW</span>
|
||||
<span class="dock-title-line"></span>
|
||||
</div>
|
||||
<div class="workflow-steps-chain">
|
||||
<div class="workflow-step-pill"><span class="step-dot red-dot">🔴</span> <strong>STOP</strong></div>
|
||||
<span class="workflow-step-arrow">➔</span>
|
||||
<div class="workflow-step-pill"><span class="step-dot">👁️</span> <strong>LOOK</strong></div>
|
||||
<span class="workflow-step-arrow">➔</span>
|
||||
<div class="workflow-step-pill"><span class="step-dot">⚠️</span> <strong>IDENTIFY HAZARDS</strong></div>
|
||||
<span class="workflow-step-arrow">➔</span>
|
||||
<div class="workflow-step-pill"><span class="step-dot">🦺</span> <strong>SELECT PPE</strong></div>
|
||||
<span class="workflow-step-arrow">➔</span>
|
||||
<div class="workflow-step-pill"><span class="step-dot">🛡️</span> <strong>WORK SAFELY</strong></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scenarios Review Summary Box -->
|
||||
<div class="summary-scenarios-review">
|
||||
<h4 class="scenarios-review-heading">Safety Assessment Summary</h4>
|
||||
<div class="summary-review-list">
|
||||
<!-- Scenario 1 Review Item -->
|
||||
<div class="summary-review-row">
|
||||
<div class="review-icon-box">
|
||||
<span class="review-scenario-icon">⚙️</span>
|
||||
</div>
|
||||
<div class="review-text-content">
|
||||
<strong class="review-scenario-name">Scenario 1: Angle Grinding Operation</strong>
|
||||
<p class="review-scenario-desc">
|
||||
Protected against flying hot sparks, high decibel noise (95+ dB), and sharp metal burrs using Full Face Shield + Glasses, Leather Grinding Gloves, Hearing Protection & Safety Boots.
|
||||
</p>
|
||||
</div>
|
||||
<div class="review-passed-badge">
|
||||
<span class="passed-check">✔</span> PASSED
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Scenario 2 Review Item -->
|
||||
<div class="summary-review-row">
|
||||
<div class="review-icon-box">
|
||||
<span class="review-scenario-icon">🧪</span>
|
||||
</div>
|
||||
<div class="review-text-content">
|
||||
<strong class="review-scenario-name">Scenario 2: Industrial Chemical Transfer</strong>
|
||||
<p class="review-scenario-desc">
|
||||
Prevented chemical burn & splash injury using Chemical Splash Goggles, Heavy Nitrile Gauntlet Gloves, Chemical Resistant Apron & Rubber Safety Boots.
|
||||
</p>
|
||||
</div>
|
||||
<div class="review-passed-badge">
|
||||
<span class="passed-check">✔</span> PASSED
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bottom Action Buttons -->
|
||||
<div class="summary-action-buttons">
|
||||
<button id="btn-replay" class="btn-summary-replay">
|
||||
<span class="summary-btn-icon">🔄</span>
|
||||
<span>REPLAY SAFETY CHALLENGE</span>
|
||||
</button>
|
||||
<button id="btn-print-certificate" class="btn btn-secondary btn-large">
|
||||
<span>📜 PRINT SAFETY CERTIFICATE</span>
|
||||
<button id="btn-print-certificate" class="btn-summary-cert">
|
||||
<span class="summary-btn-icon">📜</span>
|
||||
<span>PRINT SAFETY CERTIFICATE</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -353,6 +538,23 @@
|
||||
|
||||
</main>
|
||||
|
||||
<!-- Central Screen Start Countdown Overlay (3, 2, 1, START!) -->
|
||||
<div class="countdown-overlay" id="countdown-overlay">
|
||||
<div class="countdown-hud-card">
|
||||
<div class="countdown-radar-ring"></div>
|
||||
<div class="countdown-corner c-tl"></div>
|
||||
<div class="countdown-corner c-tr"></div>
|
||||
<div class="countdown-corner c-bl"></div>
|
||||
<div class="countdown-corner c-br"></div>
|
||||
|
||||
<div class="countdown-sub-header">SCENARIO INITIALIZING</div>
|
||||
<div class="countdown-number-box">
|
||||
<span class="countdown-digit" id="countdown-digit">3</span>
|
||||
</div>
|
||||
<div class="countdown-sub-tip" id="countdown-sub-tip">PREPARE YOUR PPE</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Audio Settings Modal Dialog -->
|
||||
<div class="modal-backdrop" id="modal-audio">
|
||||
<div class="modal-card">
|
||||
@@ -387,29 +589,38 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Printable Certificate Template (Hidden until printed) -->
|
||||
<!-- Certificate Viewer & Customization Modal Dialog (Exact Match to Reference) -->
|
||||
<div class="modal-backdrop" id="modal-certificate">
|
||||
<div class="certificate-modal-card">
|
||||
<div class="modal-header">
|
||||
<div class="cert-modal-header-title">
|
||||
<span class="cert-header-icon">📜</span>
|
||||
<h3>Certificate of Safety Excellence</h3>
|
||||
</div>
|
||||
<button id="btn-close-cert-modal" class="btn-close" aria-label="Close modal">✕</button>
|
||||
</div>
|
||||
|
||||
<div class="certificate-preview-container">
|
||||
<div class="certificate-visual-frame" id="certificate-visual-frame">
|
||||
<img src="images/certificate_template.jpg" alt="Certificate of Safety Excellence" class="certificate-base-img" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="certificate-modal-footer">
|
||||
<span class="cert-tip-hint">🏆 Official verified certificate of completion.</span>
|
||||
<div class="cert-footer-buttons">
|
||||
<button id="btn-print-cert-action" class="btn-summary-replay cert-print-btn">
|
||||
<span>🖨️ PRINT / SAVE AS PDF</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Full Resolution Printable Certificate (Exact Match for Print & PDF) -->
|
||||
<div id="printable-certificate" class="printable-certificate">
|
||||
<div class="cert-border">
|
||||
<div class="cert-header">
|
||||
<div class="cert-logo">✚ PPE SAFETY CHALLENGE</div>
|
||||
<h1>CERTIFICATE OF SAFETY EXCELLENCE</h1>
|
||||
<p class="cert-sub">This certifies that</p>
|
||||
<h2 class="cert-name">QUALIFIED INDUSTRIAL EMPLOYEE</h2>
|
||||
<p class="cert-desc">has successfully demonstrated 100% competency in Hazard Identification & Personal Protective Equipment (PPE) Selection across all high-risk industrial scenarios.</p>
|
||||
</div>
|
||||
<div class="cert-footer">
|
||||
<div class="cert-signature">
|
||||
<div class="sig-line"></div>
|
||||
<span>Safety & Environmental Health Officer</span>
|
||||
</div>
|
||||
<div class="cert-seal">
|
||||
⭐ VERIFIED SAFE ⭐
|
||||
</div>
|
||||
<div class="cert-date">
|
||||
<div class="sig-line" id="cert-date-val"></div>
|
||||
<span>Completion Date</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="print-cert-wrapper">
|
||||
<img src="images/certificate_template.jpg" alt="Certificate of Safety Excellence" class="print-cert-bg" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+259
@@ -25,6 +25,22 @@ class SoundEngine {
|
||||
this.activeGrinderNode = null;
|
||||
this.activeLiquidNode = null;
|
||||
|
||||
// Dedicated Countdown Tick Sound Clip (User uploaded custom audio)
|
||||
try {
|
||||
this.countdownTickAudio = new Audio('audio/countdown_tick.mp3');
|
||||
this.countdownTickAudio.preload = 'auto';
|
||||
} catch (e) {
|
||||
this.countdownTickAudio = null;
|
||||
}
|
||||
|
||||
// Dedicated Top Timer Last 10-Second Warning Sound Clip (User uploaded custom audio)
|
||||
try {
|
||||
this.timerLast10sAudio = new Audio('audio/timer_last_10s.wav');
|
||||
this.timerLast10sAudio.preload = 'auto';
|
||||
} catch (e) {
|
||||
this.timerLast10sAudio = null;
|
||||
}
|
||||
|
||||
this.isInitialized = false;
|
||||
}
|
||||
|
||||
@@ -91,6 +107,12 @@ class SoundEngine {
|
||||
if (this.masterGain && this.ctx) {
|
||||
this.masterGain.gain.setTargetAtTime(this.isMuted ? 0 : 1.0, this.ctx.currentTime, 0.05);
|
||||
}
|
||||
if (this.countdownTickAudio) {
|
||||
this.countdownTickAudio.volume = this.isMuted ? 0 : this.sfxVolume;
|
||||
}
|
||||
if (this.timerLast10sAudio) {
|
||||
this.timerLast10sAudio.volume = this.isMuted ? 0 : this.sfxVolume;
|
||||
}
|
||||
}
|
||||
|
||||
setMusicVolume(val) {
|
||||
@@ -105,6 +127,94 @@ class SoundEngine {
|
||||
if (this.sfxGain && this.ctx) {
|
||||
this.sfxGain.gain.setTargetAtTime(this.sfxVolume, this.ctx.currentTime, 0.05);
|
||||
}
|
||||
if (this.countdownTickAudio) {
|
||||
this.countdownTickAudio.volume = this.isMuted ? 0 : this.sfxVolume;
|
||||
}
|
||||
if (this.timerLast10sAudio) {
|
||||
this.timerLast10sAudio.volume = this.isMuted ? 0 : this.sfxVolume;
|
||||
}
|
||||
}
|
||||
|
||||
playCountdownTickAudio() {
|
||||
this.ensureContext();
|
||||
if (this.isMuted) return;
|
||||
|
||||
if (this.countdownTickAudio) {
|
||||
try {
|
||||
const sound = this.countdownTickAudio.cloneNode();
|
||||
sound.volume = Math.max(0, Math.min(1, this.sfxVolume));
|
||||
sound.play().catch(err => {
|
||||
console.warn("Countdown tick audio playback note:", err);
|
||||
});
|
||||
} catch (e) {
|
||||
try {
|
||||
this.countdownTickAudio.currentTime = 0;
|
||||
this.countdownTickAudio.volume = Math.max(0, Math.min(1, this.sfxVolume));
|
||||
this.countdownTickAudio.play().catch(() => {});
|
||||
} catch (err) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Normal Clock Tick for the first 20 seconds (30s down to 11s)
|
||||
*/
|
||||
playTimerTick() {
|
||||
this.ensureContext();
|
||||
if (!this.ctx || this.isMuted) return;
|
||||
|
||||
const t = this.ctx.currentTime;
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
const filter = this.ctx.createBiquadFilter();
|
||||
|
||||
// Crisp high-tech clock tick matching 1st beat
|
||||
osc.type = 'triangle';
|
||||
osc.frequency.setValueAtTime(1100, t);
|
||||
osc.frequency.exponentialRampToValueAtTime(320, t + 0.035);
|
||||
|
||||
filter.type = 'bandpass';
|
||||
filter.frequency.setValueAtTime(1200, t);
|
||||
filter.Q.setValueAtTime(3.5, t);
|
||||
|
||||
gain.gain.setValueAtTime(0.22, t);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.045);
|
||||
|
||||
osc.connect(filter);
|
||||
filter.connect(gain);
|
||||
gain.connect(this.sfxGain);
|
||||
|
||||
osc.start(t);
|
||||
osc.stop(t + 0.05);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dedicated Last 10 Seconds Audio Track (User uploaded custom audio)
|
||||
*/
|
||||
playTimerLast10sAudio() {
|
||||
this.ensureContext();
|
||||
if (this.isMuted) return;
|
||||
|
||||
if (this.timerLast10sAudio) {
|
||||
try {
|
||||
this.timerLast10sAudio.currentTime = 0;
|
||||
this.timerLast10sAudio.volume = Math.max(0, Math.min(1, this.sfxVolume));
|
||||
this.timerLast10sAudio.play().catch(err => {
|
||||
console.warn("Timer last 10s audio playback note:", err);
|
||||
});
|
||||
} catch (e) {
|
||||
console.warn("Error playing timer last 10s audio:", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stopTimerLast10sAudio() {
|
||||
if (this.timerLast10sAudio) {
|
||||
try {
|
||||
this.timerLast10sAudio.pause();
|
||||
this.timerLast10sAudio.currentTime = 0;
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
setAmbientEnabled(enabled) {
|
||||
@@ -520,6 +630,155 @@ class SoundEngine {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Timer Warning Tick & Urgent Countdown Beep SFX
|
||||
*/
|
||||
playTimerWarningTick(isUrgent = false) {
|
||||
this.ensureContext();
|
||||
if (!this.ctx || this.isMuted) return;
|
||||
|
||||
const t = this.ctx.currentTime;
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
const filter = this.ctx.createBiquadFilter();
|
||||
|
||||
if (isUrgent) {
|
||||
// Rapid, tense dual-tone hazard emergency beep
|
||||
osc.type = 'sawtooth';
|
||||
osc.frequency.setValueAtTime(880, t);
|
||||
osc.frequency.setValueAtTime(700, t + 0.06);
|
||||
|
||||
filter.type = 'bandpass';
|
||||
filter.frequency.setValueAtTime(1400, t);
|
||||
filter.Q.setValueAtTime(4.0, t);
|
||||
|
||||
gain.gain.setValueAtTime(0.22, t);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.14);
|
||||
|
||||
osc.connect(filter);
|
||||
filter.connect(gain);
|
||||
gain.connect(this.sfxGain);
|
||||
|
||||
osc.start(t);
|
||||
osc.stop(t + 0.15);
|
||||
} else {
|
||||
// High-tech warning clock tick
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(560, t);
|
||||
osc.frequency.exponentialRampToValueAtTime(420, t + 0.05);
|
||||
|
||||
gain.gain.setValueAtTime(0.12, t);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.08);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.sfxGain);
|
||||
|
||||
osc.start(t);
|
||||
osc.stop(t + 0.09);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Industrial Emergency Hazard Alarm Sound (Time Expired / Hazard Warning)
|
||||
*/
|
||||
playTimerHazardAlarm() {
|
||||
this.ensureContext();
|
||||
if (!this.ctx || this.isMuted) return;
|
||||
|
||||
const t = this.ctx.currentTime;
|
||||
// Industrial emergency hazard klaxon / buzzer burst
|
||||
[0, 0.18, 0.36].forEach(offset => {
|
||||
const time = t + offset;
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
const filter = this.ctx.createBiquadFilter();
|
||||
|
||||
osc.type = 'sawtooth';
|
||||
osc.frequency.setValueAtTime(620, time);
|
||||
osc.frequency.linearRampToValueAtTime(380, time + 0.14);
|
||||
|
||||
filter.type = 'lowpass';
|
||||
filter.frequency.setValueAtTime(2000, time);
|
||||
|
||||
gain.gain.setValueAtTime(0.26, time);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, time + 0.15);
|
||||
|
||||
osc.connect(filter);
|
||||
filter.connect(gain);
|
||||
gain.connect(this.sfxGain);
|
||||
|
||||
osc.start(time);
|
||||
osc.stop(time + 0.16);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Central Screen Start Countdown Beep (3, 2, 1)
|
||||
*/
|
||||
playCountdownBeep(num) {
|
||||
this.ensureContext();
|
||||
if (!this.ctx || this.isMuted) return;
|
||||
|
||||
const t = this.ctx.currentTime;
|
||||
const osc = this.ctx.createOscillator();
|
||||
const osc2 = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
const filter = this.ctx.createBiquadFilter();
|
||||
|
||||
// Pitch rises: 3 (520Hz) -> 2 (680Hz) -> 1 (880Hz)
|
||||
const baseFreq = num === 3 ? 520 : (num === 2 ? 680 : 880);
|
||||
|
||||
osc.type = 'triangle';
|
||||
osc.frequency.setValueAtTime(baseFreq, t);
|
||||
|
||||
osc2.type = 'sine';
|
||||
osc2.frequency.setValueAtTime(baseFreq * 2, t);
|
||||
|
||||
filter.type = 'lowpass';
|
||||
filter.frequency.setValueAtTime(2400, t);
|
||||
|
||||
gain.gain.setValueAtTime(0.24, t);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.28);
|
||||
|
||||
osc.connect(filter);
|
||||
osc2.connect(filter);
|
||||
filter.connect(gain);
|
||||
gain.connect(this.sfxGain);
|
||||
|
||||
osc.start(t);
|
||||
osc2.start(t);
|
||||
osc.stop(t + 0.3);
|
||||
osc2.stop(t + 0.3);
|
||||
}
|
||||
|
||||
/**
|
||||
* Central Screen Countdown Launch / GO! Chime
|
||||
*/
|
||||
playCountdownGo() {
|
||||
this.ensureContext();
|
||||
if (!this.ctx || this.isMuted) return;
|
||||
|
||||
const t = this.ctx.currentTime;
|
||||
// Energetic ascending triad (C5 -> E5 -> G5)
|
||||
[523.25, 659.25, 783.99, 1046.50].forEach((freq, idx) => {
|
||||
const time = t + (idx * 0.05);
|
||||
const osc = this.ctx.createOscillator();
|
||||
const gain = this.ctx.createGain();
|
||||
|
||||
osc.type = 'sine';
|
||||
osc.frequency.setValueAtTime(freq, time);
|
||||
|
||||
gain.gain.setValueAtTime(0.22, time);
|
||||
gain.gain.exponentialRampToValueAtTime(0.001, time + 0.45);
|
||||
|
||||
osc.connect(gain);
|
||||
gain.connect(this.sfxGain);
|
||||
|
||||
osc.start(time);
|
||||
osc.stop(time + 0.5);
|
||||
});
|
||||
}
|
||||
|
||||
playCriticalStopAlert() {
|
||||
this.ensureContext();
|
||||
if (!this.ctx || this.isMuted) return;
|
||||
|
||||
+330
-85
@@ -15,6 +15,10 @@ class SafetyGame {
|
||||
this.avatar = null;
|
||||
this.particles = null;
|
||||
|
||||
// 30-Second Countdown Timer State
|
||||
this.timerSeconds = 30;
|
||||
this.timerInterval = null;
|
||||
|
||||
// DOM Elements Cache
|
||||
this.dom = {
|
||||
screenStart: document.getElementById('screen-start'),
|
||||
@@ -34,10 +38,16 @@ class SafetyGame {
|
||||
// HUD
|
||||
hudScore: document.getElementById('hud-score'),
|
||||
hudStars: document.getElementById('hud-stars'),
|
||||
hudTimerBox: document.getElementById('hud-timer-box'),
|
||||
hudTimerVal: document.getElementById('hud-timer-val'),
|
||||
countdownOverlay: document.getElementById('countdown-overlay'),
|
||||
countdownDigit: document.getElementById('countdown-digit'),
|
||||
countdownSubTip: document.getElementById('countdown-sub-tip'),
|
||||
stepPill1: document.getElementById('step-pill-1'),
|
||||
stepPill2: document.getElementById('step-pill-2'),
|
||||
btnSoundToggle: document.getElementById('btn-sound-toggle'),
|
||||
btnMusicToggle: document.getElementById('btn-music-toggle'),
|
||||
btnFullscreenToggle: document.getElementById('btn-fullscreen-toggle'),
|
||||
btnAudioSettings: document.getElementById('btn-audio-settings'),
|
||||
|
||||
// Scene Info & Hazards
|
||||
@@ -104,14 +114,15 @@ class SafetyGame {
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
// Start Game from Intro (Instantaneous Transition, Zero Delay)
|
||||
// Start Game from Intro (Instantaneous Transition with 3,2,1,GO Central Countdown)
|
||||
if (this.dom.btnStartGame) {
|
||||
this.dom.btnStartGame.addEventListener('click', () => {
|
||||
window.soundEngine.ensureContext();
|
||||
window.soundEngine.playStartIntroSting();
|
||||
window.soundEngine.startBackgroundMusic();
|
||||
this.loadScenario(0);
|
||||
this.loadScenario(0, false);
|
||||
this.switchScreen('game');
|
||||
this.triggerStartCountdown(() => this.startTimer());
|
||||
});
|
||||
}
|
||||
|
||||
@@ -146,7 +157,8 @@ class SafetyGame {
|
||||
this.particles.stopMode();
|
||||
|
||||
if (this.currentScenarioIndex < SCENARIO_DATA.length - 1) {
|
||||
this.loadScenario(this.currentScenarioIndex + 1);
|
||||
this.loadScenario(this.currentScenarioIndex + 1, false);
|
||||
this.triggerStartCountdown(() => this.startTimer());
|
||||
} else {
|
||||
this.showFinalSummary();
|
||||
}
|
||||
@@ -161,17 +173,31 @@ class SafetyGame {
|
||||
this.scenarioCompleted = [false, false];
|
||||
this.switchScreen('game');
|
||||
window.soundEngine.startBackgroundMusic();
|
||||
this.loadScenario(0);
|
||||
this.loadScenario(0, false);
|
||||
this.triggerStartCountdown(() => this.startTimer());
|
||||
});
|
||||
}
|
||||
|
||||
// Print Certificate
|
||||
// Certificate Viewer Modal & Print Handlers
|
||||
if (this.dom.btnPrintCertificate) {
|
||||
this.dom.btnPrintCertificate.addEventListener('click', () => {
|
||||
const dateEl = document.getElementById('cert-date-val');
|
||||
if (dateEl) {
|
||||
dateEl.textContent = new Date().toLocaleDateString(undefined, { year: 'numeric', month: 'long', day: 'numeric' });
|
||||
window.soundEngine.playButtonClick();
|
||||
const certModal = document.getElementById('modal-certificate');
|
||||
if (certModal) certModal.classList.add('active');
|
||||
});
|
||||
}
|
||||
|
||||
const btnCloseCert = document.getElementById('btn-close-cert-modal');
|
||||
if (btnCloseCert) {
|
||||
btnCloseCert.addEventListener('click', () => {
|
||||
const certModal = document.getElementById('modal-certificate');
|
||||
if (certModal) certModal.classList.remove('active');
|
||||
});
|
||||
}
|
||||
|
||||
const btnPrintAction = document.getElementById('btn-print-cert-action');
|
||||
if (btnPrintAction) {
|
||||
btnPrintAction.addEventListener('click', () => {
|
||||
window.print();
|
||||
});
|
||||
}
|
||||
@@ -243,14 +269,31 @@ class SafetyGame {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.dom.toggleAmbientHum) {
|
||||
this.dom.toggleAmbientHum.addEventListener('change', (e) => {
|
||||
window.soundEngine.setAmbientEnabled(e.target.checked);
|
||||
// Fullscreen Mode Toggle
|
||||
if (this.dom.btnFullscreenToggle) {
|
||||
this.dom.btnFullscreenToggle.addEventListener('click', () => {
|
||||
this.toggleFullscreen();
|
||||
});
|
||||
}
|
||||
|
||||
// Keyboard Shortcuts (1-9 and 0 for 10 Locker items, Space for Start Task, Esc to close modals)
|
||||
const onFullscreenChange = () => {
|
||||
const isFull = !!(document.fullscreenElement || document.webkitFullscreenElement);
|
||||
document.body.classList.toggle('fullscreen-mode', isFull);
|
||||
document.documentElement.classList.toggle('fullscreen-mode', isFull);
|
||||
this.updateFullscreenIcon();
|
||||
};
|
||||
document.addEventListener('fullscreenchange', onFullscreenChange);
|
||||
document.addEventListener('webkitfullscreenchange', onFullscreenChange);
|
||||
document.addEventListener('mozfullscreenchange', onFullscreenChange);
|
||||
document.addEventListener('MSFullscreenChange', onFullscreenChange);
|
||||
|
||||
// Keyboard Shortcuts (1-9 and 0 for 10 Locker items, Space for Start Task, F for Fullscreen, Esc to close modals)
|
||||
window.addEventListener('keydown', (e) => {
|
||||
// Don't trigger game shortcuts if typing in an editable field
|
||||
if (e.target && (e.target.tagName === 'INPUT' || e.target.isContentEditable)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const num = parseInt(e.key);
|
||||
if (!isNaN(num)) {
|
||||
const cards = document.querySelectorAll('.ppe-item-card');
|
||||
@@ -260,6 +303,12 @@ class SafetyGame {
|
||||
}
|
||||
}
|
||||
|
||||
if ((e.key === 'f' || e.key === 'F') && !e.ctrlKey && !e.altKey && !e.metaKey) {
|
||||
e.preventDefault();
|
||||
this.toggleFullscreen();
|
||||
return;
|
||||
}
|
||||
|
||||
if ((e.code === 'Space' || e.code === 'Enter') && this.dom.screenStart.classList.contains('active')) {
|
||||
e.preventDefault();
|
||||
if (this.dom.btnStartGame) this.dom.btnStartGame.click();
|
||||
@@ -276,6 +325,8 @@ class SafetyGame {
|
||||
if (e.key === 'Escape') {
|
||||
if (this.dom.modalAudio && this.dom.modalAudio.classList.contains('active')) {
|
||||
this.dom.modalAudio.classList.remove('active');
|
||||
} else if (document.getElementById('modal-certificate')?.classList.contains('active')) {
|
||||
document.getElementById('modal-certificate').classList.remove('active');
|
||||
} else if (this.dom.consequenceOverlay.classList.contains('active')) {
|
||||
this.dismissConsequenceModal();
|
||||
}
|
||||
@@ -283,17 +334,206 @@ class SafetyGame {
|
||||
});
|
||||
}
|
||||
|
||||
toggleFullscreen() {
|
||||
window.soundEngine.playButtonClick();
|
||||
if (!document.fullscreenElement && !document.webkitFullscreenElement) {
|
||||
const elem = document.documentElement;
|
||||
if (elem.requestFullscreen) {
|
||||
elem.requestFullscreen().catch(() => {});
|
||||
} else if (elem.webkitRequestFullscreen) {
|
||||
elem.webkitRequestFullscreen();
|
||||
}
|
||||
} else {
|
||||
if (document.exitFullscreen) {
|
||||
document.exitFullscreen().catch(() => {});
|
||||
} else if (document.webkitExitFullscreen) {
|
||||
document.webkitExitFullscreen();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
updateFullscreenIcon() {
|
||||
const isFull = !!(document.fullscreenElement || document.webkitFullscreenElement);
|
||||
const iconSpan = document.getElementById('fullscreen-icon');
|
||||
if (iconSpan) {
|
||||
iconSpan.textContent = isFull ? '⤡' : '⛶';
|
||||
}
|
||||
if (this.dom.btnFullscreenToggle) {
|
||||
this.dom.btnFullscreenToggle.title = isFull ? 'Exit Fullscreen (F)' : 'Toggle Fullscreen (F)';
|
||||
if (isFull) {
|
||||
this.dom.btnFullscreenToggle.classList.add('fullscreen-active');
|
||||
} else {
|
||||
this.dom.btnFullscreenToggle.classList.remove('fullscreen-active');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switchScreen(screenName) {
|
||||
this.dom.screenStart.classList.remove('active');
|
||||
this.dom.screenGame.classList.remove('active');
|
||||
this.dom.screenSummary.classList.remove('active');
|
||||
|
||||
if (screenName === 'start') this.dom.screenStart.classList.add('active');
|
||||
if (screenName === 'start') {
|
||||
this.stopTimer();
|
||||
this.dom.screenStart.classList.add('active');
|
||||
}
|
||||
if (screenName === 'game') this.dom.screenGame.classList.add('active');
|
||||
if (screenName === 'summary') this.dom.screenSummary.classList.add('active');
|
||||
if (screenName === 'summary') {
|
||||
this.stopTimer();
|
||||
this.dom.screenSummary.classList.add('active');
|
||||
}
|
||||
}
|
||||
|
||||
loadScenario(index) {
|
||||
// =========================================================================
|
||||
// 30-SECOND COUNTDOWN TIMER CONTROLLER
|
||||
// =========================================================================
|
||||
|
||||
startTimer() {
|
||||
this.stopTimer();
|
||||
this.timerSeconds = 30;
|
||||
this.updateTimerDisplay();
|
||||
|
||||
this.timerInterval = setInterval(() => {
|
||||
if (this.timerSeconds > 0) {
|
||||
this.timerSeconds--;
|
||||
this.updateTimerDisplay();
|
||||
|
||||
if (this.timerSeconds > 10) {
|
||||
// Normal clock tick sound from 30s to 11s (first 20 seconds)
|
||||
window.soundEngine.playTimerTick();
|
||||
} else if (this.timerSeconds === 10) {
|
||||
// Trigger dedicated audio track for the final 10 seconds
|
||||
window.soundEngine.playTimerLast10sAudio();
|
||||
}
|
||||
} else {
|
||||
this.stopTimer();
|
||||
// Time expired: execute safety check
|
||||
if (this.dom.screenGame.classList.contains('active') &&
|
||||
!this.dom.consequenceOverlay.classList.contains('active') &&
|
||||
!this.dom.successOverlay.classList.contains('active')) {
|
||||
this.executeSafetyTest();
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
stopTimer() {
|
||||
if (this.timerInterval) {
|
||||
clearInterval(this.timerInterval);
|
||||
this.timerInterval = null;
|
||||
}
|
||||
window.soundEngine.stopTimerLast10sAudio();
|
||||
}
|
||||
|
||||
pauseTimer() {
|
||||
this.stopTimer();
|
||||
}
|
||||
|
||||
resumeTimer() {
|
||||
if (this.timerSeconds > 0 && !this.timerInterval) {
|
||||
this.timerInterval = setInterval(() => {
|
||||
if (this.timerSeconds > 0) {
|
||||
this.timerSeconds--;
|
||||
this.updateTimerDisplay();
|
||||
|
||||
if (this.timerSeconds > 10) {
|
||||
window.soundEngine.playTimerTick();
|
||||
} else if (this.timerSeconds === 10) {
|
||||
window.soundEngine.playTimerLast10sAudio();
|
||||
}
|
||||
} else {
|
||||
this.stopTimer();
|
||||
if (this.dom.screenGame.classList.contains('active') &&
|
||||
!this.dom.consequenceOverlay.classList.contains('active') &&
|
||||
!this.dom.successOverlay.classList.contains('active')) {
|
||||
this.executeSafetyTest();
|
||||
}
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
updateTimerDisplay() {
|
||||
if (!this.dom.hudTimerVal) return;
|
||||
const formatted = `00:${this.timerSeconds.toString().padStart(2, '0')}`;
|
||||
this.dom.hudTimerVal.textContent = formatted;
|
||||
|
||||
if (this.dom.hudTimerBox) {
|
||||
this.dom.hudTimerBox.classList.remove('timer-warning', 'timer-danger');
|
||||
if (this.timerSeconds <= 5) {
|
||||
this.dom.hudTimerBox.classList.add('timer-danger');
|
||||
} else if (this.timerSeconds <= 10) {
|
||||
this.dom.hudTimerBox.classList.add('timer-warning');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CENTRAL SCREEN START COUNTDOWN (3, 2, 1, GO!)
|
||||
// =========================================================================
|
||||
|
||||
triggerStartCountdown(onComplete) {
|
||||
if (!this.dom.countdownOverlay) {
|
||||
if (onComplete) onComplete();
|
||||
return;
|
||||
}
|
||||
|
||||
this.stopTimer();
|
||||
this.timerSeconds = 30;
|
||||
this.updateTimerDisplay();
|
||||
|
||||
this.dom.countdownOverlay.classList.add('active');
|
||||
const digitEl = this.dom.countdownDigit;
|
||||
const subTipEl = this.dom.countdownSubTip;
|
||||
|
||||
const steps = [
|
||||
{ text: '3', tip: 'PREPARE YOUR PPE', isGo: false, duration: 1000 },
|
||||
{ text: '2', tip: 'IDENTIFY HAZARDS', isGo: false, duration: 1000 },
|
||||
{ text: '1', tip: 'EQUIP WORKER', isGo: false, duration: 1000 },
|
||||
{ text: 'GO!', tip: '30s TIMER STARTED', isGo: true, duration: 750 }
|
||||
];
|
||||
|
||||
let currentStep = 0;
|
||||
|
||||
const playNextStep = () => {
|
||||
if (currentStep < steps.length) {
|
||||
const item = steps[currentStep];
|
||||
|
||||
if (digitEl) {
|
||||
digitEl.textContent = item.text;
|
||||
digitEl.className = 'countdown-digit';
|
||||
if (item.isGo) {
|
||||
digitEl.classList.add('go-text');
|
||||
}
|
||||
// Force CSS reflow to re-trigger pop animation
|
||||
void digitEl.offsetWidth;
|
||||
digitEl.classList.add('pop');
|
||||
}
|
||||
|
||||
if (subTipEl) {
|
||||
subTipEl.textContent = item.tip;
|
||||
}
|
||||
|
||||
// Play uploaded countdown tick audio for 3, 2, 1 every second
|
||||
if (item.isGo) {
|
||||
window.soundEngine.playCountdownGo();
|
||||
} else {
|
||||
window.soundEngine.playCountdownTickAudio();
|
||||
}
|
||||
|
||||
currentStep++;
|
||||
setTimeout(playNextStep, item.duration);
|
||||
} else {
|
||||
// Complete countdown & fade out
|
||||
this.dom.countdownOverlay.classList.remove('active');
|
||||
if (onComplete) onComplete();
|
||||
}
|
||||
};
|
||||
|
||||
playNextStep();
|
||||
}
|
||||
|
||||
loadScenario(index, autoStartTimer = true) {
|
||||
window.soundEngine.stopAllTaskSFX();
|
||||
this.currentScenarioIndex = index;
|
||||
this.currentScenario = SCENARIO_DATA[index];
|
||||
@@ -303,6 +543,14 @@ class SafetyGame {
|
||||
this.particles.clear();
|
||||
this.particles.stopMode();
|
||||
|
||||
if (autoStartTimer) {
|
||||
this.startTimer();
|
||||
} else {
|
||||
this.stopTimer();
|
||||
this.timerSeconds = 30;
|
||||
this.updateTimerDisplay();
|
||||
}
|
||||
|
||||
// Update Stepper HUD
|
||||
if (this.dom.stepPill1 && this.dom.stepPill2) {
|
||||
this.dom.stepPill1.className = index === 0 ? 'step-pill active' : (this.scenarioCompleted[0] ? 'step-pill completed' : 'step-pill');
|
||||
@@ -483,9 +731,6 @@ class SafetyGame {
|
||||
|
||||
return `
|
||||
<div class="ppe-item-card ${isEquipped ? 'equipped' : ''}" data-item-id="${item.id}" data-slot="${item.slot}" title="${item.spec}">
|
||||
<div class="item-toggle-indicator">
|
||||
<span class="radio-check-mark">${isEquipped ? '✓' : ''}</span>
|
||||
</div>
|
||||
<div class="item-icon-box">${iconContent}</div>
|
||||
<span class="item-name">${item.name}</span>
|
||||
</div>
|
||||
@@ -610,19 +855,10 @@ class SafetyGame {
|
||||
}
|
||||
|
||||
handleSafeSuccess() {
|
||||
// Start Task Simulation SFX & Particles
|
||||
if (this.currentScenario.id === 'grinding') {
|
||||
window.soundEngine.startGrinderSFX();
|
||||
this.particles.startMode('sparks');
|
||||
} else {
|
||||
window.soundEngine.startLiquidTransferSFX();
|
||||
this.particles.startMode('chemical');
|
||||
}
|
||||
|
||||
// Play Success Chime & Star sound after short operational run
|
||||
setTimeout(() => {
|
||||
window.soundEngine.stopAllTaskSFX();
|
||||
this.stopTimer();
|
||||
this.particles.clear();
|
||||
this.particles.stopMode();
|
||||
window.soundEngine.stopAllTaskSFX();
|
||||
|
||||
window.soundEngine.playSuccessChime();
|
||||
window.soundEngine.playSafetyStarSound();
|
||||
@@ -634,9 +870,15 @@ class SafetyGame {
|
||||
this.updateHUD();
|
||||
|
||||
// Show Success Modal Overlay
|
||||
if (this.dom.successTitle) this.dom.successTitle.textContent = this.currentScenario.successMessage.title;
|
||||
if (this.dom.successTitle) {
|
||||
if (this.currentScenario.id === 'grinding') {
|
||||
this.dom.successTitle.innerHTML = `<span class="title-green">SAFE</span> <span class="title-white">TO START</span>`;
|
||||
} else {
|
||||
this.dom.successTitle.innerHTML = `<span class="title-green">SAFE</span> <span class="title-white">TO PROCEED</span>`;
|
||||
}
|
||||
}
|
||||
if (this.dom.successDesc) this.dom.successDesc.textContent = this.currentScenario.successMessage.body;
|
||||
if (this.dom.awardedPointsText) this.dom.awardedPointsText.textContent = `+${points} Safety Points ⭐`;
|
||||
if (this.dom.awardedPointsText) this.dom.awardedPointsText.textContent = `+${points} Safety Points`;
|
||||
|
||||
if (this.dom.btnNextScenarioText) {
|
||||
if (this.currentScenarioIndex === SCENARIO_DATA.length - 1) {
|
||||
@@ -647,54 +889,29 @@ class SafetyGame {
|
||||
}
|
||||
|
||||
if (this.dom.successOverlay) this.dom.successOverlay.classList.add('active');
|
||||
}, 1200);
|
||||
}
|
||||
|
||||
handleUnsafeConsequence(allMissingChecks, allEquippedDistractors) {
|
||||
this.stopTimer();
|
||||
this.scenarioFirstAttempt = false;
|
||||
this.particles.clear();
|
||||
this.particles.stopMode();
|
||||
|
||||
// Start task machine momentarily
|
||||
if (this.currentScenario.id === 'grinding') {
|
||||
window.soundEngine.startGrinderSFX();
|
||||
this.particles.startMode('sparks');
|
||||
} else {
|
||||
window.soundEngine.startLiquidTransferSFX();
|
||||
this.particles.startMode('chemical');
|
||||
}
|
||||
|
||||
// Determine target position for incoming projectile
|
||||
const avatarCoords = this.avatar.getAvatarCenterCoordinates();
|
||||
const stageWidth = this.dom.sceneStage ? this.dom.sceneStage.clientWidth : 600;
|
||||
const stageHeight = this.dom.sceneStage ? this.dom.sceneStage.clientHeight : 450;
|
||||
const originX = stageWidth * 0.72;
|
||||
const originY = stageHeight * 0.65;
|
||||
|
||||
const hasEyeFaceIssue = allMissingChecks.some(chk => chk.slot === 'eyes' || chk.slot === 'head');
|
||||
const targetX = hasEyeFaceIssue ? avatarCoords.eyeX : avatarCoords.handX;
|
||||
const targetY = hasEyeFaceIssue ? avatarCoords.eyeY : avatarCoords.handY;
|
||||
|
||||
setTimeout(() => {
|
||||
window.soundEngine.playWhooshImpact();
|
||||
this.particles.spawnHazardProjectile(
|
||||
this.currentScenario.id === 'grinding' ? 'spark' : 'chemical_drop',
|
||||
originX, originY, targetX, targetY
|
||||
);
|
||||
}, 400);
|
||||
|
||||
// Store consequence context for freeze overlay
|
||||
this.activeConsequenceData = { allMissingChecks, allEquippedDistractors };
|
||||
this.handleFreezeFrameAlert();
|
||||
}
|
||||
|
||||
handleFreezeFrameAlert(projectile) {
|
||||
// 1. Freeze particle engine & stop machine motors
|
||||
this.particles.setFrozen(true);
|
||||
this.particles.clear();
|
||||
this.particles.setFrozen(false);
|
||||
this.particles.stopMode();
|
||||
window.soundEngine.stopGrinderSFX();
|
||||
window.soundEngine.stopLiquidTransferSFX();
|
||||
|
||||
// 2. Duck background music
|
||||
// Duck background music
|
||||
window.soundEngine.duckMusic(true);
|
||||
|
||||
// 3. Play warning tones
|
||||
// Play warning tones
|
||||
window.soundEngine.playWarningTone();
|
||||
window.soundEngine.playCriticalStopAlert();
|
||||
|
||||
@@ -712,22 +929,25 @@ class SafetyGame {
|
||||
|
||||
// 4. Update Header & Badges
|
||||
if (this.dom.hazardFlasherBadge) {
|
||||
this.dom.hazardFlasherBadge.textContent = `⚠️ STOP! ${totalIssues} SAFETY ${totalIssues === 1 ? 'HAZARD' : 'HAZARDS'} DETECTED`;
|
||||
this.dom.hazardFlasherBadge.innerHTML = `
|
||||
<span class="flasher-icon">⚠️</span>
|
||||
<span class="flasher-text">STOP! ${totalIssues} SAFETY ${totalIssues === 1 ? 'HAZARD' : 'HAZARDS'} DETECTED</span>
|
||||
`;
|
||||
}
|
||||
if (this.dom.missingCountNum) this.dom.missingCountNum.textContent = totalIssues;
|
||||
|
||||
if (totalIssues === 1) {
|
||||
if (allEquippedDistractors.length > 0) {
|
||||
const d = allEquippedDistractors[0];
|
||||
if (this.dom.consequenceTitle) this.dom.consequenceTitle.textContent = "Incorrect / Ineffective PPE Detected";
|
||||
if (this.dom.consequenceTitle) this.dom.consequenceTitle.innerHTML = `<span class="title-red">INCORRECT</span> <span class="title-white">PPE DETECTED</span>`;
|
||||
if (this.dom.consequenceDesc) this.dom.consequenceDesc.textContent = d.rejectReason;
|
||||
} else {
|
||||
const m = allMissingChecks[0];
|
||||
if (this.dom.consequenceTitle) this.dom.consequenceTitle.textContent = m.consequenceTitle;
|
||||
if (this.dom.consequenceTitle) this.dom.consequenceTitle.innerHTML = `<span class="title-red">CRITICAL</span> <span class="title-white">HAZARD DETECTED</span>`;
|
||||
if (this.dom.consequenceDesc) this.dom.consequenceDesc.textContent = m.consequenceDesc;
|
||||
}
|
||||
} else {
|
||||
if (this.dom.consequenceTitle) this.dom.consequenceTitle.textContent = "Critical Protection Gaps Detected";
|
||||
if (this.dom.consequenceTitle) this.dom.consequenceTitle.innerHTML = `<span class="title-red">CRITICAL</span> <span class="title-white">PROTECTION GAPS DETECTED</span>`;
|
||||
if (this.dom.consequenceDesc) this.dom.consequenceDesc.textContent = "Operating without complete PPE exposes multiple body zones to direct, severe workplace injuries. All required safety equipment must be equipped before starting.";
|
||||
}
|
||||
|
||||
@@ -735,31 +955,49 @@ class SafetyGame {
|
||||
let cardsHtml = '';
|
||||
|
||||
allMissingChecks.forEach(chk => {
|
||||
const icon = slotIcons[chk.slot] || '⚠️';
|
||||
const reqItem = this.currentScenario.lockerItems.find(it => it.id === chk.requiredItem);
|
||||
const iconImgHtml = reqItem && reqItem.iconImg
|
||||
? `<img src="${reqItem.iconImg}" alt="${reqItem.name}" class="hazard-card-ppe-img">`
|
||||
: `<span class="hazard-card-fallback-icon">${slotIcons[chk.slot] || '⚠️'}</span>`;
|
||||
|
||||
const slotCap = chk.slot ? chk.slot.charAt(0).toUpperCase() + chk.slot.slice(1) : 'Body';
|
||||
|
||||
cardsHtml += `
|
||||
<div class="hazard-card-item">
|
||||
<div class="card-icon">${icon}</div>
|
||||
<div class="card-body">
|
||||
<div class="card-header-line">
|
||||
<span class="card-item-name">${chk.missingLabel}</span>
|
||||
<span class="card-slot-badge">${chk.slot} - Missing</span>
|
||||
<div class="cyber-hazard-card">
|
||||
<div class="hazard-card-icon-box">
|
||||
${iconImgHtml}
|
||||
</div>
|
||||
<span class="card-hazard-text">${chk.consequenceDesc}</span>
|
||||
<div class="hazard-card-content">
|
||||
<div class="hazard-card-top-row">
|
||||
<span class="hazard-card-name">${chk.missingLabel || (reqItem ? reqItem.name : 'Missing PPE')}</span>
|
||||
<span class="hazard-card-badge missing-badge">
|
||||
<span class="badge-exclamation">!</span> ${slotCap} - Missing
|
||||
</span>
|
||||
</div>
|
||||
<p class="hazard-card-desc">${chk.consequenceDesc}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
allEquippedDistractors.forEach(dist => {
|
||||
const iconImgHtml = dist.iconImg
|
||||
? `<img src="${dist.iconImg}" alt="${dist.name}" class="hazard-card-ppe-img">`
|
||||
: `<span class="hazard-card-fallback-icon">🚫</span>`;
|
||||
|
||||
cardsHtml += `
|
||||
<div class="hazard-card-item distractor-card">
|
||||
<div class="card-icon">🚫</div>
|
||||
<div class="card-body">
|
||||
<div class="card-header-line">
|
||||
<span class="card-item-name">REMOVE: ${dist.name}</span>
|
||||
<span class="card-slot-badge">Unsafe Gear</span>
|
||||
<div class="cyber-hazard-card distractor-card">
|
||||
<div class="hazard-card-icon-box">
|
||||
${iconImgHtml}
|
||||
</div>
|
||||
<span class="card-hazard-text">${dist.rejectReason}</span>
|
||||
<div class="hazard-card-content">
|
||||
<div class="hazard-card-top-row">
|
||||
<span class="hazard-card-name">REMOVE: ${dist.name}</span>
|
||||
<span class="hazard-card-badge distractor-badge">
|
||||
<span class="badge-exclamation">!</span> Unsafe Gear
|
||||
</span>
|
||||
</div>
|
||||
<p class="hazard-card-desc">${dist.rejectReason}</p>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -801,6 +1039,13 @@ class SafetyGame {
|
||||
const rows = this.dom.coverageGrid.querySelectorAll('.status-row-item');
|
||||
rows.forEach(row => row.classList.remove('alert-missing'));
|
||||
}
|
||||
|
||||
if (this.timerSeconds <= 0) {
|
||||
this.timerSeconds = 15; // Extra 15s to fix PPE
|
||||
this.resumeTimer();
|
||||
} else {
|
||||
this.resumeTimer();
|
||||
}
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
|
||||
+4
-49
@@ -52,62 +52,17 @@ class ParticleEngine {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
// Spawn near-miss hazardous particle / projectile flying at avatar
|
||||
// Particle Spawning (Disabled for clean direct feedback)
|
||||
spawnHazardProjectile(type, startX, startY, targetX, targetY) {
|
||||
const dx = targetX - startX;
|
||||
const dy = targetY - startY;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
const speed = 14;
|
||||
|
||||
this.projectiles.push({
|
||||
type: type, // 'spark' or 'chemical_drop'
|
||||
x: startX,
|
||||
y: startY,
|
||||
vx: (dx / dist) * speed,
|
||||
vy: (dy / dist) * speed,
|
||||
targetX: targetX,
|
||||
targetY: targetY,
|
||||
size: type === 'spark' ? 7 : 12,
|
||||
color: type === 'spark' ? '#ff3b30' : '#00f0ff',
|
||||
trail: [],
|
||||
reached: false
|
||||
});
|
||||
// Disabled
|
||||
}
|
||||
|
||||
spawnSparks(originX, originY, count = 12) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
const angle = (Math.PI * 0.8) + (Math.random() * 0.9 - 0.45); // Directed left-upward arc
|
||||
const speed = Math.random() * 12 + 6;
|
||||
this.particles.push({
|
||||
type: 'spark',
|
||||
x: originX,
|
||||
y: originY,
|
||||
vx: Math.cos(angle) * speed,
|
||||
vy: Math.sin(angle) * speed - (Math.random() * 4),
|
||||
gravity: 0.35,
|
||||
life: 1.0,
|
||||
decay: Math.random() * 0.03 + 0.02,
|
||||
size: Math.random() * 3 + 1.5,
|
||||
color: Math.random() > 0.3 ? '#ffdd55' : '#ff5511'
|
||||
});
|
||||
}
|
||||
// Disabled
|
||||
}
|
||||
|
||||
spawnChemicalFlow(originX, originY) {
|
||||
for (let i = 0; i < 4; i++) {
|
||||
this.particles.push({
|
||||
type: 'chemical',
|
||||
x: originX + (Math.random() * 8 - 4),
|
||||
y: originY,
|
||||
vx: -(Math.random() * 2 + 1),
|
||||
vy: Math.random() * 4 + 2,
|
||||
gravity: 0.25,
|
||||
life: 1.0,
|
||||
decay: 0.02,
|
||||
size: Math.random() * 4 + 3,
|
||||
color: '#10b981'
|
||||
});
|
||||
}
|
||||
// Disabled
|
||||
}
|
||||
|
||||
spawnConfetti() {
|
||||
|
||||
Reference in New Issue
Block a user