APACHE - INTERACTIVE ARCADE GAME SOURCE CODE
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>Apache//Can.yon — Dusk Ops</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Oswald:wght@500;600;700&family=Share+Tech+Mono&display=swap');
/* ===== Design tokens: sunset desert combat ===== */
:root {
--ink: #140b08;
--panel: #1c110b;
--sky-top: #1e1330;
--sky-mid: #7a2f3d;
--sky-horizon: #e8763a;
--sun-core: #ffe9b8;
--sun-glow: #ffb454;
--sand-shadow: #3a1c10;
--sand-dark: #6b2f1a;
--sand-mid: #9c4a26;
--sand-light: #d98a4f;
--rim-light: #ffcf8a;
--amber: #f0a44c;
--amber-bright: #ffc36b;
--rust-red: #d9502f;
--dust: #c9a67a;
}
* {
margin: 0; padding: 0; box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
}
body {
background: var(--ink);
color: var(--amber);
font-family: 'Share Tech Mono', 'Courier New', monospace;
overflow: hidden;
width: 100vw; height: 100vh;
display: flex; justify-content: center; align-items: center;
background-image:
linear-gradient(rgba(240, 164, 76, 0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(240, 164, 76, 0.04) 1px, transparent 1px);
background-size: 40px 40px;
touch-action: none;
position: relative;
}
#gameContainer {
position: relative;
width: 100%; height: 100%; max-width: 1200px;
display: flex; flex-direction: column;
border: 1px solid var(--sand-mid);
box-shadow: 0 0 40px rgba(217, 138, 79, 0.15), inset 0 0 90px rgba(0,0,0,0.65);
overflow: hidden;
}
#hud {
display: flex; flex-direction: row; flex-wrap: wrap;
align-items: center; justify-content: center;
gap: clamp(6px, 1.4vw, 18px);
padding: clamp(4px, 0.8vh, 8px) clamp(8px, 1.5vw, 14px);
background: linear-gradient(180deg, rgba(28,17,11,0.97), rgba(20,11,8,0.92));
border-bottom: 1px solid var(--sand-mid);
font-size: clamp(9px, 1.3vh, 13px);
flex-shrink: 0;
z-index: 3;
}
.hud-item {
display: flex; align-items: baseline; gap: 5px;
padding: 0 6px;
border-left: 2px solid rgba(240, 164, 76, 0.5);
transition: filter 0.2s ease;
}
.hud-item:first-child { border-left: none; }
.hud-label {
font-family: 'Oswald', sans-serif;
color: var(--dust);
font-size: clamp(7px, 1vh, 10px);
letter-spacing: 0.08em;
text-transform: uppercase;
opacity: 0.85;
white-space: nowrap;
}
.hud-value {
color: var(--amber-bright);
font-size: clamp(11px, 1.7vh, 16px);
font-weight: bold;
text-shadow: 0 0 8px rgba(255, 180, 84, 0.5);
letter-spacing: 0.03em;
white-space: nowrap;
transition: color 0.3s ease, text-shadow 0.3s ease;
}
.hud-bar-item { gap: 6px; }
.hud-bar {
position: relative;
width: clamp(46px, 8vw, 90px);
height: clamp(10px, 1.8vh, 15px);
border: 1px solid var(--sand-mid);
background: rgba(0, 0, 0, 0.55);
overflow: hidden;
box-shadow: inset 0 0 8px rgba(217, 138, 79, 0.12);
}
.hud-bar-fill {
position: absolute; top: 0; bottom: 0; left: 0;
background: linear-gradient(to right, var(--rust-red), var(--amber));
transition: width 0.3s ease, background 0.3s ease;
box-shadow: 0 0 10px rgba(240, 164, 76, 0.5);
}
.hud-bar-value {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
font-size: clamp(7px, 1.1vh, 10px); font-weight: bold;
color: var(--amber-bright);
text-shadow: 0 0 4px rgba(0,0,0,0.9);
white-space: nowrap;
}
#gameCanvas {
flex: 1; min-width: 0; min-height: 0;
background: #000;
touch-action: none; display: block;
}
#message {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
font-family: 'Oswald', sans-serif;
font-size: clamp(20px, 4vh, 32px);
color: var(--amber-bright);
text-align: center;
text-shadow: 0 0 20px rgba(255,180,84,0.6), 2px 0 rgba(217,80,47,0.4), -2px 0 rgba(255,233,184,0.3);
display: none; z-index: 20;
background: rgba(20, 11, 8, 0.97);
padding: clamp(16px, 4vh, 32px);
border: 2px solid var(--amber);
box-shadow: 0 0 50px rgba(217, 138, 79, 0.35);
max-width: 90vw; max-height: 90vh; overflow: auto;
}
.title-screen { text-align: center; line-height: 1.5; }
.controls-hint {
position: absolute; bottom: clamp(8px, 2vh, 16px); left: 50%;
transform: translateX(-50%);
font-size: clamp(10px, 1.4vh, 14px);
color: var(--amber);
text-align: center; letter-spacing: 0.05em;
background: rgba(20, 11, 8, 0.97);
padding: clamp(6px, 1vh, 10px) clamp(12px, 2vh, 16px);
border: 1px solid var(--sand-mid);
opacity: 0.9; max-width: 90vw; white-space: normal; line-height: 1.4;
z-index: 7;
}
#muteStatus { margin-left: 6px; opacity: 0.9; }
/* ---- atmosphere overlays: dust haze + warm vignette (replaces CRT phosphor look) ---- */
.dust-grain {
position: absolute; inset: 0; pointer-events: none; z-index: 5;
background:
radial-gradient(1px 1px at 10% 20%, rgba(255,220,170,0.5), transparent),
radial-gradient(1px 1px at 80% 60%, rgba(255,220,170,0.4), transparent),
radial-gradient(1px 1px at 40% 80%, rgba(255,220,170,0.35), transparent),
radial-gradient(1px 1px at 65% 15%, rgba(255,220,170,0.4), transparent),
radial-gradient(1px 1px at 25% 55%, rgba(255,220,170,0.3), transparent);
background-size: 300px 300px;
opacity: 0.5;
animation: driftDust 22s linear infinite;
mix-blend-mode: screen;
}
@keyframes driftDust {
0% { background-position: 0 0; }
100% { background-position: -300px 400px; }
}
.warm-vignette {
position: absolute; inset: 0; pointer-events: none; z-index: 6;
background: radial-gradient(ellipse at 50% 60%, rgba(0,0,0,0) 45%, rgba(20,8,4,0.7) 100%);
transition: background 0.2s ease;
}
.heat-flicker { animation: flicker 7s infinite; }
@keyframes flicker {
0%, 100% { opacity: 1; }
91% { opacity: 1; }
92% { opacity: 0.93; }
93% { opacity: 1; }
96% { opacity: 0.88; }
97% { opacity: 1; }
}
#gameContainer.critical .warm-vignette {
animation: critPulse 0.55s ease-in-out infinite;
}
@keyframes critPulse {
0%, 100% { background: radial-gradient(ellipse at 50% 60%, rgba(217,80,47,0) 40%, rgba(217,80,47,0.35) 100%); }
50% { background: radial-gradient(ellipse at 50% 60%, rgba(217,80,47,0) 50%, rgba(217,80,47,0.6) 100%); }
}
#gameContainer.critical .hud-item {
filter: drop-shadow(0 0 5px rgba(217,80,47,0.7));
}
.mobile-warning {
display: none;
position: fixed; top: 0; left: 0; width: 100vw; height: 100vh;
background: radial-gradient(ellipse at 50% 30%, #3a1c10 0%, #140b08 70%);
z-index: 1000;
flex-direction: column; justify-content: center; align-items: center;
padding: 20px; text-align: center;
border: 3px solid var(--amber);
box-shadow: 0 0 60px rgba(217, 138, 79, 0.4) inset;
}
.warning-content {
max-width: 800px; width: 90%; padding: 40px;
background: rgba(20, 11, 8, 0.96);
border: 2px solid var(--amber);
box-shadow: 0 0 40px rgba(217, 138, 79, 0.3);
}
.warning-title {
font-family: 'Oswald', sans-serif;
font-size: clamp(24px, 5vh, 48px);
color: var(--amber-bright);
margin-bottom: 30px;
text-shadow: 0 0 20px rgba(255,180,84,0.6), 2px 0 rgba(217,80,47,0.5), -2px 0 rgba(255,233,184,0.4);
letter-spacing: 0.1em; line-height: 1.3;
}
.warning-message { font-size: clamp(16px, 3vh, 24px); color: var(--amber); margin-bottom: 40px; line-height: 1.6; opacity: 0.9; }
.warning-subtitle { font-size: clamp(14px, 2.5vh, 20px); color: var(--dust); margin-top: 40px; opacity: 0.8; border-top: 1px solid var(--sand-mid); padding-top: 20px; }
.scan-effect {
position: absolute; top: 0; left: 0; width: 100%; height: 2px;
background: rgba(255, 180, 84, 0.5); box-shadow: 0 0 10px var(--amber);
animation: scan 3s linear infinite;
}
@keyframes scan { 0% { top: 0; opacity: 1; } 50% { opacity: 0.7; } 100% { top: 100%; opacity: 1; } }
.grid-overlay-warning {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background-image: linear-gradient(rgba(240, 164, 76, 0.04) 1px, transparent 1px), linear-gradient(90deg, rgba(240, 164, 76, 0.04) 1px, transparent 1px);
background-size: 30px 30px; pointer-events: none;
}
.version-label {
position: absolute; top: clamp(8px, 2vh, 16px); right: clamp(8px, 2vw, 16px);
font-size: clamp(8px, 1.2vh, 12px); color: var(--dust); opacity: 0.6; letter-spacing: 0.05em; z-index: 7;
}
.grid-overlay {
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
background-image: linear-gradient(rgba(240, 164, 76, 0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(240, 164, 76, 0.03) 1px, transparent 1px);
background-size: 20px 20px; pointer-events: none; z-index: 1;
}
@media (max-width: 480px) {
#hud { gap: clamp(4px, 1.5vw, 10px); padding: 4px 6px; }
.hud-item { padding: 0 4px; }
.hud-bar { width: clamp(38px, 14vw, 60px); }
}
@media (max-height: 400px) {
#hud { padding: 2px 6px; gap: 4px; }
.hud-label { display: none; }
}
body.mobile-active #gameContainer { display: none !important; }
body.mobile-active .mobile-warning { display: flex !important; }
</style>
</head>
<body>
<div class="mobile-warning" id="mobileWarning">
<div class="grid-overlay-warning"></div>
<div class="scan-effect"></div>
<div class="warning-content">
<div class="warning-title">APACHE//CAN.YON<br>DUSK OPS — VER 5.0GB</div>
<div class="warning-message">
<div style="margin-bottom: 20px; color: #d9502f; text-shadow: 0 0 10px #d9502f;">⚠️ DESKTOP KEYBOARD REQUIRED ⚠️</div>
<div style="margin-bottom: 30px;">This simulation requires physical keyboard controls<br>and cannot be played on mobile devices.</div>
<div style="font-size: 0.9em; opacity: 0.85;">
REQUIRED CONTROLS:<br>
[Z]: FIRE WEAPON<br>
[X]: LAUNCH MISSILE<br>
[←] [→]: NAVIGATE CANYON<br>
[SPACE]: START/PAUSE<br>
[M]: MUTE AUDIO
</div>
</div>
<div class="warning-subtitle">Please use a desktop or laptop computer<br>with a keyboard to play this game.</div>
</div>
</div>
<div id="gameContainer">
<div class="grid-overlay"></div>
<div class="version-label">DUSK OPS · VER 5.0GB</div>
<div id="hud">
<div class="hud-item">
<span class="hud-label">HI-SC</span><span class="hud-value" id="hiscore">000000</span>
</div>
<div class="hud-item">
<span class="hud-label">SCORE</span><span class="hud-value" id="score">000000</span>
</div>
<div class="hud-item">
<span class="hud-label">LIVES</span><span class="hud-value" id="lives">03</span>
</div>
<div class="hud-item">
<span class="hud-label">TIME</span><span class="hud-value" id="time">000</span>
</div>
<div class="hud-item">
<span class="hud-label">MSL</span><span class="hud-value" id="missiles">10</span>
</div>
<div class="hud-item">
<span class="hud-label">SPD</span><span class="hud-value" id="speed">2.5</span>
</div>
<div class="hud-item hud-bar-item">
<span class="hud-label">SHLD</span>
<div class="hud-bar">
<div class="hud-bar-fill" id="shieldsBar"></div>
<span class="hud-bar-value" id="shieldsValue">100%</span>
</div>
</div>
<div class="hud-item hud-bar-item">
<span class="hud-label">FUEL</span>
<div class="hud-bar">
<div class="hud-bar-fill" id="fuelBar"></div>
<span class="hud-bar-value" id="fuelValue">100%</span>
</div>
</div>
</div>
<canvas id="gameCanvas"></canvas>
<div class="dust-grain heat-flicker"></div>
<div class="warm-vignette"></div>
<div id="message"></div>
<div class="controls-hint">[Z]: FIRE | [X]: MISSILE | [←]: LEFT | [→]: RIGHT | [SPACE]: START/PAUSE<span id="muteStatus">[M]: MUTE (OFF)</span></div>
</div>
<script>
const isMobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
const body = document.body;
if (isMobile) body.classList.add('mobile-active');
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// ===================== Palette =====================
const PAL = {
skyTop: '#1e1330',
skyMid: '#7a2f3d',
skyHorizon: '#e8763a',
sunCore: '#ffe9b8',
sunGlow: '#ffb454',
sandShadow: '#3a1c10',
sandDark: '#6b2f1a',
sandMid: '#9c4a26',
sandLight: '#d98a4f',
rim: '#ffcf8a',
amber: '#f0a44c',
amberBright: '#ffc36b',
rust: '#d9502f',
dust: '#c9a67a'
};
// ===================== AUDIO ENGINE (procedural, no external files) =====================
const AudioEngine = {
ctx: null, master: null, muted: false, noiseBuffer: null,
engineOsc: null, engineGain: null,
init() {
if (this.ctx) return;
try { this.ctx = new (window.AudioContext || window.webkitAudioContext)(); } catch (e) { return; }
this.master = this.ctx.createGain();
this.master.gain.value = this.muted ? 0 : 0.35;
this.master.connect(this.ctx.destination);
const bufferSize = this.ctx.sampleRate * 1;
this.noiseBuffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = this.noiseBuffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
},
resume() { if (this.ctx && this.ctx.state === 'suspended') this.ctx.resume(); },
tone(freq, duration, type = 'square', vol = 0.25, sweepTo = null) {
if (!this.ctx) return;
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type;
const t0 = this.ctx.currentTime;
osc.frequency.setValueAtTime(freq, t0);
if (sweepTo) osc.frequency.exponentialRampToValueAtTime(Math.max(20, sweepTo), t0 + duration);
gain.gain.setValueAtTime(vol, t0);
gain.gain.exponentialRampToValueAtTime(0.001, t0 + duration);
osc.connect(gain); gain.connect(this.master);
osc.start(t0); osc.stop(t0 + duration + 0.02);
},
noise(duration, vol = 0.3, filterFreq = 1000) {
if (!this.ctx) return;
const src = this.ctx.createBufferSource();
src.buffer = this.noiseBuffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass'; filter.frequency.value = filterFreq;
const gain = this.ctx.createGain();
const t0 = this.ctx.currentTime;
gain.gain.setValueAtTime(vol, t0);
gain.gain.exponentialRampToValueAtTime(0.001, t0 + duration);
src.connect(filter); filter.connect(gain); gain.connect(this.master);
src.start(t0); src.stop(t0 + duration + 0.02);
},
laser() { this.tone(880, 0.08, 'square', 0.14, 440); },
missile() { this.tone(220, 0.3, 'sawtooth', 0.18, 90); this.noise(0.2, 0.08, 2200); },
explosion(big = false) {
this.noise(big ? 0.55 : 0.28, big ? 0.45 : 0.28, big ? 400 : 900);
this.tone(big ? 80 : 150, big ? 0.5 : 0.25, 'sawtooth', 0.18, 35);
},
hit() { this.tone(200, 0.06, 'square', 0.14, 80); },
powerup() { this.tone(440, 0.08, 'sine', 0.18, 880); this.tone(880, 0.1, 'sine', 0.14, 1320); },
scrape() { this.noise(0.09, 0.07, 3200); },
gameover() { this.tone(220, 0.6, 'sawtooth', 0.22, 55); },
start() { this.tone(440, 0.1, 'square', 0.18, 880); },
alarmBeep() { this.tone(660, 0.1, 'square', 0.16, 560); },
setEngine(speedRatio) {
if (!this.ctx) return;
if (!this.engineOsc) {
this.engineOsc = this.ctx.createOscillator();
this.engineOsc.type = 'sawtooth';
this.engineGain = this.ctx.createGain();
this.engineGain.gain.value = 0;
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass'; filter.frequency.value = 220;
this.engineOsc.connect(filter); filter.connect(this.engineGain); this.engineGain.connect(this.master);
this.engineOsc.start();
}
this.engineOsc.frequency.setValueAtTime(55 + speedRatio * 35, this.ctx.currentTime);
this.engineGain.gain.setTargetAtTime(0.035, this.ctx.currentTime, 0.15);
},
stopEngine() { if (this.engineGain) this.engineGain.gain.setTargetAtTime(0, this.ctx.currentTime, 0.25); },
toggleMute() {
this.muted = !this.muted;
if (this.master) this.master.gain.value = this.muted ? 0 : 0.35;
return this.muted;
}
};
function resizeCanvas() {
if (isMobile) return;
const container = document.getElementById('gameContainer');
const hud = document.getElementById('hud');
const containerRect = container.getBoundingClientRect();
const hudHeight = hud ? hud.getBoundingClientRect().height : 0;
canvas.width = containerRect.width;
canvas.height = containerRect.height - hudHeight;
if (canvas.width < 100) canvas.width = 100;
if (canvas.height < 100) canvas.height = 100;
if (game && game.player) {
game.player.x = Math.max(10, Math.min(canvas.width - game.player.width - 10, game.player.x));
game.player.y = canvas.height * 0.7;
}
if (game && game.canyon && game.canyon.leftWall.length > 0) {
game.canyon.leftWall = []; game.canyon.rightWall = []; game.canyon.segmentCounter = 0;
seedCanyonSegments();
}
initDust();
initRidges();
}
window.addEventListener('load', () => { if (!isMobile) { setTimeout(resizeCanvas, 100); setTimeout(resizeCanvas, 500); } });
let resizeTimeout;
window.addEventListener('resize', () => {
if (isMobile) return;
clearTimeout(resizeTimeout);
resizeTimeout = setTimeout(resizeCanvas, 100);
});
let game = {
state: 'start',
score: 0, hiscore: 0, fuel: 100, shields: 100, missiles: 10, lives: 3,
scrollSpeed: 2.5, time: 0, frame: 0, idleFrame: 0,
canyon: { leftWall: [], rightWall: [], width: 300, segmentCounter: 0 },
player: { x: 0, y: 0, width: 16, height: 16, speed: 4 },
bullets: [], playerMissiles: [], enemies: [], powerups: [], forceFields: [],
particles: [], shockwaves: [], dustMotes: [], ridges: [],
keys: {},
enemySpawnTimer: 0, powerupSpawnTimer: 0,
shake: { intensity: 0 },
muzzleFlash: 0, playerFlash: 0
};
if (!isMobile) {
function seedCanyonSegments() {
// Seed from below the bottom of the screen up through the lookahead
// buffer, so the corridor already covers the whole visible area
// (including the player's row) on the very first frame.
const startY = canvas.height + 100;
const count = Math.ceil((startY + 800) / 4);
for (let i = 0; i < count; i++) addCanyonSegment(startY - i * 4);
}
function initCanyon() {
game.canyon.leftWall = []; game.canyon.rightWall = []; game.canyon.segmentCounter = 0;
seedCanyonSegments();
}
function addCanyonSegment(y) {
const centerX = canvas.width / 2;
const t = game.canyon.segmentCounter;
const variation = Math.sin(t * 0.0234) * 70 + Math.cos(t * 0.0431) * 55 + Math.sin(t * 0.0627) * 40 + Math.cos(t * 0.0089) * 60 + Math.sin(t * 0.0153) * 35;
const narrowing = Math.sin(t * 0.0179) * 55 + Math.cos(t * 0.0347) * 45 + Math.sin(t * 0.0521) * 30;
const leftX = centerX - game.canyon.width / 2 + variation - narrowing;
const rightX = centerX + game.canyon.width / 2 + variation + narrowing;
game.canyon.leftWall.push({ x: leftX, y: y });
game.canyon.rightWall.push({ x: rightX, y: y });
game.canyon.segmentCounter++;
}
function updateCanyon() {
game.canyon.width = Math.min(300, canvas.width * 0.7);
game.canyon.leftWall.forEach(p => p.y += game.scrollSpeed);
game.canyon.rightWall.forEach(p => p.y += game.scrollSpeed);
while (game.canyon.leftWall[0] && game.canyon.leftWall[0].y > canvas.height + 50) {
game.canyon.leftWall.shift(); game.canyon.rightWall.shift();
}
while (game.canyon.leftWall.length < canvas.height / 4 + 100) {
const lastY = game.canyon.leftWall[game.canyon.leftWall.length - 1].y;
addCanyonSegment(lastY - 4);
}
}
// ---------------- Dust motes drifting through the corridor ----------------
function initDust() {
game.dustMotes = [];
const count = Math.floor((canvas.width * canvas.height) / 9000);
for (let i = 0; i < count; i++) {
game.dustMotes.push({
x: Math.random() * canvas.width, y: Math.random() * canvas.height,
size: Math.random() * 1.6 + 0.5, speed: Math.random() * 0.6 + 0.2,
drift: (Math.random() - 0.5) * 0.3,
alpha: Math.random() * 0.4 + 0.1, phase: Math.random() * Math.PI * 2
});
}
}
function updateDust() {
game.dustMotes.forEach(s => {
s.y += s.speed * (game.scrollSpeed / 2.5);
s.x += s.drift;
s.phase += 0.03;
if (s.y > canvas.height) { s.y = 0; s.x = Math.random() * canvas.width; }
});
}
function drawDust() {
game.dustMotes.forEach(s => {
const tw = 0.5 + Math.sin(s.phase) * 0.5;
ctx.fillStyle = `rgba(255,208,150,${(s.alpha * tw).toFixed(3)})`;
ctx.fillRect(s.x, s.y, s.size, s.size);
});
}
// ---------------- Distant mountain ridgelines (parallax) ----------------
function initRidges() {
game.ridges = [[], []];
for (let layer = 0; layer < 2; layer++) {
let pts = [];
let h = canvas.height * (layer === 0 ? 0.42 : 0.5);
for (let x = -50; x <= canvas.width + 50; x += 40) {
h += (Math.random() - 0.5) * (layer === 0 ? 24 : 34);
h = Math.max(canvas.height * 0.25, Math.min(canvas.height * 0.6, h));
pts.push({ x, y: h });
}
game.ridges[layer] = pts;
}
}
function drawRidges() {
const layers = [
{ pts: game.ridges[0], color: 'rgba(58, 28, 16, 0.75)' },
{ pts: game.ridges[1], color: 'rgba(107, 47, 26, 0.6)' }
];
layers.forEach(layer => {
if (!layer.pts.length) return;
ctx.fillStyle = layer.color;
ctx.beginPath();
ctx.moveTo(layer.pts[0].x, canvas.height * 0.65);
layer.pts.forEach(p => ctx.lineTo(p.x, p.y));
ctx.lineTo(layer.pts[layer.pts.length - 1].x, canvas.height * 0.65);
ctx.closePath();
ctx.fill();
});
}
function drawSky() {
const grad = ctx.createLinearGradient(0, 0, 0, canvas.height * 0.7);
grad.addColorStop(0, PAL.skyTop);
grad.addColorStop(0.45, PAL.skyMid);
grad.addColorStop(1, PAL.skyHorizon);
ctx.fillStyle = grad;
ctx.fillRect(0, 0, canvas.width, canvas.height);
// sun with soft glow, low on the horizon
const sunX = canvas.width * 0.5;
const sunY = canvas.height * 0.44;
const sunR = Math.min(70, canvas.width * 0.09);
const glow = ctx.createRadialGradient(sunX, sunY, 0, sunX, sunY, sunR * 3.2);
glow.addColorStop(0, 'rgba(255,233,184,0.55)');
glow.addColorStop(0.4, 'rgba(255,180,84,0.22)');
glow.addColorStop(1, 'rgba(255,180,84,0)');
ctx.fillStyle = glow;
ctx.beginPath(); ctx.arc(sunX, sunY, sunR * 3.2, 0, Math.PI * 2); ctx.fill();
ctx.save();
ctx.shadowColor = PAL.sunGlow; ctx.shadowBlur = 25;
ctx.fillStyle = PAL.sunCore;
ctx.beginPath(); ctx.arc(sunX, sunY, sunR, 0, Math.PI * 2); ctx.fill();
ctx.restore();
// heat-haze shimmer band near horizon
ctx.save();
ctx.globalAlpha = 0.25;
for (let i = 0; i < 4; i++) {
const yy = canvas.height * 0.6 + i * 6;
const wobble = Math.sin(game.idleFrame * 0.05 + i) * 3;
ctx.strokeStyle = 'rgba(255,207,138,0.5)';
ctx.beginPath();
ctx.moveTo(0, yy + wobble);
for (let x = 0; x <= canvas.width; x += 20) {
ctx.lineTo(x, yy + Math.sin(x * 0.03 + game.idleFrame * 0.06 + i) * 3);
}
ctx.stroke();
}
ctx.restore();
drawRidges();
}
function drawCanyon() {
drawSky();
drawDust();
// filled rock mass either side of the flight corridor
if (game.canyon.leftWall.length > 1) {
const rockGrad = ctx.createLinearGradient(0, 0, 0, canvas.height);
rockGrad.addColorStop(0, PAL.sandDark);
rockGrad.addColorStop(1, PAL.sandShadow);
ctx.fillStyle = rockGrad;
ctx.beginPath();
ctx.moveTo(0, game.canyon.leftWall[0].y);
game.canyon.leftWall.forEach(p => ctx.lineTo(p.x, p.y));
ctx.lineTo(0, game.canyon.leftWall[game.canyon.leftWall.length - 1].y);
ctx.closePath(); ctx.fill();
ctx.beginPath();
ctx.moveTo(canvas.width, game.canyon.rightWall[0].y);
game.canyon.rightWall.forEach(p => ctx.lineTo(p.x, p.y));
ctx.lineTo(canvas.width, game.canyon.rightWall[game.canyon.rightWall.length - 1].y);
ctx.closePath(); ctx.fill();
// sedimentary strata bands
ctx.save();
ctx.globalAlpha = 0.35;
for (let i = 0; i < game.canyon.leftWall.length; i += 14) {
const lp = game.canyon.leftWall[i], rp = game.canyon.rightWall[i];
if (!lp || !rp) continue;
ctx.strokeStyle = i % 28 === 0 ? PAL.sandLight : PAL.sandMid;
ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(0, lp.y); ctx.lineTo(lp.x, lp.y); ctx.stroke();
ctx.beginPath(); ctx.moveTo(canvas.width, rp.y); ctx.lineTo(rp.x, rp.y); ctx.stroke();
}
ctx.restore();
// sun-facing rim light along corridor edges
ctx.save();
ctx.shadowColor = PAL.rim; ctx.shadowBlur = 12;
ctx.strokeStyle = PAL.rim;
ctx.lineWidth = Math.max(1.5, canvas.width / 700);
ctx.beginPath();
for (let i = 0; i < game.canyon.leftWall.length; i += 2) {
const p = game.canyon.leftWall[i];
if (i === 0) ctx.moveTo(p.x, p.y); else ctx.lineTo(p.x, p.y);
}
ctx.stroke();
ctx.beginPath();
for (let i = 0; i < game.canyon.rightWall.length; i += 2) {
const p = game.canyon.rightWall[i];
if (i === 0) ctx.moveTo(p.x, p.y); else ctx.lineTo(p.x, p.y);
}
ctx.stroke();
ctx.restore();
// rock outcrop ticks
ctx.strokeStyle = 'rgba(58, 28, 16, 0.6)';
ctx.lineWidth = 1;
for (let i = 0; i < game.canyon.leftWall.length; i += 10) {
const lp = game.canyon.leftWall[i], rp = game.canyon.rightWall[i];
if (!lp || !rp) continue;
ctx.beginPath(); ctx.moveTo(lp.x, lp.y); ctx.lineTo(lp.x + 10, lp.y + 6); ctx.stroke();
ctx.beginPath(); ctx.moveTo(rp.x, rp.y); ctx.lineTo(rp.x - 10, rp.y + 6); ctx.stroke();
}
}
// ground haze near the bottom of the screen (closer = warmer/hazier)
const hazeGrad = ctx.createLinearGradient(0, canvas.height * 0.75, 0, canvas.height);
hazeGrad.addColorStop(0, 'rgba(217,138,79,0)');
hazeGrad.addColorStop(1, 'rgba(217,138,79,0.08)');
ctx.fillStyle = hazeGrad;
ctx.fillRect(0, canvas.height * 0.75, canvas.width, canvas.height * 0.25);
}
function drawPlayer() {
const p = game.player;
const scale = Math.max(1, canvas.width / 800);
const x = Math.floor(p.x), y = Math.floor(p.y);
ctx.save();
ctx.shadowColor = PAL.rust; ctx.shadowBlur = 6;
ctx.fillStyle = '#241108';
// sand kicked up beneath the chopper
const exhaustPulse = 0.6 + Math.sin(game.frame * 0.4) * 0.15;
ctx.save();
ctx.globalAlpha = exhaustPulse * 0.45;
ctx.fillStyle = PAL.dust;
ctx.beginPath();
ctx.ellipse(x + 8 * scale, y + 15 * scale, 5 * scale, 3 * scale, 0, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
// rotor blur
const rotorPhase = Math.floor(game.frame / 2) % 2;
ctx.save();
ctx.globalAlpha = 0.3;
ctx.fillRect(x - 8 * scale, y + 2 * scale, 32 * scale, 1 * scale);
ctx.restore();
if (rotorPhase === 0) ctx.fillRect(x - 6 * scale, y + 2 * scale, 28 * scale, 2 * scale);
else ctx.fillRect(x + 6 * scale, y, 2 * scale, 6 * scale);
// dark silhouette body, rim-lit by the sun above
ctx.fillRect(x + 4 * scale, y + 6 * scale, 8 * scale, 6 * scale);
ctx.fillRect(x + 2 * scale, y + 8 * scale, 12 * scale, 4 * scale);
ctx.fillRect(x + 12 * scale, y + 9 * scale, 4 * scale, 2 * scale);
ctx.fillRect(x + 15 * scale, y + 7 * scale, 2 * scale, 6 * scale);
ctx.fillRect(x + 2 * scale, y + 12 * scale, 2 * scale, 2 * scale);
ctx.fillRect(x + 10 * scale, y + 12 * scale, 2 * scale, 2 * scale);
// amber rim light along the top edge
ctx.strokeStyle = PAL.rim; ctx.lineWidth = 1;
ctx.strokeRect(x + 2 * scale, y + 8 * scale, 12 * scale, 4 * scale);
// running light + cockpit glint
ctx.fillStyle = PAL.amberBright;
ctx.fillRect(x + 5 * scale, y + 7 * scale, 2 * scale, 1.5 * scale);
ctx.restore();
if (game.muzzleFlash > 0) {
ctx.save();
ctx.shadowColor = '#fff5e0'; ctx.shadowBlur = 16;
ctx.fillStyle = `rgba(255,230,180,${game.muzzleFlash / 4})`;
ctx.beginPath();
ctx.arc(x + p.width / 2, y - 2, 5 * scale * (game.muzzleFlash / 4), 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
if (game.playerFlash > 0) {
ctx.save();
ctx.strokeStyle = `rgba(217,80,47,${game.playerFlash / 6})`;
ctx.lineWidth = 3;
ctx.shadowColor = PAL.rust; ctx.shadowBlur = 12;
ctx.beginPath();
ctx.arc(x + p.width / 2, y + p.height / 2, 16 * scale, 0, Math.PI * 2);
ctx.stroke();
ctx.restore();
}
}
function spawnEnemy() {
const wallIndex = Math.min(60, Math.floor(game.canyon.leftWall.length * 0.3));
if (wallIndex >= game.canyon.leftWall.length) return;
const leftBound = game.canyon.leftWall[wallIndex].x + 15;
const rightBound = game.canyon.rightWall[wallIndex].x - 30;
const types = ['drone', 'uav', 'mine'];
const type = types[Math.floor(Math.random() * types.length)];
game.enemies.push({
x: leftBound + Math.random() * (rightBound - leftBound), y: -20,
vx: (Math.random() - 0.5) * 1.5, vy: type === 'mine' ? 0.5 : 1.5,
width: 16, height: 16, type: type,
health: type === 'mine' ? 1 : (type === 'uav' ? 3 : 2),
shootTimer: 60 + Math.random() * 60, flash: 0
});
}
function spawnPowerup(type) {
const wallIndex = Math.min(60, Math.floor(game.canyon.leftWall.length * 0.3));
if (wallIndex >= game.canyon.leftWall.length) return;
const leftBound = game.canyon.leftWall[wallIndex].x + 15;
const rightBound = game.canyon.rightWall[wallIndex].x - 30;
game.powerups.push({ x: leftBound + Math.random() * (rightBound - leftBound), y: -20, width: 12, height: 12, type: type, vy: 1.5, rotation: 0 });
}
function spawnForceField() {
const wallIndex = Math.min(60, Math.floor(game.canyon.leftWall.length * 0.3));
if (wallIndex >= game.canyon.leftWall.length) return;
const leftBound = game.canyon.leftWall[wallIndex].x + 15;
const rightBound = game.canyon.rightWall[wallIndex].x - 50;
game.forceFields.push({ x: leftBound + Math.random() * (rightBound - leftBound), y: -30, width: Math.min(40, canvas.width * 0.1), height: 80, health: 6, vy: 1, pulse: 0, flash: 0 });
}
function createExplosion(x, y, rgb = { r: 255, g: 140, b: 40 }, count = 12) {
for (let i = 0; i < count; i++) {
const angle = (i * Math.PI * 2) / count;
const spd = 2 + Math.random() * 2.5;
game.particles.push({
x: x, y: y, vx: Math.cos(angle) * spd, vy: Math.sin(angle) * spd,
life: 28, maxLife: 28, size: 2 + Math.random() * 2.5,
r: rgb.r, g: rgb.g, b: rgb.b
});
}
game.shockwaves.push({ x, y, radius: 2, maxRadius: count > 15 ? 65 : 38, life: 20 });
}
function triggerShake(intensity) { game.shake.intensity = Math.max(game.shake.intensity, intensity); }
function update() {
if (game.state !== 'playing') return;
game.frame++;
if (game.frame % 60 === 0) game.time++;
updateCanyon();
updateDust();
if (game.shake.intensity > 0.1) game.shake.intensity *= 0.88; else game.shake.intensity = 0;
if (game.muzzleFlash > 0) game.muzzleFlash--;
if (game.playerFlash > 0) game.playerFlash--;
AudioEngine.setEngine(game.scrollSpeed / 4.5);
const p = game.player;
if (game.keys[','] || game.keys['<'] || game.keys['arrowleft']) p.x -= p.speed;
if (game.keys['.'] || game.keys['>'] || game.keys['arrowright']) p.x += p.speed;
if (game.frame % 2 === 0) {
game.particles.push({
x: p.x + p.width / 2 + (Math.random() - 0.5) * 6, y: p.y + p.height,
vx: (Math.random() - 0.5) * 0.5, vy: 1 + Math.random(),
life: 16, maxLife: 16, size: 1.5 + Math.random(), r: 201, g: 166, b: 122
});
}
let playerCenterY = 0, minDist = Infinity;
for (let i = 0; i < game.canyon.leftWall.length; i++) {
const dist = Math.abs(game.canyon.leftWall[i].y - game.player.y);
if (dist < minDist) { minDist = dist; playerCenterY = i; }
}
if (playerCenterY < game.canyon.leftWall.length) {
const leftBound = game.canyon.leftWall[playerCenterY].x + 10;
const rightBound = game.canyon.rightWall[playerCenterY].x - p.width - 10;
p.x = Math.max(leftBound, Math.min(rightBound, p.x));
if (p.x <= leftBound + 2 || p.x >= rightBound - 2) {
game.shields -= 0.5;
if (game.frame % 8 === 0) {
AudioEngine.scrape();
const sparkX = p.x <= leftBound + 2 ? p.x : p.x + p.width;
game.particles.push({ x: sparkX, y: p.y + p.height / 2, vx: (Math.random() - 0.5) * 2, vy: (Math.random() - 0.5) * 2, life: 10, maxLife: 10, size: 2, r: 255, g: 210, b: 130 });
}
if (game.shields <= 0) loseLife();
}
}
if (game.keys['z'] && game.frame % 6 === 0) {
game.bullets.push({ x: p.x + p.width / 2 - 1, y: p.y, vy: -8, width: 2, height: 8 });
game.muzzleFlash = 4;
AudioEngine.laser();
}
if (game.keys['x'] && !game.keys['x_fired'] && game.missiles > 0) {
game.missiles--;
let target = game.enemies[0];
game.playerMissiles.push({ x: p.x + p.width / 2 - 2, y: p.y, vx: 0, vy: -6, target: target, width: 4, height: 12 });
game.keys['x_fired'] = true;
AudioEngine.missile();
triggerShake(3);
}
if (!game.keys['x']) game.keys['x_fired'] = false;
game.bullets = game.bullets.filter(b => { b.y += b.vy; return b.y > -10; });
game.playerMissiles = game.playerMissiles.filter(m => {
if (m.target && game.enemies.includes(m.target)) {
const dx = m.target.x - m.x, dy = m.target.y - m.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist > 0) { m.vx += (dx / dist) * 0.5; m.vy += (dy / dist) * 0.5; }
}
m.x += m.vx; m.y += m.vy;
const speed = Math.sqrt(m.vx * m.vx + m.vy * m.vy);
if (speed > 8) { m.vx = (m.vx / speed) * 8; m.vy = (m.vy / speed) * 8; }
return m.y > -10 && m.y < canvas.height;
});
game.enemySpawnTimer++;
if (game.enemySpawnTimer > 80 && game.enemies.length < 6) {
spawnEnemy(); game.enemySpawnTimer = 0;
if (Math.random() < 0.15) spawnForceField();
}
game.powerupSpawnTimer++;
if (game.powerupSpawnTimer > 240) {
const rand = Math.random();
if (rand < 0.4) spawnPowerup('fuel'); else if (rand < 0.7) spawnPowerup('shield'); else spawnPowerup('missile');
game.powerupSpawnTimer = 0;
}
game.enemies = game.enemies.filter(e => {
if (e.flash > 0) e.flash--;
e.x += e.vx; e.y += e.vy;
for (let i = game.bullets.length - 1; i >= 0; i--) {
const b = game.bullets[i];
if (b.x >= e.x && b.x <= e.x + e.width && b.y >= e.y && b.y <= e.y + e.height) {
e.health--; e.flash = 4;
game.bullets.splice(i, 1);
AudioEngine.hit();
if (e.health <= 0) {
createExplosion(e.x + e.width / 2, e.y + e.height / 2, { r: 255, g: 140, b: 40 }, e.type === 'uav' ? 18 : 12);
AudioEngine.explosion(e.type === 'uav');
triggerShake(e.type === 'uav' ? 6 : 4);
game.score += e.type === 'uav' ? 300 : (e.type === 'mine' ? 100 : 200);
if (game.score > game.hiscore) game.hiscore = game.score;
return false;
}
break;
}
}
for (let i = game.playerMissiles.length - 1; i >= 0; i--) {
const m = game.playerMissiles[i];
if (m.x >= e.x && m.x <= e.x + e.width && m.y >= e.y && m.y <= e.y + e.height) {
createExplosion(e.x + e.width / 2, e.y + e.height / 2, { r: 255, g: 140, b: 40 }, e.type === 'uav' ? 18 : 12);
AudioEngine.explosion(e.type === 'uav');
triggerShake(e.type === 'uav' ? 7 : 5);
game.playerMissiles.splice(i, 1);
game.score += e.type === 'uav' ? 300 : (e.type === 'mine' ? 100 : 200);
if (game.score > game.hiscore) game.hiscore = game.score;
return false;
}
}
if (e.x < p.x + p.width && e.x + e.width > p.x && e.y < p.y + p.height && e.y + e.height > p.y) {
game.shields -= 25; game.playerFlash = 6;
createExplosion(e.x + e.width / 2, e.y + e.height / 2, { r: 217, g: 80, b: 47 }, 14);
AudioEngine.explosion(false); triggerShake(10);
if (game.shields <= 0) loseLife();
return false;
}
return e.y < canvas.height + 20;
});
game.powerups = game.powerups.filter(pw => {
pw.y += pw.vy; pw.rotation += 0.1;
if (pw.x < p.x + p.width && pw.x + pw.width > p.x && pw.y < p.y + p.height && pw.y + pw.height > p.y) {
if (pw.type === 'fuel') game.fuel = Math.min(100, game.fuel + 30);
else if (pw.type === 'shield') game.shields = Math.min(100, game.shields + 40);
else if (pw.type === 'missile') game.missiles = Math.min(99, game.missiles + 5);
AudioEngine.powerup();
createExplosion(pw.x + pw.width / 2, pw.y + pw.height / 2, { r: 255, g: 195, b: 107 }, 8);
return false;
}
return pw.y < canvas.height;
});
game.forceFields.forEach(f => { f.y += f.vy; f.pulse = (f.pulse + 0.1) % (Math.PI * 2); if (f.flash > 0) f.flash--; });
game.forceFields = game.forceFields.filter(f => {
for (let i = game.bullets.length - 1; i >= 0; i--) {
const b = game.bullets[i];
if (b.x >= f.x && b.x <= f.x + f.width && b.y >= f.y && b.y <= f.y + f.height) {
f.health--; f.flash = 4;
game.bullets.splice(i, 1);
AudioEngine.hit();
if (f.health <= 0) {
createExplosion(f.x + f.width / 2, f.y + f.height / 2, { r: 255, g: 140, b: 40 }, 22);
AudioEngine.explosion(true); triggerShake(8);
game.score += 400;
if (game.score > game.hiscore) game.hiscore = game.score;
return false;
}
break;
}
}
for (let i = game.playerMissiles.length - 1; i >= 0; i--) {
const m = game.playerMissiles[i];
if (m.x >= f.x && m.x <= f.x + f.width && m.y >= f.y && m.y <= f.y + f.height) {
f.health -= 2; f.flash = 4;
game.playerMissiles.splice(i, 1);
createExplosion(f.x + f.width / 2, f.y + f.height / 2, { r: 255, g: 140, b: 40 }, 10);
AudioEngine.hit();
if (f.health <= 0) {
AudioEngine.explosion(true); triggerShake(8);
game.score += 400;
if (game.score > game.hiscore) game.hiscore = game.score;
return false;
}
break;
}
}
if (f.x < p.x + p.width && f.x + f.width > p.x && f.y < p.y + p.height && f.y + f.height > p.y) {
game.shields -= 30; game.playerFlash = 6;
createExplosion(p.x + p.width / 2, p.y + p.height / 2, { r: 217, g: 80, b: 47 }, 16);
AudioEngine.explosion(false); triggerShake(12);
if (game.shields <= 0) loseLife();
return false;
}
return f.y < canvas.height;
});
game.particles = game.particles.filter(pt => {
pt.x += pt.vx; pt.y += pt.vy; pt.life--; pt.size *= 0.96;
return pt.life > 0 && pt.size > 0.4;
});
game.shockwaves = game.shockwaves.filter(s => { s.radius += (s.maxRadius - s.radius) * 0.25; s.life--; return s.life > 0; });
game.fuel -= 0.03;
if (game.fuel <= 0) loseLife();
if ((game.fuel < 25 || game.shields < 25) && game.frame % 40 === 0) AudioEngine.alarmBeep();
if (game.frame % 900 === 0) game.scrollSpeed = Math.min(4.5, game.scrollSpeed + 0.2);
updateHUD();
}
function drawEnemies() {
game.enemies.forEach(e => {
const x = Math.floor(e.x), y = Math.floor(e.y);
const flashing = e.flash > 0;
ctx.save();
ctx.shadowColor = flashing ? '#fff5e0' : PAL.rust;
ctx.shadowBlur = flashing ? 14 : 6;
ctx.strokeStyle = flashing ? '#fff5e0' : PAL.rust;
ctx.lineWidth = 1;
ctx.fillStyle = flashing ? 'rgba(255,245,224,0.5)' : 'rgba(30,15,8,0.85)';
if (e.type === 'drone') {
ctx.fillRect(x, y, e.width, e.height);
ctx.strokeRect(x, y, e.width, e.height);
ctx.beginPath();
ctx.moveTo(x, y); ctx.lineTo(x + e.width, y + e.height);
ctx.moveTo(x + e.width, y); ctx.lineTo(x, y + e.height);
ctx.stroke();
} else if (e.type === 'uav') {
ctx.beginPath();
for (let i = 0; i < 6; i++) {
const angle = (i * Math.PI) / 3;
const px = x + e.width / 2 + Math.cos(angle) * e.width / 2;
const py = y + e.height / 2 + Math.sin(angle) * e.height / 2;
if (i === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.closePath(); ctx.fill(); ctx.stroke();
} else {
ctx.beginPath();
ctx.arc(x + e.width / 2, y + e.height / 2, e.width / 2, 0, Math.PI * 2);
ctx.fill(); ctx.stroke();
}
// glowing targeting eye
ctx.fillStyle = flashing ? '#ffffff' : PAL.amberBright;
ctx.beginPath();
ctx.arc(x + e.width / 2, y + e.height / 2, 2, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
const healthRatio = e.health / (e.type === 'uav' ? 3 : (e.type === 'mine' ? 1 : 2));
ctx.fillStyle = healthRatio > 0.5 ? PAL.amberBright : healthRatio > 0.2 ? '#ffcf3a' : PAL.rust;
ctx.fillRect(x, y - 4, e.width * healthRatio, 2);
});
}
function drawPowerups() {
game.powerups.forEach(pw => {
const x = Math.floor(pw.x), y = Math.floor(pw.y);
ctx.save();
ctx.translate(x + pw.width / 2, y + pw.height / 2);
ctx.rotate(pw.rotation);
ctx.shadowColor = PAL.amberBright; ctx.shadowBlur = 8;
ctx.strokeStyle = PAL.amberBright;
ctx.lineWidth = 1;
ctx.fillStyle = 'rgba(240, 164, 76, 0.25)';
ctx.beginPath();
ctx.moveTo(0, -pw.height / 2); ctx.lineTo(pw.width / 2, 0);
ctx.lineTo(0, pw.height / 2); ctx.lineTo(-pw.width / 2, 0);
ctx.closePath(); ctx.fill(); ctx.stroke();
ctx.shadowBlur = 0;
ctx.fillStyle = PAL.amberBright;
ctx.font = '8px "Share Tech Mono", monospace';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(pw.type === 'fuel' ? 'F' : pw.type === 'shield' ? 'S' : 'M', 0, 0);
ctx.restore();
});
}
function drawGlitchTitle(text, x, y, size) {
ctx.save();
ctx.font = `bold ${size}px 'Oswald', sans-serif`;
ctx.textAlign = 'center';
ctx.globalCompositeOperation = 'lighter';
ctx.fillStyle = 'rgba(217,80,47,0.5)';
ctx.fillText(text, x - 2, y);
ctx.fillStyle = 'rgba(255,233,184,0.5)';
ctx.fillText(text, x + 2, y);
ctx.globalCompositeOperation = 'source-over';
ctx.shadowColor = PAL.sunGlow; ctx.shadowBlur = 18;
ctx.fillStyle = PAL.amberBright;
ctx.fillText(text, x, y);
ctx.shadowBlur = 0;
ctx.restore();
}
function draw() {
if (canvas.width <= 0 || canvas.height <= 0) { resizeCanvas(); return; }
game.idleFrame++;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.fillStyle = '#000';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.save();
const sh = game.shake.intensity;
if (sh > 0.1) ctx.translate((Math.random() - 0.5) * sh, (Math.random() - 0.5) * sh);
drawCanyon();
// clip everything below to the flight corridor so nothing is
// visible beyond the canyon walls (occluded by the rock)
ctx.save();
if (game.canyon.leftWall.length > 1) {
ctx.beginPath();
ctx.moveTo(game.canyon.leftWall[0].x, game.canyon.leftWall[0].y);
for (let i = 1; i < game.canyon.leftWall.length; i++) {
ctx.lineTo(game.canyon.leftWall[i].x, game.canyon.leftWall[i].y);
}
for (let i = game.canyon.rightWall.length - 1; i >= 0; i--) {
ctx.lineTo(game.canyon.rightWall[i].x, game.canyon.rightWall[i].y);
}
ctx.closePath();
ctx.clip();
}
game.forceFields.forEach(f => {
const x = Math.floor(f.x), y = Math.floor(f.y);
ctx.strokeStyle = f.flash > 0 ? '#fff5e0' : PAL.amber;
ctx.lineWidth = 2;
ctx.globalAlpha = 0.3 + Math.sin(f.pulse) * 0.2;
for (let i = 0; i < f.width; i += 8) {
const offset = Math.sin(f.pulse + i * 0.2) * 4;
ctx.beginPath(); ctx.moveTo(x + i, y); ctx.lineTo(x + i + offset, y + f.height); ctx.stroke();
}
ctx.globalAlpha = 1;
ctx.strokeRect(x, y, f.width, f.height);
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 6; j++) {
const sparkX = x + (i + 1) * f.width / 4;
const sparkY = y + (j + 1) * f.height / 7;
const pulse = Math.sin(f.pulse + i + j) * 2;
ctx.fillStyle = PAL.amberBright;
ctx.fillRect(sparkX + pulse, sparkY + pulse, 2, 2);
}
}
});
drawPowerups();
drawEnemies();
game.bullets.forEach(b => {
ctx.save();
ctx.shadowColor = PAL.amberBright; ctx.shadowBlur = 8;
ctx.fillStyle = '#fff5e0';
ctx.fillRect(Math.floor(b.x), Math.floor(b.y), b.width, b.height);
ctx.restore();
});
game.playerMissiles.forEach(m => {
ctx.save();
ctx.shadowColor = PAL.rust; ctx.shadowBlur = 10;
ctx.fillStyle = PAL.amberBright;
ctx.fillRect(Math.floor(m.x), Math.floor(m.y), m.width, m.height);
ctx.restore();
for (let i = 0; i < 3; i++) {
ctx.fillStyle = `rgba(217, 80, 47, ${0.5 - i * 0.15})`;
ctx.fillRect(Math.floor(m.x), Math.floor(m.y) + m.height + i * 3, m.width, 2);
}
});
game.particles.forEach(pt => {
ctx.fillStyle = `rgba(${pt.r},${pt.g},${pt.b},${(pt.life / pt.maxLife).toFixed(3)})`;
ctx.fillRect(Math.floor(pt.x), Math.floor(pt.y), pt.size, pt.size);
});
game.shockwaves.forEach(s => {
ctx.strokeStyle = `rgba(255,180,84,${((s.life / 20) * 0.6).toFixed(3)})`;
ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(s.x, s.y, s.radius, 0, Math.PI * 2); ctx.stroke();
});
drawPlayer();
ctx.restore(); // end corridor clip
ctx.restore(); // end shake transform
if (game.state === 'start') {
ctx.fillStyle = 'rgba(20, 11, 8, 0.94)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
drawGlitchTitle('APACHE//CAN.YON', canvas.width / 2, canvas.height / 2 - 60, Math.min(48, canvas.width / 15));
ctx.fillStyle = PAL.amber;
ctx.textAlign = 'center';
ctx.font = `${Math.min(24, canvas.width / 25)}px 'Share Tech Mono', monospace`;
ctx.fillText('DUSK OPS — SYSTEM BOOT...', canvas.width / 2, canvas.height / 2 - 10);
ctx.fillText('VER 5.0GB', canvas.width / 2, canvas.height / 2 + 20);
ctx.font = `${Math.min(16, canvas.width / 35)}px 'Share Tech Mono', monospace`;
ctx.fillText('INITIATE SEQUENCE [SPACE]', canvas.width / 2, canvas.height / 2 + 60);
const loadWidth = Math.min(300, canvas.width * 0.7);
const loadProgress = (game.idleFrame % 120) / 120;
ctx.strokeStyle = PAL.amber;
ctx.strokeRect(canvas.width / 2 - loadWidth / 2, canvas.height / 2 + 90, loadWidth, 4);
ctx.shadowColor = PAL.amberBright; ctx.shadowBlur = 8;
ctx.fillStyle = PAL.amberBright;
ctx.fillRect(canvas.width / 2 - loadWidth / 2, canvas.height / 2 + 90, loadWidth * loadProgress, 4);
ctx.shadowBlur = 0;
}
}
function updateHUD() {
document.getElementById('score').textContent = game.score.toString().padStart(6, '0');
document.getElementById('hiscore').textContent = game.hiscore.toString().padStart(6, '0');
document.getElementById('lives').textContent = game.lives.toString().padStart(2, '0');
document.getElementById('missiles').textContent = game.missiles.toString().padStart(2, '0');
document.getElementById('time').textContent = game.time.toString().padStart(3, '0');
document.getElementById('speed').textContent = game.scrollSpeed.toFixed(1);
const fuelBar = document.getElementById('fuelBar');
const shieldsBar = document.getElementById('shieldsBar');
const fuelValue = document.getElementById('fuelValue');
const shieldsValue = document.getElementById('shieldsValue');
fuelBar.style.width = game.fuel + '%';
shieldsBar.style.width = game.shields + '%';
fuelValue.textContent = Math.floor(game.fuel) + '%';
shieldsValue.textContent = Math.floor(game.shields) + '%';
if (game.fuel < 30) { fuelBar.style.background = 'linear-gradient(to right, #6b2f1a, #d9502f)'; fuelValue.style.color = '#ffb454'; }
else { fuelBar.style.background = 'linear-gradient(to right, #d9502f, #f0a44c)'; fuelValue.style.color = '#ffc36b'; }
if (game.shields < 30) { shieldsBar.style.background = 'linear-gradient(to right, #6b2f1a, #d9502f)'; shieldsValue.style.color = '#ffb454'; }
else { shieldsBar.style.background = 'linear-gradient(to right, #d9502f, #f0a44c)'; shieldsValue.style.color = '#ffc36b'; }
const gameContainer = document.getElementById('gameContainer');
if (game.state === 'playing' && (game.fuel < 25 || game.shields < 25)) gameContainer.classList.add('critical');
else gameContainer.classList.remove('critical');
}
function loseLife() {
game.lives--;
triggerShake(15);
AudioEngine.explosion(true);
if (game.lives <= 0) {
gameOver();
} else {
game.shields = 100; game.fuel = 100;
game.player.x = canvas.width / 2 - game.player.width / 2;
game.enemies = []; game.bullets = []; game.playerMissiles = [];
}
}
function gameOver() {
game.state = 'gameover';
AudioEngine.gameover();
AudioEngine.stopEngine();
const msg = document.getElementById('message');
msg.innerHTML = `<div class="title-screen">
<div style="font-size: clamp(20px, 32px, 4vh); margin-bottom: 20px;">SYSTEM FAILURE</div>
<div>MISSION TERMINATED</div>
<div style="margin: 20px 0; font-size: clamp(16px, 24px, 3vh);">SCORE: ${game.score.toString().padStart(6, '0')}</div>
<div>TIME: ${game.time.toString().padStart(3, '0')} SECONDS</div>
<div style="margin-top: 30px; font-size: clamp(12px, 16px, 2vh);">[SPACE] TO REBOOT</div>
</div>`;
msg.style.display = 'block';
}
function restart() {
game.score = 0; game.fuel = 100; game.shields = 100; game.missiles = 10; game.lives = 3;
game.scrollSpeed = 2.5; game.time = 0; game.frame = 0;
game.bullets = []; game.playerMissiles = []; game.enemies = []; game.powerups = [];
game.forceFields = []; game.particles = []; game.shockwaves = [];
game.enemySpawnTimer = 0; game.powerupSpawnTimer = 0;
game.shake.intensity = 0; game.muzzleFlash = 0; game.playerFlash = 0;
game.player.x = canvas.width / 2 - game.player.width / 2;
game.player.y = canvas.height * 0.7;
initCanyon();
document.getElementById('gameContainer').classList.remove('critical');
document.getElementById('message').style.display = 'none';
game.state = 'playing';
}
function gameLoop() { update(); draw(); requestAnimationFrame(gameLoop); }
document.addEventListener('keydown', e => {
game.keys[e.key.toLowerCase()] = true;
if (e.key.toLowerCase() === 'm') {
const muted = AudioEngine.toggleMute();
document.getElementById('muteStatus').textContent = `[M]: MUTE (${muted ? 'ON' : 'OFF'})`;
}
if (e.key === ' ') {
e.preventDefault();
AudioEngine.init();
AudioEngine.resume();
if (game.state === 'start' || game.state === 'gameover') {
AudioEngine.start();
restart();
} else if (game.state === 'playing') {
game.state = 'paused';
AudioEngine.stopEngine();
document.getElementById('message').innerHTML = `<div class="title-screen">
<div style="font-size: clamp(20px, 32px, 4vh); margin-bottom: 20px;">SYSTEM PAUSED</div>
<div>[SPACE] TO RESUME</div>
</div>`;
document.getElementById('message').style.display = 'block';
} else if (game.state === 'paused') {
game.state = 'playing';
document.getElementById('message').style.display = 'none';
}
}
if (e.key === 'Escape' && game.state === 'playing') {
game.state = 'paused';
AudioEngine.stopEngine();
document.getElementById('message').innerHTML = `<div class="title-screen">
<div style="font-size: clamp(20px, 32px, 4vh); margin-bottom: 20px;">SYSTEM PAUSED</div>
<div>[SPACE] TO RESUME</div>
</div>`;
document.getElementById('message').style.display = 'block';
}
});
document.addEventListener('keyup', e => { game.keys[e.key.toLowerCase()] = false; });
window.addEventListener('load', () => {
setTimeout(() => {
game.player.x = canvas.width / 2 - game.player.width / 2;
game.player.y = canvas.height * 0.7;
initCanyon();
initDust();
initRidges();
updateHUD();
gameLoop();
}, 500);
});
}
document.addEventListener('contextmenu', e => e.preventDefault());
</script>
</body>
</html>