1. Project Overview & Features
The Missile Command β Frost βοΈ vs Inferno π₯ (3D & Procedural Ice Crystals) is an essential frontend project designed to help you bridge the gap between learning JavaScript syntax and building interactive, responsive web applications.
Key Features Implemented:
- Procedural fractal snowflake starbursts with dendritic ice needle branches upon missile detonation
- 5 glowing frost ground citadels (1 grand central shield fortress + 4 glowing crystal spires)
- Molten Inferno comets with trailing flame ember particles and physics trajectories
- Dynamic comet freezing: comets entering crystal starburst zones freeze and shatter in chain-reaction combos
- Dotted circular particle shockwave rings expanding upon target interception
- Autonomous AI defense mode with predictive target lead calculation
- Absolute Zero Freeze Beam superweapon (Spacebar) that sweeps the sky
- Real-time Heat Intensity and Wave Progress HUD meters
- Rich Web Audio API sound synthesizers for launches, crystal chime growth, and ice shatters
2. Step-by-Step Code Walkthrough
A Step 1: Semantic HTML Structure
We structure the interface using clean, semantic HTML elements. This ensures maximum accessibility and provides intuitive hooks for CSS classes and JavaScript query selectors.
<div class="frost-inferno-app" id="frostInfernoApp">
<!-- Game Canvas -->
<canvas id="gameCanvas" class="game-canvas"></canvas>
<!-- Top Game Header -->
<header class="game-header">
<div class="header-left">
<div class="badge-pill"><i class="fas fa-snowflake text-cyan"></i> Frost βοΈ vs Inferno π₯</div>
<div class="header-title-box">
<span class="game-mode-tag">CITADEL DEFENSE</span>
<h1 class="header-title">Missile Command: Frost vs Inferno</h1>
</div>
</div>
<!-- Center Heat / Threat Meter -->
<div class="header-center">
<div class="heat-meter-box">
<div class="meter-header">
<span class="meter-label"><i class="fas fa-fire text-amber me-1"></i> HEAT INTENSITY</span>
<span class="meter-val text-amber" id="dispHeatPct">0%</span>
</div>
<div class="meter-track">
<div class="meter-fill" id="heatMeterFill"></div>
</div>
</div>
</div>
<!-- Right Controls Dock -->
<div class="header-right">
<button type="button" class="dock-btn" id="btnAiMode" title="Toggle AI Auto-Defense">
<i class="fas fa-robot text-cyan"></i> <span class="btn-text">AI: OFF</span>
</button>
<button type="button" class="dock-btn btn-freeze" id="btnFreezeBeam" title="Absolute Zero Freeze Beam (Spacebar)">
<i class="fas fa-snowflake text-cyan"></i> <span class="btn-text">Freeze Beam</span>
<div class="cooldown-overlay" id="beamCooldown"></div>
</button>
<button type="button" class="dock-btn" id="btnAudioToggle" title="Toggle Sound FX">
<i class="fas fa-volume-up"></i>
</button>
<button type="button" class="dock-btn" id="btnRestartGame" title="Restart Mission">
<i class="fas fa-rotate-right"></i>
</button>
</div>
</header>
<!-- Top In-Game Stats Overlay -->
<div class="game-stats-hud">
<div class="stat-card">
<span class="stat-label">SCORE</span>
<span class="stat-value text-cyan" id="dispScore">0</span>
</div>
<div class="stat-card">
<span class="stat-label">WAVE</span>
<span class="stat-value text-amber" id="dispWave">1</span>
</div>
<div class="stat-card">
<span class="stat-label">COMBO</span>
<span class="stat-value text-pink" id="dispCombo">1x</span>
</div>
<div class="stat-card">
<span class="stat-label">CITADELS</span>
<div class="citadel-indicators" id="citadelIndicators">
<span class="c-dot alive" title="Spire 1"></span>
<span class="c-dot alive" title="Spire 2"></span>
<span class="c-dot alive c-center" title="Grand Shield Citadel"></span>
<span class="c-dot alive" title="Spire 4"></span>
<span class="c-dot alive" title="Spire 5"></span>
</div>
</div>
</div>
<!-- Bottom Wave Progress Bar -->
<div class="bottom-hud-wrapper">
<div class="wave-progress-card">
<div class="wave-prog-header">
<span class="prog-title" id="dispWaveProgressText">WAVE PROGRESS: 0 / 15</span>
<span class="prog-comets-left" id="dispCometsLeft">15 REMAINING</span>
</div>
<div class="wave-track">
<div class="wave-fill" id="waveMeterFill"></div>
</div>
</div>
<div class="targeting-hint" id="targetingHint">
<i class="fas fa-crosshairs text-cyan me-1"></i> Click / Tap sky to launch Frost Missiles β’ Press <kbd>Space</kbd> for Freeze Beam
</div>
</div>
<!-- Game Over / Victory Modal -->
<div class="custom-modal-overlay hidden" id="gameOverModal">
<div class="custom-modal-dialog">
<div class="modal-badge" id="modalBadge">
<i class="fas fa-snowflake text-cyan"></i>
</div>
<h3 class="modal-title" id="modalTitle">Frost Kingdom Defended!</h3>
<p class="modal-subtitle" id="modalSubtitle">The Frost Citadels weathered the molten Inferno comet storm.</p>
<div class="modal-stats-grid">
<div class="mstat-row">
<span>Final Score</span>
<strong class="text-cyan" id="mStatScore">0</strong>
</div>
<div class="mstat-row">
<span>Comets Frozen & Shattered</span>
<strong class="text-amber" id="mStatComets">0</strong>
</div>
<div class="mstat-row">
<span>Highest Frost Combo</span>
<strong class="text-pink" id="mStatCombo">1x</strong>
</div>
<div class="mstat-row">
<span>Wave Reached</span>
<strong class="text-white" id="mStatWave">Wave 1</strong>
</div>
</div>
<div class="modal-actions">
<button type="button" class="btn-modal-primary" id="btnPlayAgain">
<i class="fas fa-play me-2"></i> Defend Again
</button>
</div>
</div>
</div>
</div>B Step 2: Styling & Responsive CSS
Modern CSS flexbox and grid layouts are used to make the UI look polished, centered, and responsive across desktop, tablet, and mobile screens.
/* 3D Missile Command: Frost vs Inferno Styling */
:root {
--cyan-primary: #38bdf8;
--cyan-bright: #00e5ff;
--cyan-glow: rgba(56, 189, 248, 0.6);
--cyan-deep: #0284c7;
--amber-flame: #f59e0b;
--amber-glow: rgba(245, 158, 11, 0.5);
--red-fire: #ef4444;
--pink-combo: #ec4899;
--dark-bg: #040106;
--card-bg: rgba(8, 16, 32, 0.85);
--card-border: rgba(56, 189, 248, 0.3);
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.frost-inferno-app {
position: relative;
width: 100%;
height: 100vh;
min-height: 640px;
background-color: var(--dark-bg);
font-family: var(--font-sans);
color: #ffffff;
overflow: hidden;
user-select: none;
}
/* Game Canvas */
.game-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 1;
cursor: crosshair;
display: block;
}
/* Top Game Header */
.game-header {
position: absolute;
top: 14px;
left: 18px;
right: 18px;
display: flex;
justify-content: space-between;
align-items: center;
z-index: 20;
pointer-events: none;
gap: 12px;
}
.header-left, .header-center, .header-right {
pointer-events: auto;
display: flex;
align-items: center;
gap: 10px;
}
.badge-pill {
display: inline-flex;
align-items: center;
gap: 6px;
background: rgba(8, 20, 40, 0.88);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
padding: 5px 12px;
border-radius: 9999px;
font-size: 0.76rem;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
color: #e0f2fe;
border: 1px solid var(--card-border);
box-shadow: 0 0 16px rgba(56, 189, 248, 0.25);
}
.header-title-box {
display: flex;
flex-direction: column;
}
.game-mode-tag {
font-size: 0.65rem;
font-weight: 800;
letter-spacing: 0.08em;
color: var(--cyan-primary);
text-transform: uppercase;
}
.header-title {
font-size: 1.15rem;
font-weight: 900;
color: #ffffff;
letter-spacing: -0.02em;
text-shadow: 0 0 12px rgba(56, 189, 248, 0.5);
line-height: 1.1;
}
/* Center Heat Meter */
.header-center {
flex: 1;
max-width: 260px;
justify-content: center;
}
.heat-meter-box {
width: 100%;
background: rgba(8, 20, 40, 0.88);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(245, 158, 11, 0.35);
border-radius: 12px;
padding: 6px 12px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
}
.meter-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px;
}
.meter-label {
font-size: 0.68rem;
font-weight: 800;
letter-spacing: 0.06em;
color: #fcd34d;
}
.meter-val {
font-family: var(--font-mono);
font-size: 0.72rem;
font-weight: 900;
}
.meter-track {
width: 100%;
height: 6px;
background: rgba(0, 0, 0, 0.6);
border-radius: 9999px;
overflow: hidden;
border: 1px solid rgba(245, 158, 11, 0.2);
}
.meter-fill {
width: 0%;
height: 100%;
background: linear-gradient(90deg, #f59e0b, #ef4444);
border-radius: 9999px;
box-shadow: 0 0 10px #f59e0b;
transition: width 0.2s ease-out;
}
/* Utility Buttons */
.dock-btn {
height: 38px;
padding: 0 12px;
border-radius: 11px;
background: rgba(8, 20, 40, 0.88);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--card-border);
color: #ffffff;
display: flex;
align-items: center;
justify-content: center;
gap: 7px;
font-size: 0.82rem;
font-weight: 700;
cursor: pointer;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.4);
transition: all 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
position: relative;
overflow: hidden;
}
.dock-btn:hover {
transform: translateY(-2px);
border-color: var(--cyan-primary);
box-shadow: 0 0 16px var(--cyan-glow);
}
.dock-btn:active {
transform: scale(0.96);
}
.btn-freeze {
background: linear-gradient(135deg, rgba(8, 24, 48, 0.95) 0%, rgba(2, 132, 199, 0.35) 100%);
border-color: rgba(56, 189, 248, 0.6);
}
.cooldown-overlay {
position: absolute;
top: 0;
left: 0;
width: 0%;
height: 100%;
background: rgba(56, 189, 248, 0.35);
pointer-events: none;
transition: width 0.1s linear;
}
/* In-Game Stats HUD */
.game-stats-hud {
position: absolute;
top: 68px;
left: 18px;
display: flex;
gap: 8px;
z-index: 20;
pointer-events: none;
}
.stat-card {
background: rgba(8, 20, 40, 0.88);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 7px 12px;
display: flex;
flex-direction: column;
gap: 2px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.35);
}
.stat-label {
font-size: 0.65rem;
font-weight: 800;
letter-spacing: 0.08em;
color: #94a3b8;
}
.stat-value {
font-family: var(--font-mono);
font-size: 1.12rem;
font-weight: 900;
line-height: 1;
}
.citadel-indicators {
display: flex;
align-items: center;
gap: 4px;
margin-top: 3px;
}
.c-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #334155;
transition: all 0.3s ease;
}
.c-dot.c-center {
width: 11px;
height: 11px;
border-radius: 3px;
transform: rotate(45deg);
}
.c-dot.alive {
background: var(--cyan-bright);
box-shadow: 0 0 10px var(--cyan-bright);
}
/* Bottom Progress & Hints */
.bottom-hud-wrapper {
position: absolute;
bottom: 16px;
left: 50%;
transform: translateX(-50%);
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
z-index: 20;
pointer-events: none;
width: 90%;
max-width: 440px;
}
.wave-progress-card {
width: 100%;
background: rgba(8, 20, 40, 0.9);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--card-border);
border-radius: 12px;
padding: 8px 14px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5), 0 0 15px rgba(56, 189, 248, 0.15);
}
.wave-prog-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}
.prog-title {
font-family: var(--font-mono);
font-size: 0.75rem;
font-weight: 800;
letter-spacing: 0.06em;
color: var(--cyan-primary);
}
.prog-comets-left {
font-size: 0.68rem;
font-weight: 800;
color: #94a3b8;
}
.wave-track {
width: 100%;
height: 8px;
background: rgba(0, 0, 0, 0.65);
border-radius: 9999px;
overflow: hidden;
border: 1px solid rgba(56, 189, 248, 0.25);
}
.wave-fill {
width: 0%;
height: 100%;
background: linear-gradient(90deg, #0284c7, #38bdf8, #00e5ff);
border-radius: 9999px;
box-shadow: 0 0 12px #00e5ff;
transition: width 0.25s ease-out;
}
.targeting-hint {
background: rgba(8, 20, 40, 0.85);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid var(--card-border);
padding: 5px 14px;
border-radius: 9999px;
font-size: 0.76rem;
font-weight: 700;
color: #e0f2fe;
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.4);
}
kbd {
display: inline-block;
padding: 1px 5px;
font-family: var(--font-mono);
font-size: 0.7rem;
background: rgba(56, 189, 248, 0.2);
border: 1px solid rgba(56, 189, 248, 0.4);
border-radius: 4px;
color: #ffffff;
}
/* Modal Overlay */
.custom-modal-overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(3, 7, 18, 0.78);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
z-index: 50;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
}
.custom-modal-overlay.hidden {
opacity: 0;
pointer-events: none;
}
.custom-modal-dialog {
background: rgba(8, 20, 40, 0.96);
border: 1px solid rgba(56, 189, 248, 0.45);
border-radius: 24px;
width: 90%;
max-width: 420px;
padding: 28px 24px;
text-align: center;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.7), 0 0 30px rgba(56, 189, 248, 0.25);
}
.modal-badge {
width: 56px;
height: 56px;
border-radius: 18px;
background: rgba(56, 189, 248, 0.15);
border: 1px solid rgba(56, 189, 248, 0.35);
color: #38bdf8;
font-size: 1.5rem;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 16px auto;
box-shadow: 0 0 24px rgba(56, 189, 248, 0.35);
}
.modal-title {
font-size: 1.35rem;
font-weight: 900;
color: #ffffff;
margin-bottom: 6px;
}
.modal-subtitle {
font-size: 0.82rem;
color: #94a3b8;
line-height: 1.45;
margin-bottom: 20px;
}
.modal-stats-grid {
background: rgba(0, 0, 0, 0.4);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 14px;
padding: 14px 16px;
margin-bottom: 20px;
display: flex;
flex-direction: column;
gap: 10px;
}
.mstat-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 0.82rem;
color: #cbd5e1;
}
.btn-modal-primary {
width: 100%;
padding: 12px;
border-radius: 12px;
background: linear-gradient(135deg, #0284c7 0%, #0369a1 100%);
border: 1px solid #38bdf8;
color: #ffffff;
font-size: 0.95rem;
font-weight: 800;
cursor: pointer;
box-shadow: 0 4px 20px rgba(56, 189, 248, 0.4);
transition: all 0.2s ease;
}
.btn-modal-primary:hover {
background: linear-gradient(135deg, #38bdf8 0%, #0284c7 100%);
transform: translateY(-1px);
}
.text-cyan { color: var(--cyan-bright) !important; }
.text-amber { color: #f59e0b !important; }
.text-pink { color: var(--pink-combo) !important; }
@media (max-width: 768px) {
.header-center {
display: none;
}
.game-stats-hud {
top: 64px;
}
.btn-text {
display: none;
}
}C Step 3: JavaScript Logic & Event Handling
Here is the complete JavaScript code handling the state, event listeners, mathematical logic, and dynamic DOM rendering:
(() => {
// --- DOM Elements ---
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
const dispScore = document.getElementById('dispScore');
const dispWave = document.getElementById('dispWave');
const dispCombo = document.getElementById('dispCombo');
const dispHeatPct = document.getElementById('dispHeatPct');
const heatMeterFill = document.getElementById('heatMeterFill');
const dispWaveProgressText = document.getElementById('dispWaveProgressText');
const dispCometsLeft = document.getElementById('dispCometsLeft');
const waveMeterFill = document.getElementById('waveMeterFill');
const citadelDots = document.querySelectorAll('.citadel-indicators .c-dot');
const btnAiMode = document.getElementById('btnAiMode');
const btnFreezeBeam = document.getElementById('btnFreezeBeam');
const beamCooldown = document.getElementById('beamCooldown');
const btnAudioToggle = document.getElementById('btnAudioToggle');
const btnRestartGame = document.getElementById('btnRestartGame');
const gameOverModal = document.getElementById('gameOverModal');
const modalBadge = document.getElementById('modalBadge');
const modalTitle = document.getElementById('modalTitle');
const modalSubtitle = document.getElementById('modalSubtitle');
const mStatScore = document.getElementById('mStatScore');
const mStatComets = document.getElementById('mStatComets');
const mStatCombo = document.getElementById('mStatCombo');
const mStatWave = document.getElementById('mStatWave');
const btnPlayAgain = document.getElementById('btnPlayAgain');
// --- Audio Engine (Web Audio API) ---
let audioEnabled = true;
let audioCtx = null;
function initAudio() {
if (!audioCtx) {
const AudioContext = window.AudioContext || window.webkitAudioContext;
audioCtx = new AudioContext();
}
if (audioCtx.state === 'suspended') {
audioCtx.resume();
}
}
function playTone(freq, dur, type = 'sine', vol = 0.2) {
if (!audioEnabled) return;
initAudio();
if (!audioCtx) return;
try {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
gain.gain.setValueAtTime(0.001, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(vol, audioCtx.currentTime + 0.015);
gain.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime + dur);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + dur);
} catch (e) {}
}
function playLaunchSound() {
if (!audioEnabled) return;
initAudio();
if (!audioCtx) return;
try {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'triangle';
osc.frequency.setValueAtTime(220, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(950, audioCtx.currentTime + 0.22);
gain.gain.setValueAtTime(0.18, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.22);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.22);
} catch (e) {}
}
function playCrystalChimeSound(isChain = false) {
if (!audioEnabled) return;
initAudio();
if (!audioCtx) return;
const baseFreqs = isChain ? [783.99, 1046.50, 1318.51, 1567.98] : [523.25, 659.25, 783.99, 1046.50];
baseFreqs.forEach((freq, idx) => {
setTimeout(() => {
playTone(freq, 0.28, 'sine', isChain ? 0.12 : 0.15);
}, idx * 35);
});
}
function playFreezeShatterSound() {
if (!audioEnabled) return;
initAudio();
if (!audioCtx) return;
try {
[1174.66, 1396.91, 1760.00, 2093.00].forEach((f, i) => {
setTimeout(() => playTone(f, 0.18, 'triangle', 0.14), i * 25);
});
} catch (e) {}
}
function playImpactBoom() {
if (!audioEnabled) return;
initAudio();
if (!audioCtx) return;
try {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(140, audioCtx.currentTime);
osc.frequency.exponentialRampToValueAtTime(25, audioCtx.currentTime + 0.45);
gain.gain.setValueAtTime(0.35, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.45);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.45);
} catch (e) {}
}
function playFreezeBeamSound() {
if (!audioEnabled) return;
initAudio();
if (!audioCtx) return;
try {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(440, audioCtx.currentTime);
osc.frequency.linearRampToValueAtTime(880, audioCtx.currentTime + 0.3);
osc.frequency.linearRampToValueAtTime(220, audioCtx.currentTime + 0.8);
gain.gain.setValueAtTime(0.3, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 0.8);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + 0.8);
} catch (e) {}
}
// --- Game State & Entities ---
let W = window.innerWidth;
let H = window.innerHeight;
let dpr = Math.min(window.devicePixelRatio || 1, 2);
let score = 0;
let wave = 1;
let combo = 1;
let maxCombo = 1;
let cometsDestroyed = 0;
let waveCometsRemaining = 15;
let waveTotalComets = 15;
let heatIntensity = 0; // 0 to 100%
let isGameOver = false;
let aiMode = false;
let freezeBeamReady = true;
// Mouse Crosshair Reticle
let mouseX = -100;
let mouseY = -100;
// Core Arrays
const seeds = [];
const crystals = [];
const comets = [];
const missiles = [];
const shockwaves = [];
const particles = [];
const floatingTexts = [];
// Ground Citadels (5 total: 1 central shield + 4 side crystal spires)
let citadels = [];
// Background Ambience
let stars = [];
let floatingEmbers = [];
let mountainRidges = [];
function initDimensions() {
W = window.innerWidth;
H = window.innerHeight;
canvas.width = W * dpr;
canvas.height = H * dpr;
ctx.resetTransform?.();
ctx.scale(dpr, dpr);
initCitadels();
initEnvironment();
}
function initCitadels() {
citadels = [
{ x: W * 0.15, y: H - 45, type: 'spire', alive: true, glowRadius: 42, ammo: 999 },
{ x: W * 0.32, y: H - 45, type: 'spire', alive: true, glowRadius: 42, ammo: 999 },
{ x: W * 0.50, y: H - 55, type: 'shield', alive: true, glowRadius: 65, ammo: 999 },
{ x: W * 0.68, y: H - 45, type: 'spire', alive: true, glowRadius: 42, ammo: 999 },
{ x: W * 0.85, y: H - 45, type: 'spire', alive: true, glowRadius: 42, ammo: 999 }
];
updateCitadelHUD();
}
function initEnvironment() {
// Stars
stars = [];
for (let i = 0; i < 140; i++) {
stars.push({
x: Math.random() * W,
y: Math.random() * (H * 0.75),
radius: Math.random() * 1.5 + 0.5,
baseAlpha: Math.random() * 0.6 + 0.3,
twinkleSpeed: Math.random() * 0.04 + 0.01,
phase: Math.random() * Math.PI * 2
});
}
// Atmospheric Embers
floatingEmbers = [];
for (let i = 0; i < 35; i++) {
floatingEmbers.push({
x: Math.random() * W,
y: Math.random() * H,
radius: Math.random() * 1.6 + 0.6,
vx: (Math.random() - 0.5) * 0.3,
vy: -0.2 - Math.random() * 0.5,
alpha: Math.random() * 0.7 + 0.2,
color: Math.random() < 0.7 ? '#f59e0b' : '#ef4444'
});
}
// Layered Mountain Ridges
mountainRidges = [
// Far Layer
{
color: '#160307',
points: [
{ x: 0, y: H - 70 },
{ x: W * 0.18, y: H - 160 },
{ x: W * 0.38, y: H - 90 },
{ x: W * 0.62, y: H - 180 },
{ x: W * 0.82, y: H - 100 },
{ x: W, y: H - 150 }
]
},
// Near Layer
{
color: '#0d0205',
points: [
{ x: 0, y: H - 40 },
{ x: W * 0.25, y: H - 120 },
{ x: W * 0.50, y: H - 75 },
{ x: W * 0.75, y: H - 135 },
{ x: W, y: H - 60 }
]
}
];
}
// --- Procedural Snowflake Crystal Detonation Engine ---
function spawnCrystalStarburst(x, y, radius = 68, isChain = false) {
const axesCount = Math.random() < 0.25 ? 8 : 6;
const rotation = Math.random() * Math.PI;
// Generate precomputed branching spines & needle sub-branches (fractal dendrites)
const spines = [];
const branchCount = axesCount === 6 ? 4 : 3;
for (let i = 0; i < axesCount; i++) {
const angle = (i * Math.PI * 2) / axesCount;
const subBranches = [];
for (let b = 1; b <= branchCount; b++) {
const posRatio = (b / (branchCount + 1)) * 0.92;
// Longer branches in the middle, tapering towards tips
const lenFactor = Math.sin(posRatio * Math.PI);
const branchLen = (0.28 + lenFactor * 0.22);
subBranches.push({
posRatio,
branchLen,
angleOffset: Math.PI / 3 // 60 degrees standard snowflake dendrite
});
}
spines.push({ angle, subBranches });
}
const crystal = {
x,
y,
maxRadius: radius,
axesCount,
rotation,
spines,
growth: 0, // animates 0 -> 1
growthSpeed: isChain ? 4.2 : 3.2,
life: 1.5, // seconds
maxLife: 1.5,
alpha: 1.0,
isChain
};
crystals.push(crystal);
seeds.push({ x, y, radius: 4, alpha: 1.0 });
// Spawn expanding shockwave ring with ice particles
spawnShockwave(x, y, radius * 1.3);
playCrystalChimeSound(isChain);
}
function spawnShockwave(x, y, maxRadius = 85) {
const dots = [];
const dotCount = 28;
for (let i = 0; i < dotCount; i++) {
const angle = (i * Math.PI * 2) / dotCount + (Math.random() - 0.5) * 0.12;
dots.push({
angle,
dist: 4,
speed: 2.4 + Math.random() * 0.9,
size: 1.2 + Math.random() * 1.6,
alpha: 1.0
});
}
shockwaves.push({
x,
y,
radius: 6,
maxRadius,
growthSpeed: 3.6,
alpha: 0.95,
dots
});
}
// --- Particles & Floating FX ---
function emitParticles(x, y, color, count = 10, speed = 2.5, size = 2.5, life = 0.8) {
for (let i = 0; i < count; i++) {
const angle = Math.random() * Math.PI * 2;
const spd = (Math.random() * 0.8 + 0.2) * speed;
particles.push({
x,
y,
vx: Math.cos(angle) * spd,
vy: Math.sin(angle) * spd,
color,
size: Math.random() * size + 1.0,
alpha: 1.0,
life,
maxLife: life
});
}
}
function addFloatingText(text, x, y, color = '#38bdf8') {
floatingTexts.push({
text,
x,
y,
vy: -1.2,
alpha: 1.0,
color
});
}
// --- Gameplay Actions (Launch, Comet Spawning, Collision) ---
function launchMissile(targetX, targetY) {
if (isGameOver) return;
// Find nearest living citadel
let bestCitadel = null;
let minDist = Infinity;
citadels.forEach(c => {
if (c.alive) {
const d = Math.hypot(c.x - targetX, c.y - targetY);
if (d < minDist) {
minDist = d;
bestCitadel = c;
}
}
});
if (!bestCitadel) return;
const dx = targetX - bestCitadel.x;
const dy = targetY - bestCitadel.y;
const dist = Math.hypot(dx, dy);
const speed = 12.0;
playLaunchSound();
missiles.push({
x: bestCitadel.x,
y: bestCitadel.y,
targetX,
targetY,
vx: (dx / dist) * speed,
vy: (dy / dist) * speed,
trail: [],
alive: true
});
}
function spawnComet() {
if (isGameOver) return;
const livingCitadels = citadels.filter(c => c.alive);
if (livingCitadels.length === 0) return;
// Pick target: either a citadel or ground point near one
const targetCitadel = livingCitadels[Math.floor(Math.random() * livingCitadels.length)];
const targetX = targetCitadel.x + (Math.random() - 0.5) * 30;
const targetY = targetCitadel.y;
const startX = Math.random() * (W * 0.9) + W * 0.05;
const startY = -25;
const isLarge = Math.random() < 0.18 + wave * 0.03;
const speed = (1.2 + wave * 0.18) * (isLarge ? 0.75 : 1.0);
const dx = targetX - startX;
const dy = targetY - startY;
const dist = Math.hypot(dx, dy);
comets.push({
x: startX,
y: startY,
targetX,
targetY,
targetCitadel,
vx: (dx / dist) * speed,
vy: (dy / dist) * speed,
radius: isLarge ? 14 : 8,
isLarge,
tailParticles: [],
alive: true
});
}
function triggerFreezeBeam() {
if (!freezeBeamReady || isGameOver) return;
freezeBeamReady = false;
// Animate Cooldown Bar
beamCooldown.style.width = '100%';
setTimeout(() => {
freezeBeamReady = true;
beamCooldown.style.width = '0%';
}, 7000);
playFreezeBeamSound();
// Flash screen with absolute zero frost blizzard
addFloatingText('ABSOLUTE ZERO FREEZE!', W / 2, H * 0.4, '#00e5ff');
emitParticles(W / 2, H / 2, '#38bdf8', 60, 10, 4, 1.2);
// Freeze all active comets
comets.forEach(c => {
if (c.alive) {
c.alive = false;
spawnCrystalStarburst(c.x, c.y, 55, true);
emitParticles(c.x, c.y, '#38bdf8', 18, 4.0, 3);
score += 150 * combo;
cometsDestroyed++;
waveCometsRemaining = Math.max(0, waveCometsRemaining - 1);
}
});
updateHUD();
checkWaveProgress();
}
// --- AI Predictive Auto-Defense ---
let aiCooldown = 0;
function updateAiDefense() {
if (!aiMode || isGameOver) return;
aiCooldown++;
if (aiCooldown < 24) return;
for (const c of comets) {
if (c.alive && c.y > 60 && c.y < H - 180) {
// Intercept ahead of comet path
const leadTicks = 16;
const predX = c.x + c.vx * leadTicks;
const predY = c.y + c.vy * leadTicks;
// Ensure no active crystal already covers this area
const alreadyCovered = crystals.some(cr => {
return cr.alpha > 0.3 && Math.hypot(cr.x - predX, cr.y - predY) < cr.maxRadius * 0.85;
});
if (!alreadyCovered) {
launchMissile(predX, predY);
aiCooldown = 0;
break;
}
}
}
}
// --- Main Update Loop ---
let cometSpawnCounter = 0;
function updateGame(dt) {
if (isGameOver) return;
// 1. Spawning Comets
if (waveCometsRemaining > 0) {
cometSpawnCounter += dt;
const spawnInterval = Math.max(0.65, 2.4 - wave * 0.18);
if (cometSpawnCounter >= spawnInterval) {
spawnComet();
cometSpawnCounter = 0;
}
}
// Heat Intensity calculation (scales with active comets and wave)
const targetHeat = Math.min(100, (comets.length * 14) + (wave * 6));
heatIntensity += (targetHeat - heatIntensity) * 0.05;
heatMeterFill.style.width = `${Math.min(100, Math.round(heatIntensity))}%`;
dispHeatPct.innerText = `${Math.round(heatIntensity)}%`;
// 2. Update Comets
for (const c of comets) {
if (!c.alive) continue;
c.x += c.vx;
c.y += c.vy;
// Trail particles
if (Math.random() < 0.75) {
c.tailParticles.push({
x: c.x + (Math.random() - 0.5) * 6,
y: c.y - (Math.random() * 4),
vx: -c.vx * 0.2 + (Math.random() - 0.5) * 0.4,
vy: -1.0 - Math.random() * 0.8,
radius: (Math.random() * 0.6 + 0.4) * (c.radius * 0.6),
color: Math.random() < 0.6 ? '#f59e0b' : (Math.random() < 0.5 ? '#ff3300' : '#ffffff'),
alpha: 0.9,
life: 0.5
});
}
// Update tail particles
for (const p of c.tailParticles) {
p.x += p.vx;
p.y += p.vy;
p.alpha -= dt * 2.2;
p.radius *= 0.95;
}
c.tailParticles = c.tailParticles.filter(p => p.alpha > 0);
// Check Ground / Citadel Impact
if (c.y >= c.targetY) {
c.alive = false;
playImpactBoom();
emitParticles(c.x, c.y, '#ef4444', 28, 5.0, 3.5);
emitParticles(c.x, c.y, '#f59e0b', 20, 3.5, 2.5);
// Destroy target citadel if hit directly
if (c.targetCitadel && c.targetCitadel.alive) {
c.targetCitadel.alive = false;
updateCitadelHUD();
checkGameOver();
}
// Reset combo
combo = 1;
dispCombo.innerText = '1x';
waveCometsRemaining = Math.max(0, waveCometsRemaining - 1);
checkWaveProgress();
}
}
// 3. Update Missiles
for (const m of missiles) {
if (!m.alive) continue;
m.x += m.vx;
m.y += m.vy;
m.trail.push({ x: m.x, y: m.y, alpha: 0.85 });
for (const t of m.trail) t.alpha -= dt * 3.5;
m.trail = m.trail.filter(t => t.alpha > 0);
// Check Target Arrival
const distToTarget = Math.hypot(m.x - m.targetX, m.y - m.targetY);
if (distToTarget <= 14 || m.y <= m.targetY) {
m.alive = false;
spawnCrystalStarburst(m.targetX, m.targetY);
}
}
// 4. Update Crystals & Interception Collision
for (const cr of crystals) {
if (cr.growth < 1.0) {
cr.growth += cr.growthSpeed * dt;
if (cr.growth > 1.0) cr.growth = 1.0;
}
cr.life -= dt;
if (cr.life <= 0.4) {
cr.alpha = Math.max(0, cr.life / 0.4);
}
const activeRadius = cr.maxRadius * cr.growth;
// Comet vs Snowflake Crystal Collision
if (cr.growth > 0.25 && cr.alpha > 0.2) {
for (const c of comets) {
if (!c.alive) continue;
const dist = Math.hypot(c.x - cr.x, c.y - cr.y);
if (dist <= activeRadius + c.radius) {
c.alive = false;
playFreezeShatterSound();
// Spawn ice shards & flame extinguishment
emitParticles(c.x, c.y, '#38bdf8', 18, 4.0, 3.0);
emitParticles(c.x, c.y, '#ffffff', 12, 3.0, 2.0);
emitParticles(c.x, c.y, '#94a3b8', 8, 1.5, 1.5);
// Chain Reaction Snowflake Burst!
spawnCrystalStarburst(c.x, c.y, 48, true);
const pointsEarned = 100 * combo;
score += pointsEarned;
cometsDestroyed++;
waveCometsRemaining = Math.max(0, waveCometsRemaining - 1);
addFloatingText(`+${pointsEarned} ${combo > 1 ? 'x' + combo : ''}`, c.x, c.y, combo > 2 ? '#f43f5e' : '#38bdf8');
combo++;
maxCombo = Math.max(maxCombo, combo);
updateHUD();
checkWaveProgress();
}
}
}
}
// 5. Update Shockwaves
for (const sw of shockwaves) {
sw.radius += sw.growthSpeed * 60 * dt;
sw.alpha -= dt * 1.35;
for (const dot of sw.dots) {
dot.dist += dot.speed * 60 * dt;
dot.alpha = sw.alpha;
}
}
// 6. Update Particles & Floating Text
for (const p of particles) {
p.x += p.vx;
p.y += p.vy;
p.life -= dt;
p.alpha = Math.max(0, p.life / p.maxLife);
}
for (const ft of floatingTexts) {
ft.y += ft.vy;
ft.alpha -= dt * 1.2;
}
// 7. Atmospheric Embers Update
for (const e of floatingEmbers) {
e.x += e.vx;
e.y += e.vy;
if (e.y < -10) {
e.y = H + 10;
e.x = Math.random() * W;
}
}
// 8. Clean up Dead Entities
for (let i = comets.length - 1; i >= 0; i--) if (!comets[i].alive) comets.splice(i, 1);
for (let i = missiles.length - 1; i >= 0; i--) if (!missiles[i].alive) missiles.splice(i, 1);
for (let i = crystals.length - 1; i >= 0; i--) if (crystals[i].alpha <= 0) crystals.splice(i, 1);
for (let i = shockwaves.length - 1; i >= 0; i--) if (shockwaves[i].alpha <= 0) shockwaves.splice(i, 1);
for (let i = particles.length - 1; i >= 0; i--) if (particles[i].alpha <= 0) particles.splice(i, 1);
for (let i = floatingTexts.length - 1; i >= 0; i--) if (floatingTexts[i].alpha <= 0) floatingTexts.splice(i, 1);
// AI Defense Check
updateAiDefense();
}
function updateHUD() {
dispScore.innerText = score.toLocaleString();
dispWave.innerText = wave;
dispCombo.innerText = `${combo}x`;
const progress = Math.min(100, Math.round(((waveTotalComets - waveCometsRemaining) / waveTotalComets) * 100));
waveMeterFill.style.width = `${progress}%`;
dispWaveProgressText.innerText = `WAVE PROGRESS: ${waveTotalComets - waveCometsRemaining} / ${waveTotalComets}`;
dispCometsLeft.innerText = `${waveCometsRemaining} REMAINING`;
}
function updateCitadelHUD() {
citadels.forEach((c, idx) => {
if (citadelDots[idx]) {
if (c.alive) citadelDots[idx].classList.add('alive');
else citadelDots[idx].classList.remove('alive');
}
});
}
function checkWaveProgress() {
if (waveCometsRemaining <= 0 && comets.length === 0 && !isGameOver) {
// Wave Complete!
wave++;
waveTotalComets = 15 + wave * 4;
waveCometsRemaining = waveTotalComets;
addFloatingText(`WAVE ${wave} COMMENCING!`, W / 2, H * 0.35, '#f59e0b');
playTone(1046.50, 0.4, 'triangle', 0.25);
updateHUD();
}
}
function checkGameOver() {
const livingCitadels = citadels.filter(c => c.alive);
if (livingCitadels.length === 0) {
isGameOver = true;
setTimeout(showGameOverModal, 600);
}
}
function showGameOverModal() {
mStatScore.innerText = score.toLocaleString();
mStatComets.innerText = cometsDestroyed;
mStatCombo.innerText = `${maxCombo}x`;
mStatWave.innerText = `Wave ${wave}`;
modalTitle.innerText = wave >= 5 ? 'Frost Kingdom Victorious!' : 'Citadels Shattered';
modalSubtitle.innerText = wave >= 5
? 'Your Ice Crystal Interceptors broke the Inferno Comet Storm.'
: 'All Frost Citadels were overwhelmed by molten fireballs. Rebuild your defenses!';
gameOverModal.classList.remove('hidden');
}
function restartGame() {
isGameOver = false;
score = 0;
wave = 1;
combo = 1;
maxCombo = 1;
cometsDestroyed = 0;
waveTotalComets = 15;
waveCometsRemaining = 15;
comets.length = 0;
missiles.length = 0;
crystals.length = 0;
shockwaves.length = 0;
particles.length = 0;
floatingTexts.length = 0;
initCitadels();
updateHUD();
gameOverModal.classList.add('hidden');
}
// --- Rendering Pipeline ---
function draw() {
ctx.clearRect(0, 0, W, H);
// 1. Cosmic Night Sky Gradient (Deep crimson horizon)
const skyGrad = ctx.createLinearGradient(0, 0, 0, H);
skyGrad.addColorStop(0, '#040003');
skyGrad.addColorStop(0.5, '#160206');
skyGrad.addColorStop(0.85, '#3b060d');
skyGrad.addColorStop(1.0, '#540b14');
ctx.fillStyle = skyGrad;
ctx.fillRect(0, 0, W, H);
// 2. Stars
ctx.save();
for (const s of stars) {
s.phase += s.twinkleSpeed;
const alpha = s.baseAlpha + Math.sin(s.phase) * 0.25;
ctx.fillStyle = `rgba(224, 242, 254, ${Math.max(0.1, alpha)})`;
ctx.beginPath();
ctx.arc(s.x, s.y, s.radius, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
// 3. Floating Atmospheric Embers
ctx.save();
for (const e of floatingEmbers) {
ctx.fillStyle = e.color;
ctx.globalAlpha = e.alpha;
ctx.beginPath();
ctx.arc(e.x, e.y, e.radius, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
// 4. Layered Mountain Silhouettes
for (const ridge of mountainRidges) {
ctx.fillStyle = ridge.color;
ctx.beginPath();
ctx.moveTo(0, H);
for (const p of ridge.points) {
ctx.lineTo(p.x, p.y);
}
ctx.lineTo(W, H);
ctx.closePath();
ctx.fill();
}
// 5. Ground Line with Subtle Neon Grid
ctx.strokeStyle = 'rgba(56, 189, 248, 0.25)';
ctx.lineWidth = 1.5;
ctx.beginPath();
ctx.moveTo(0, H - 28);
ctx.lineTo(W, H - 28);
ctx.stroke();
// 6. Frost Citadels & Glowing Aura Halos
for (const c of citadels) {
if (c.alive) {
// Glowing Aura Circle
const auraGrad = ctx.createRadialGradient(c.x, c.y, 2, c.x, c.y, c.glowRadius);
auraGrad.addColorStop(0, 'rgba(56, 189, 248, 0.45)');
auraGrad.addColorStop(0.6, 'rgba(14, 116, 144, 0.18)');
auraGrad.addColorStop(1, 'rgba(56, 189, 248, 0)');
ctx.fillStyle = auraGrad;
ctx.beginPath();
ctx.arc(c.x, c.y, c.glowRadius, 0, Math.PI * 2);
ctx.fill();
if (c.type === 'shield') {
// Central Shield Citadel (Pentagon Fortress)
ctx.save();
ctx.translate(c.x, c.y);
ctx.fillStyle = '#0284c7';
ctx.strokeStyle = '#38bdf8';
ctx.lineWidth = 2.5;
ctx.shadowBlur = 16;
ctx.shadowColor = '#38bdf8';
ctx.beginPath();
// Draw Pentagon / Shield
const r = 20;
for (let i = 0; i < 5; i++) {
const angle = (i * Math.PI * 2) / 5 - Math.PI / 2;
const px = Math.cos(angle) * r;
const py = Math.sin(angle) * r;
if (i === 0) ctx.moveTo(px, py);
else ctx.lineTo(px, py);
}
ctx.closePath();
ctx.fill();
ctx.stroke();
// Inner Glowing Core
ctx.fillStyle = '#e0f2fe';
ctx.beginPath();
ctx.arc(0, 0, 7, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
} else {
// Side Crystal Spires (Obelisks)
ctx.save();
ctx.translate(c.x, c.y);
ctx.fillStyle = '#0369a1';
ctx.strokeStyle = '#38bdf8';
ctx.lineWidth = 2;
ctx.shadowBlur = 12;
ctx.shadowColor = '#38bdf8';
ctx.beginPath();
ctx.moveTo(0, -22);
ctx.lineTo(8, -6);
ctx.lineTo(6, 12);
ctx.lineTo(-6, 12);
ctx.lineTo(-8, -6);
ctx.closePath();
ctx.fill();
ctx.stroke();
// Core Highlight
ctx.fillStyle = '#e0f2fe';
ctx.beginPath();
ctx.moveTo(0, -18);
ctx.lineTo(3, -4);
ctx.lineTo(-3, -4);
ctx.closePath();
ctx.fill();
ctx.restore();
}
} else {
// Destroyed Ruins
ctx.fillStyle = '#1e293b';
ctx.beginPath();
ctx.arc(c.x, c.y + 6, 10, 0, Math.PI * 2);
ctx.fill();
}
}
// 7. Missile Launch Trails & Projectiles
for (const m of missiles) {
if (!m.alive) continue;
// Trail line
if (m.trail.length > 1) {
ctx.save();
ctx.strokeStyle = 'rgba(56, 189, 248, 0.7)';
ctx.lineWidth = 2;
ctx.shadowBlur = 8;
ctx.shadowColor = '#38bdf8';
ctx.beginPath();
ctx.moveTo(m.trail[0].x, m.trail[0].y);
for (let i = 1; i < m.trail.length; i++) {
ctx.lineTo(m.trail[i].x, m.trail[i].y);
}
ctx.stroke();
ctx.restore();
}
// Missile Head
ctx.save();
ctx.fillStyle = '#ffffff';
ctx.shadowBlur = 10;
ctx.shadowColor = '#38bdf8';
ctx.beginPath();
ctx.arc(m.x, m.y, 2.5, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// 8. Comets (Molten Core & Flame Smoke Plumes)
for (const c of comets) {
if (!c.alive) continue;
// Flame Plume Particles
for (const p of c.tailParticles) {
ctx.fillStyle = p.color;
ctx.globalAlpha = p.alpha;
ctx.beginPath();
ctx.arc(p.x, p.y, p.radius, 0, Math.PI * 2);
ctx.fill();
}
ctx.globalAlpha = 1.0;
// Comet Fiery Core
ctx.save();
const cometGrad = ctx.createRadialGradient(c.x, c.y, 1, c.x, c.y, c.radius);
cometGrad.addColorStop(0, '#ffffff');
cometGrad.addColorStop(0.3, '#ffcc00');
cometGrad.addColorStop(0.7, '#ff4400');
cometGrad.addColorStop(1, 'rgba(255, 34, 0, 0)');
ctx.fillStyle = cometGrad;
ctx.shadowBlur = 18;
ctx.shadowColor = '#ff4400';
ctx.beginPath();
ctx.arc(c.x, c.y, c.radius, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// 9. Dotted Shockwave Rings
for (const sw of shockwaves) {
ctx.save();
ctx.globalAlpha = sw.alpha;
// Subtle expanding circle outline
ctx.strokeStyle = 'rgba(56, 189, 248, 0.4)';
ctx.lineWidth = 1.2;
ctx.beginPath();
ctx.arc(sw.x, sw.y, sw.radius, 0, Math.PI * 2);
ctx.stroke();
// Discrete Sparkling Ring Dots (matching media reference)
ctx.fillStyle = '#ffffff';
ctx.shadowBlur = 6;
ctx.shadowColor = '#38bdf8';
for (const dot of sw.dots) {
const dx = sw.x + Math.cos(dot.angle) * dot.dist;
const dy = sw.y + Math.sin(dot.angle) * dot.dist;
ctx.beginPath();
ctx.arc(dx, dy, dot.size, 0, Math.PI * 2);
ctx.fill();
}
ctx.restore();
}
// 10. Procedural Snowflake Crystals (Fractal Starbursts)
for (const cr of crystals) {
if (cr.alpha <= 0) continue;
ctx.save();
ctx.translate(cr.x, cr.y);
ctx.rotate(cr.rotation);
const r = cr.maxRadius * cr.growth;
const alpha = Math.min(1.0, cr.alpha);
// Pass A: Radiant Cyan Glowing Spines
ctx.strokeStyle = `rgba(56, 189, 248, ${alpha * 0.9})`;
ctx.lineWidth = 3.2;
ctx.lineCap = 'round';
ctx.shadowBlur = 14;
ctx.shadowColor = '#00e5ff';
ctx.beginPath();
for (const spine of cr.spines) {
const cosA = Math.cos(spine.angle);
const sinA = Math.sin(spine.angle);
const tipX = cosA * r;
const tipY = sinA * r;
ctx.moveTo(0, 0);
ctx.lineTo(tipX, tipY);
// Branching Dendrites
for (const sub of spine.subBranches) {
if (cr.growth < sub.posRatio * 0.4) continue;
const bx = cosA * (r * sub.posRatio);
const by = sinA * (r * sub.posRatio);
const bLen = r * sub.branchLen * Math.min(1.0, (cr.growth - sub.posRatio * 0.25) * 2.5);
if (bLen > 0) {
const a1 = spine.angle + sub.angleOffset;
ctx.moveTo(bx, by);
ctx.lineTo(bx + Math.cos(a1) * bLen, by + Math.sin(a1) * bLen);
const a2 = spine.angle - sub.angleOffset;
ctx.moveTo(bx, by);
ctx.lineTo(bx + Math.cos(a2) * bLen, by + Math.sin(a2) * bLen);
}
}
}
ctx.stroke();
// Pass B: Pure Icy White Core Strokes
ctx.strokeStyle = `rgba(255, 255, 255, ${alpha})`;
ctx.lineWidth = 1.3;
ctx.shadowBlur = 4;
ctx.shadowColor = '#ffffff';
ctx.beginPath();
for (const spine of cr.spines) {
const cosA = Math.cos(spine.angle);
const sinA = Math.sin(spine.angle);
const tipX = cosA * r;
const tipY = sinA * r;
ctx.moveTo(0, 0);
ctx.lineTo(tipX, tipY);
for (const sub of spine.subBranches) {
if (cr.growth < sub.posRatio * 0.4) continue;
const bx = cosA * (r * sub.posRatio);
const by = sinA * (r * sub.posRatio);
const bLen = r * sub.branchLen * Math.min(1.0, (cr.growth - sub.posRatio * 0.25) * 2.5);
if (bLen > 0) {
const a1 = spine.angle + sub.angleOffset;
ctx.moveTo(bx, by);
ctx.lineTo(bx + Math.cos(a1) * bLen, by + Math.sin(a1) * bLen);
const a2 = spine.angle - sub.angleOffset;
ctx.moveTo(bx, by);
ctx.lineTo(bx + Math.cos(a2) * bLen, by + Math.sin(a2) * bLen);
}
}
}
ctx.stroke();
// Center Seed Starburst Gem
ctx.fillStyle = `rgba(255, 255, 255, ${alpha})`;
ctx.beginPath();
ctx.arc(0, 0, Math.min(3.5, r * 0.08), 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// 11. Sparks & Debris Particles
for (const p of particles) {
ctx.save();
ctx.fillStyle = p.color;
ctx.globalAlpha = p.alpha;
ctx.beginPath();
ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
// 12. Floating Score Texts
ctx.save();
ctx.font = '800 13px ui-monospace, SFMono-Regular, monospace';
ctx.textAlign = 'center';
for (const ft of floatingTexts) {
ctx.fillStyle = ft.color;
ctx.globalAlpha = ft.alpha;
ctx.shadowBlur = 8;
ctx.shadowColor = ft.color;
ctx.fillText(ft.text, ft.x, ft.y);
}
ctx.restore();
// 13. Crosshair Reticle (subtle cyan '+')
if (mouseX > 0 && mouseY > 0 && mouseY < H - 40) {
ctx.save();
ctx.strokeStyle = 'rgba(56, 189, 248, 0.7)';
ctx.lineWidth = 1.2;
ctx.beginPath();
ctx.moveTo(mouseX - 8, mouseY);
ctx.lineTo(mouseX + 8, mouseY);
ctx.moveTo(mouseX, mouseY - 8);
ctx.lineTo(mouseX, mouseY + 8);
ctx.stroke();
ctx.beginPath();
ctx.arc(mouseX, mouseY, 4, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
}
// --- Animation Loop ---
let lastTime = performance.now();
function gameLoop(now) {
const dt = Math.min((now - lastTime) / 1000, 0.1);
lastTime = now;
updateGame(dt);
draw();
requestAnimationFrame(gameLoop);
}
// --- Event Listeners & Interaction ---
window.addEventListener('resize', initDimensions);
canvas.addEventListener('mousemove', (e) => {
const rect = canvas.getBoundingClientRect();
mouseX = e.clientX - rect.left;
mouseY = e.clientY - rect.top;
});
canvas.addEventListener('mouseleave', () => {
mouseX = -100;
mouseY = -100;
});
canvas.addEventListener('pointerdown', (e) => {
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (y < H - 40) {
launchMissile(x, y);
}
});
window.addEventListener('keydown', (e) => {
if (e.code === 'Space') {
e.preventDefault();
triggerFreezeBeam();
}
});
btnAiMode.addEventListener('click', () => {
aiMode = !aiMode;
btnAiMode.querySelector('.btn-text').innerText = aiMode ? 'AI: ON' : 'AI: OFF';
btnAiMode.style.borderColor = aiMode ? 'var(--cyan-primary)' : 'var(--card-border)';
});
btnFreezeBeam.addEventListener('click', triggerFreezeBeam);
btnAudioToggle.addEventListener('click', () => {
audioEnabled = !audioEnabled;
btnAudioToggle.innerHTML = audioEnabled ? '<i class="fas fa-volume-up"></i>' : '<i class="fas fa-volume-xmark"></i>';
});
btnRestartGame.addEventListener('click', restartGame);
btnPlayAgain.addEventListener('click', restartGame);
// --- Boot Game ---
initDimensions();
updateHUD();
requestAnimationFrame(gameLoop);
})();3. Core JavaScript Concepts You Learned
Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.
Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.
Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.
Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.
Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.
Crucial concept utilized to power the dynamic state, user interactions, and styling of this project.
4. How to Run Locally on Your Computer
- Download the project ZIP: Click the Download Project (.ZIP) button at the top or in the live runner to save
missile-command-frost-vs-inferno-project.zip. - Extract the archive: Unzip the downloaded file. Inside you will find separate
index.html,style.css,script.js,index-standalone.html, and a comprehensiveREADME.md. - Open in any browser: Simply double-click
index.html(orindex-standalone.html) to open and test immediately in Chrome, Firefox, Safari, or Edge. - Edit in Visual Studio Code: Open the extracted folder in VS Code. Right-click
index.htmland select "Open with Live Server" to enjoy instant live reloading as you modify HTML, CSS, or JS!
Bonus Challenges for Learners
Ready to level up? Try adding these extra features on your own:
- Add sound effects or audio feedback for user button clicks.
- Add custom theme color customizer with CSS variables.
- Export the results to PDF or copy as a formatted text summary.