From 87f2506fabe108c23bae4ed533dfd5e822590ca3 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 3 Jul 2026 11:45:23 +0000 Subject: [PATCH] Add Space Invaders Nouveau: a full PWA recreation with Art Nouveau styling Implements the complete spec: requestAnimationFrame game loop with a finite-state machine (attract/play/game-over), hive-mind alien swarm with hardware-accurate speed-up, deterministic Rolling/Plunger/Squiggly alien firing patterns, hidden shot-count-based UFO scoring table, and pixel-level destructible bunkers via off-screen canvas + ImageData. Reskins the classic in a gilded, jewel-toned Art Nouveau visual language (vector-drawn stained-glass aliens, whiplash-curve backdrops, ornamental frame) with all sound effects procedurally synthesized via the Web Audio API rather than sampled. Includes mobile touch controls gated on pointer:coarse, a web app manifest, and a cache-first service worker for offline play. --- space-invaders/README.md | 29 +++ space-invaders/css/style.css | 157 ++++++++++++++ space-invaders/icons/icon-192.png | Bin 0 -> 27985 bytes space-invaders/icons/icon-512.png | Bin 0 -> 36405 bytes space-invaders/icons/icon-maskable-512.png | Bin 0 -> 33151 bytes space-invaders/icons/icon.svg | 26 +++ space-invaders/index.html | 46 ++++ space-invaders/js/audio.js | 139 ++++++++++++ space-invaders/js/collision.js | 104 +++++++++ space-invaders/js/constants.js | 89 ++++++++ space-invaders/js/entities/alien.js | 16 ++ .../js/entities/alienFireController.js | 49 +++++ space-invaders/js/entities/bullet.js | 56 +++++ space-invaders/js/entities/bunker.js | 94 ++++++++ space-invaders/js/entities/explosion.js | 21 ++ space-invaders/js/entities/mysteryShip.js | 78 +++++++ space-invaders/js/entities/player.js | 90 ++++++++ space-invaders/js/entities/swarm.js | 127 +++++++++++ space-invaders/js/game.js | 165 ++++++++++++++ space-invaders/js/input.js | 66 ++++++ space-invaders/js/render.js | 203 ++++++++++++++++++ space-invaders/js/state/attractState.js | 85 ++++++++ space-invaders/js/state/gameOverState.js | 51 +++++ space-invaders/js/state/playState.js | 128 +++++++++++ space-invaders/js/state/stateMachine.js | 24 +++ space-invaders/manifest.webmanifest | 17 ++ space-invaders/sw.js | 55 +++++ 27 files changed, 1915 insertions(+) create mode 100644 space-invaders/README.md create mode 100644 space-invaders/css/style.css create mode 100644 space-invaders/icons/icon-192.png create mode 100644 space-invaders/icons/icon-512.png create mode 100644 space-invaders/icons/icon-maskable-512.png create mode 100644 space-invaders/icons/icon.svg create mode 100644 space-invaders/index.html create mode 100644 space-invaders/js/audio.js create mode 100644 space-invaders/js/collision.js create mode 100644 space-invaders/js/constants.js create mode 100644 space-invaders/js/entities/alien.js create mode 100644 space-invaders/js/entities/alienFireController.js create mode 100644 space-invaders/js/entities/bullet.js create mode 100644 space-invaders/js/entities/bunker.js create mode 100644 space-invaders/js/entities/explosion.js create mode 100644 space-invaders/js/entities/mysteryShip.js create mode 100644 space-invaders/js/entities/player.js create mode 100644 space-invaders/js/entities/swarm.js create mode 100644 space-invaders/js/game.js create mode 100644 space-invaders/js/input.js create mode 100644 space-invaders/js/render.js create mode 100644 space-invaders/js/state/attractState.js create mode 100644 space-invaders/js/state/gameOverState.js create mode 100644 space-invaders/js/state/playState.js create mode 100644 space-invaders/js/state/stateMachine.js create mode 100644 space-invaders/manifest.webmanifest create mode 100644 space-invaders/sw.js diff --git a/space-invaders/README.md b/space-invaders/README.md new file mode 100644 index 000000000..d323ebc1e --- /dev/null +++ b/space-invaders/README.md @@ -0,0 +1,29 @@ +# Space Invaders — Nouveau + +A mobile-responsive Progressive Web App recreation of the 1978 arcade classic, +built with vanilla HTML5 Canvas, CSS, and JavaScript (ES modules) — no build +step, no dependencies. + +Faithful to the original's mechanics: the hive-mind alien swarm with its +hardware-accurate speed-up curve, deterministic Rolling/Plunger/Squiggly +firing patterns, the hidden shot-count-based mystery ship scoring table, +pixel-level destructible bunkers, and the one-bullet-at-a-time cannon — all +reskinned in a gilded, jewel-toned Art Nouveau visual style with procedurally +synthesized (not sampled) sound effects. + +## Running locally + +Any static file server works, e.g.: + +```bash +cd space-invaders +python3 -m http.server 8080 +``` + +Then open `http://localhost:8080`. Installable as a PWA (offline-capable via +service worker) from a supporting browser. + +## Controls + +- **Move**: Arrow keys / A-D, or the on-screen D-pad on touch devices. +- **Fire**: Space / Up / W, or the on-screen fire button. diff --git a/space-invaders/css/style.css b/space-invaders/css/style.css new file mode 100644 index 000000000..76540ac9b --- /dev/null +++ b/space-invaders/css/style.css @@ -0,0 +1,157 @@ +:root { + --gold: #d4af37; + --gold-bright: #f3d47a; + --gold-dim: #8a6a2a; + --ink: #120a24; + --ink-deep: #0a0616; + --plum: #1d1030; + --parchment: #e8d9a8; + --ruby: #c0384f; +} + +* { + box-sizing: border-box; +} + +html, body { + height: 100%; + margin: 0; + background: radial-gradient(ellipse at center, var(--plum) 0%, var(--ink-deep) 75%); + color: var(--parchment); + font-family: Georgia, 'Palatino Linotype', 'Book Antiqua', serif; + overflow: hidden; + -webkit-tap-highlight-color: transparent; + touch-action: none; +} + +#stage { + height: 100%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.6rem; + padding: 0.6rem env(safe-area-inset-right) 0.6rem env(safe-area-inset-left); +} + +#hud { + width: min(92vw, 460px); + display: flex; + justify-content: space-between; + align-items: baseline; + letter-spacing: 0.12em; + text-transform: uppercase; + padding: 0 0.4rem; +} + +.hud-block { + display: flex; + flex-direction: column; + align-items: center; + min-width: 3.5rem; +} + +.hud-label { + font-size: 0.55rem; + color: var(--gold-dim); + letter-spacing: 0.2em; +} + +.hud-value { + font-size: 1.1rem; + color: var(--gold-bright); + text-shadow: 0 0 6px rgba(212, 175, 55, 0.55); + font-variant-numeric: tabular-nums; +} + +#canvas-frame { + position: relative; + padding: 14px; + border-radius: 18px 18px 8px 8px; + background: + linear-gradient(180deg, rgba(212, 175, 55, 0.08), transparent 40%), + var(--ink); + border: 2px solid var(--gold); + box-shadow: + 0 0 0 1px rgba(212, 175, 55, 0.25), + 0 0 24px rgba(212, 175, 55, 0.18), + inset 0 0 30px rgba(0, 0, 0, 0.6); + max-height: 82vh; + display: flex; +} + +#canvas-frame::before { + content: ''; + position: absolute; + inset: 4px; + pointer-events: none; + border: 1px solid rgba(212, 175, 55, 0.35); + border-radius: 14px 14px 6px 6px; +} + +#game-canvas { + display: block; + width: auto; + height: auto; + max-width: 88vw; + max-height: 78vh; + aspect-ratio: 224 / 256; + background: var(--ink-deep); + border-radius: 10px 10px 4px 4px; +} + +#touch-controls { + display: none; + width: min(92vw, 460px); + justify-content: space-between; + align-items: center; + padding: 0 0.5rem; +} + +@media (pointer: coarse) { + #touch-controls { + display: flex; + } + #stage { + justify-content: space-between; + } + #game-canvas { + max-height: 62vh; + } +} + +.dpad { + display: flex; + gap: 0.9rem; +} + +.ctrl { + font-family: inherit; + width: 3.4rem; + height: 3.4rem; + border-radius: 50%; + border: 2px solid var(--gold); + background: radial-gradient(circle at 35% 30%, rgba(212, 175, 55, 0.35), rgba(18, 10, 36, 0.9)); + color: var(--gold-bright); + font-size: 1.3rem; + display: flex; + align-items: center; + justify-content: center; + box-shadow: 0 0 12px rgba(212, 175, 55, 0.3); + user-select: none; +} + +.ctrl:active { + background: radial-gradient(circle at 35% 30%, rgba(243, 212, 122, 0.5), rgba(18, 10, 36, 0.9)); +} + +.ctrl--fire { + width: 4rem; + height: 4rem; + font-size: 1.6rem; +} + +@media (max-height: 480px) { + .hud-value { font-size: 0.9rem; } + .hud-label { font-size: 0.5rem; } +} diff --git a/space-invaders/icons/icon-192.png b/space-invaders/icons/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..cfc342f739236a087cae72c64c59011f61fc41c6 GIT binary patch literal 27985 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 Y0P<=Jcw%SUaR2}S07*qoM6N<$f}cGi!vFvP literal 0 HcmV?d00001 diff --git a/space-invaders/icons/icon-512.png b/space-invaders/icons/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..c9d440cf9f0cbe5377053ec07633a85607e16299 GIT binary patch literal 36405 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 E02&Q3t^fc4 literal 0 HcmV?d00001 diff --git a/space-invaders/icons/icon-maskable-512.png b/space-invaders/icons/icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..ae75c75efb22c2bbbd8bf95005e12d12a84bce40 GIT binary patch literal 33151 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 literal 0 HcmV?d00001 diff --git a/space-invaders/icons/icon.svg b/space-invaders/icons/icon.svg new file mode 100644 index 000000000..bfcec82c1 --- /dev/null +++ b/space-invaders/icons/icon.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/space-invaders/index.html b/space-invaders/index.html new file mode 100644 index 000000000..ecb14acad --- /dev/null +++ b/space-invaders/index.html @@ -0,0 +1,46 @@ + + + + + + Space Invaders — Nouveau + + + + + + + + +

+
+
+ Score + 0000 +
+
+ High Score + 0000 +
+
+ Lives + 3 +
+
+ +
+ +
+ + +
+ + + + diff --git a/space-invaders/js/audio.js b/space-invaders/js/audio.js new file mode 100644 index 000000000..609101009 --- /dev/null +++ b/space-invaders/js/audio.js @@ -0,0 +1,139 @@ +// AudioManager — all sound effects are synthesized procedurally at load time +// (no external .wav assets), then played back as low-latency AudioBufferSourceNodes. +export class AudioManager { + constructor() { + this.ctx = null; + this.buffers = new Map(); + this.masterGain = null; + this.ufoSource = null; + this.ufoGain = null; + this.enabled = true; + } + + // AudioContext must be created/resumed from a user gesture on most browsers. + unlock() { + if (this.ctx) { + if (this.ctx.state === 'suspended') this.ctx.resume(); + return; + } + const Ctx = window.AudioContext || window.webkitAudioContext; + this.ctx = new Ctx(); + this.masterGain = this.ctx.createGain(); + this.masterGain.gain.value = 0.55; + this.masterGain.connect(this.ctx.destination); + this._generateAll(); + } + + _makeBuffer(duration, fn) { + const sr = this.ctx.sampleRate; + const length = Math.max(1, Math.floor(sr * duration)); + const buffer = this.ctx.createBuffer(1, length, sr); + const data = buffer.getChannelData(0); + for (let i = 0; i < length; i++) { + const t = i / sr; + data[i] = fn(t, i, sr); + } + return buffer; + } + + _generateAll() { + const noise = (t, decay) => (Math.random() * 2 - 1) * Math.exp(-t * decay); + + // Player shot: quick descending square-ish sweep, bright and thin. + this.buffers.set('shoot', this._makeBuffer(0.16, (t) => { + const freq = 900 - t * 3200; + const env = Math.exp(-t * 14); + return Math.sign(Math.sin(2 * Math.PI * freq * t)) * 0.35 * env; + })); + + // Alien hit: a short "pop" — filtered noise burst with a falling thud. + this.buffers.set('alienHit', this._makeBuffer(0.14, (t) => { + const thud = Math.sin(2 * Math.PI * (140 - t * 400) * t) * 0.5; + return (noise(t, 24) * 0.6 + thud) * Math.exp(-t * 18); + })); + + // Player explosion: bigger, longer, low rumbling noise burst. + this.buffers.set('playerExplosion', this._makeBuffer(0.5, (t) => { + const rumble = Math.sin(2 * Math.PI * (90 - t * 60) * t) * 0.4; + return (noise(t, 5) * 0.8 + rumble) * Math.exp(-t * 4.2); + })); + + // UFO hit: rewarding bright descending chime/arpeggio. + this.buffers.set('ufoHit', this._makeBuffer(0.55, (t) => { + const notes = [1568, 1244, 987, 1568]; + const idx = Math.min(notes.length - 1, Math.floor(t * 9)); + const freq = notes[idx]; + const env = Math.exp(-((t % 0.14)) * 10); + return Math.sin(2 * Math.PI * freq * t) * 0.4 * env; + })); + + // UFO flying loop: two alternating warbling tones (76477-chip style siren). + this.buffers.set('ufoFlying', this._makeBuffer(0.6, (t) => { + const warble = Math.sin(2 * Math.PI * 6 * t); + const freq = 300 + warble * 90; + return Math.sin(2 * Math.PI * freq * t) * 0.25; + })); + + // Four descending "march" notes forming the alien heartbeat. + const marchFreqs = [110, 98, 87, 82]; + marchFreqs.forEach((freq, idx) => { + this.buffers.set(`march${idx + 1}`, this._makeBuffer(0.11, (t) => { + const env = Math.exp(-t * 16); + return (Math.sign(Math.sin(2 * Math.PI * freq * t)) * 0.5 + Math.sin(2 * Math.PI * freq * 2 * t) * 0.2) * env; + })); + }); + + // Extra life awarded: bright ascending arpeggio. + this.buffers.set('extraLife', this._makeBuffer(0.5, (t) => { + const notes = [523, 659, 784, 1046]; + const idx = Math.min(notes.length - 1, Math.floor(t * 9)); + const env = Math.exp(-((t % 0.12)) * 9); + return Math.sin(2 * Math.PI * notes[idx] * t) * 0.35 * env; + })); + + // Wave clear fanfare. + this.buffers.set('waveClear', this._makeBuffer(0.7, (t) => { + const notes = [392, 523, 659, 784, 1046]; + const idx = Math.min(notes.length - 1, Math.floor(t * 7.5)); + const env = Math.exp(-((t % 0.13)) * 7); + return Math.sin(2 * Math.PI * notes[idx] * t) * 0.32 * env; + })); + + // Game over: descending, mournful. + this.buffers.set('gameOver', this._makeBuffer(1.1, (t) => { + const freq = 220 - t * 120; + const env = Math.exp(-t * 1.6); + return Math.sin(2 * Math.PI * Math.max(40, freq) * t) * 0.3 * env; + })); + } + + playSound(name, { loop = false, gain = 1 } = {}) { + if (!this.enabled || !this.ctx) return null; + const buffer = this.buffers.get(name); + if (!buffer) return null; + const source = this.ctx.createBufferSource(); + source.buffer = buffer; + source.loop = loop; + const g = this.ctx.createGain(); + g.gain.value = gain; + source.connect(g).connect(this.masterGain); + source.start(0); + return source; + } + + startUfoLoop() { + if (this.ufoSource || !this.ctx) return; + this.ufoSource = this.playSound('ufoFlying', { loop: true, gain: 0.6 }); + } + + stopUfoLoop() { + if (this.ufoSource) { + try { this.ufoSource.stop(); } catch (e) { /* already stopped */ } + this.ufoSource = null; + } + } + + playMarchStep(step) { + this.playSound(`march${(step % 4) + 1}`); + } +} diff --git a/space-invaders/js/collision.js b/space-invaders/js/collision.js new file mode 100644 index 000000000..8a38d6d1b --- /dev/null +++ b/space-invaders/js/collision.js @@ -0,0 +1,104 @@ +import { Explosion } from './entities/explosion.js'; +import { PALETTE } from './constants.js'; + +// Broad-phase: cheap Axis-Aligned Bounding Box overlap test used for every +// entity-to-entity pair before any expensive pixel work is considered. +export function isColliding(a, b) { + return a.x < b.x + b.width + && a.x + a.width > b.x + && a.y < b.y + b.height + && a.y + a.height > b.y; +} + +function bunkerNarrowPhaseHit(bunker, bullet) { + // Sample a few points along the bullet's leading edge against the bunker's + // live pixel bitmap — the narrow-phase check, only reached after AABB hits. + const points = [ + [bullet.x + bullet.width / 2, bullet.y], + [bullet.x + bullet.width / 2, bullet.y + bullet.height], + [bullet.x, bullet.y + bullet.height / 2], + [bullet.x + bullet.width, bullet.y + bullet.height / 2], + ]; + for (const [px, py] of points) { + if (bunker.isSolidAt(px, py)) return { x: px, y: py }; + } + return null; +} + +export function checkCollisions(game) { + const { player, swarm, mysteryShip, bunkers, playerBullets, alienBullets, explosions, audio } = game; + + // Player bullets vs aliens. + for (const bullet of playerBullets) { + if (!bullet.isAlive) continue; + for (const alien of swarm.aliens) { + if (!alien.isAlive) continue; + const pos = swarm.alienScreenPosition(alien); + const rect = { x: pos.x, y: pos.y, width: alien.width, height: alien.height }; + if (isColliding(bullet, rect)) { + alien.isAlive = false; + bullet.isAlive = false; + game.score += alien.points; + game.onScoreChanged(); + explosions.push(new Explosion(pos.x + alien.width / 2, pos.y + alien.height / 2, PALETTE.goldBright)); + audio.playSound('alienHit'); + break; + } + } + } + + // Player bullets vs mystery ship. + if (mysteryShip.active) { + for (const bullet of playerBullets) { + if (!bullet.isAlive) continue; + if (isColliding(bullet, mysteryShip)) { + bullet.isAlive = false; + const points = mysteryShip.scoreValue(); + game.score += points; + game.onScoreChanged(); + explosions.push(new Explosion( + mysteryShip.x + mysteryShip.width / 2, + mysteryShip.y + mysteryShip.height / 2, + PALETTE.ufo, + 0.5, + )); + audio.playSound('ufoHit'); + mysteryShip.despawn(); + } + } + } + + // Alien bullets vs player. + if (player.isAlive) { + for (const bullet of alienBullets) { + if (!bullet.isAlive) continue; + if (isColliding(bullet, player)) { + bullet.isAlive = false; + game.onPlayerHit(); + } + } + } + + // Bullets vs bunkers (AABB broad-phase, then pixel narrow-phase + damage). + const allBullets = [...playerBullets, ...alienBullets]; + for (const bullet of allBullets) { + if (!bullet.isAlive) continue; + for (const bunker of bunkers) { + if (!isColliding(bullet, bunker.getBounds())) continue; + const hit = bunkerNarrowPhaseHit(bunker, bullet); + if (hit) { + bunker.applyDamage(hit.x, hit.y); + bullet.isAlive = false; + break; + } + } + } + + // Reset the player's single-shot lock whenever its bullet has resolved. + for (const bullet of playerBullets) { + if (!bullet.isAlive) player.onBulletResolved(); + } + + game.playerBullets = playerBullets.filter((b) => b.isAlive); + game.alienBullets = alienBullets.filter((b) => b.isAlive); +} diff --git a/space-invaders/js/constants.js b/space-invaders/js/constants.js new file mode 100644 index 000000000..52c498d2c --- /dev/null +++ b/space-invaders/js/constants.js @@ -0,0 +1,89 @@ +// Core game constants — the "magic numbers" centralized for easy tuning. +export const CANVAS_WIDTH = 224; +export const CANVAS_HEIGHT = 256; + +export const ALIEN_ROWS = 5; +export const ALIEN_COLS = 11; +export const ALIEN_H_SPACING = 16; +export const ALIEN_V_SPACING = 16; +export const ALIEN_START_X = 16; +export const ALIEN_START_Y = 34; +export const ALIEN_DROP_DISTANCE = 8; +export const ALIEN_MAX_DEPTH_Y = 170; + +export const ALIEN_TYPES = { + SQUID: { name: 'squid', row: 0, width: 8, height: 8, points: 30 }, + CRAB: { name: 'crab', rows: [1, 2], width: 11, height: 8, points: 20 }, + OCTOPUS: { name: 'octopus', rows: [3, 4], width: 12, height: 8, points: 10 }, +}; + +export function alienTypeForRow(row) { + if (row === 0) return 'squid'; + if (row === 1 || row === 2) return 'crab'; + return 'octopus'; +} + +export function pointsForType(type) { + if (type === 'squid') return 30; + if (type === 'crab') return 20; + return 10; +} + +export const BASE_MOVE_INTERVAL = 900; // ms, slowest possible step (full swarm) +export const MIN_MOVE_INTERVAL = 45; // ms, fastest possible step (last alien) + +export const PLAYER_WIDTH = 13; +export const PLAYER_HEIGHT = 8; +export const PLAYER_SPEED = 90; // px/sec +export const PLAYER_START_LIVES = 3; +export const EXTRA_LIFE_SCORE = 1500; + +export const PLAYER_BULLET_SPEED = 220; // px/sec upward +export const ALIEN_BULLET_SPEED = 110; // px/sec downward + +export const MYSTERY_SHIP_WIDTH = 16; +export const MYSTERY_SHIP_HEIGHT = 7; +export const MYSTERY_SHIP_SPEED = 60; // px/sec +export const MYSTERY_SHIP_MIN_DELAY = 12000; +export const MYSTERY_SHIP_MAX_DELAY = 22000; + +export const UFO_SCORE_TABLE = [100, 50, 50, 100, 150, 100, 100, 50, 300, 100, 100, 100, 50, 150, 100]; + +export const BUNKER_COUNT = 4; +export const BUNKER_WIDTH = 22; +export const BUNKER_HEIGHT = 16; +export const BUNKER_Y = 200; + +export const ALIEN_FIRE_MIN_INTERVAL = 350; +export const ALIEN_FIRE_MAX_INTERVAL = 1000; + +// Column target sequences for the three canonical shot "personalities". +export const FIRE_PATTERNS = { + ROLLING: [4, 8, 2, 6, 10, 0, 9, 3, 7, 1, 5], + PLUNGER: [1, 7, 1, 1, 1, 4, 8, 2, 6, 10, 0, 9, 3, 7, 1], + SQUIGGLY: [5, 9, 3, 7, 1, 5, 10, 4, 8, 2, 6, 0, 9, 3, 7], +}; + +// Art Nouveau palette — jewel tones on deep indigo, gold linework throughout. +export const PALETTE = { + bgTop: '#120a24', + bgBottom: '#1d1030', + gold: '#d4af37', + goldBright: '#f3d47a', + goldDim: '#8a6a2a', + squid: '#3ea88a', + squidDark: '#215e4c', + crab: '#a56cc1', + crabDark: '#5c3670', + octopus: '#d4763f', + octopusDark: '#7a4022', + player: '#4fb8c9', + playerDark: '#245a63', + ufo: '#c0384f', + ufoDark: '#6e1c2b', + bunker: '#7a6a9a', + bunkerDark: '#3a2f52', + bulletPlayer: '#f3d47a', + bulletAlien: '#e0668a', + text: '#e8d9a8', +}; diff --git a/space-invaders/js/entities/alien.js b/space-invaders/js/entities/alien.js new file mode 100644 index 000000000..4c7dc50f8 --- /dev/null +++ b/space-invaders/js/entities/alien.js @@ -0,0 +1,16 @@ +import { alienTypeForRow, pointsForType } from '../constants.js'; + +export class Alien { + constructor(row, col, offsetX, offsetY) { + this.row = row; + this.col = col; + this.offsetX = offsetX; + this.offsetY = offsetY; + this.type = alienTypeForRow(row); + this.points = pointsForType(this.type); + this.isAlive = true; + this.frame = 0; + this.width = this.type === 'squid' ? 8 : this.type === 'crab' ? 11 : 12; + this.height = 8; + } +} diff --git a/space-invaders/js/entities/alienFireController.js b/space-invaders/js/entities/alienFireController.js new file mode 100644 index 000000000..10a8e08b3 --- /dev/null +++ b/space-invaders/js/entities/alienFireController.js @@ -0,0 +1,49 @@ +import { FIRE_PATTERNS, ALIEN_FIRE_MIN_INTERVAL, ALIEN_FIRE_MAX_INTERVAL, ALIEN_COLS } from '../constants.js'; +import { AlienBullet } from './bullet.js'; + +const PATTERN_NAMES = ['ROLLING', 'PLUNGER', 'SQUIGGLY']; + +// Models the original's deterministic, column-based alien firing rather than +// pure randomness: a shot "personality" is picked, then a sequence of target +// columns is walked, always hitting the lowest living alien in that column. +export class AlienFireController { + constructor(swarm) { + this.swarm = swarm; + this.timer = 0; + this.nextInterval = this._randomInterval(); + this.sequenceIndex = { ROLLING: 0, PLUNGER: 0, SQUIGGLY: 0 }; + } + + _randomInterval() { + return ALIEN_FIRE_MIN_INTERVAL + Math.random() * (ALIEN_FIRE_MAX_INTERVAL - ALIEN_FIRE_MIN_INTERVAL); + } + + reset() { + this.timer = 0; + this.nextInterval = this._randomInterval(); + } + + update(dt, alienBullets) { + this.timer += dt * 1000; + if (this.timer < this.nextInterval) return; + this.timer = 0; + this.nextInterval = this._randomInterval(); + + const patternName = PATTERN_NAMES[Math.floor(Math.random() * PATTERN_NAMES.length)]; + const sequence = FIRE_PATTERNS[patternName]; + const idx = this.sequenceIndex[patternName]; + const col = sequence[idx % sequence.length] % ALIEN_COLS; + this.sequenceIndex[patternName] = idx + 1; + + const shooter = this.swarm.lowestAlienInColumn(col); + if (!shooter) return; + + const pos = this.swarm.alienScreenPosition(shooter); + const bullet = new AlienBullet( + pos.x + shooter.width / 2 - 1.5, + pos.y + shooter.height, + patternName.toLowerCase(), + ); + alienBullets.push(bullet); + } +} diff --git a/space-invaders/js/entities/bullet.js b/space-invaders/js/entities/bullet.js new file mode 100644 index 000000000..32bae3464 --- /dev/null +++ b/space-invaders/js/entities/bullet.js @@ -0,0 +1,56 @@ +import { PLAYER_BULLET_SPEED, ALIEN_BULLET_SPEED, CANVAS_HEIGHT, PALETTE } from '../constants.js'; + +export class PlayerBullet { + constructor(x, y) { + this.x = x; + this.y = y; + this.width = 1; + this.height = 4; + this.isAlive = true; + } + + update(dt) { + this.y -= PLAYER_BULLET_SPEED * dt; + if (this.y + this.height < 0) this.isAlive = false; + } + + render(ctx) { + ctx.fillStyle = PALETTE.bulletPlayer; + ctx.shadowColor = PALETTE.bulletPlayer; + ctx.shadowBlur = 3; + ctx.fillRect(this.x, this.y, this.width, this.height); + ctx.shadowBlur = 0; + } +} + +const ALIEN_BULLET_WAVE = { amplitude: 1.5, frequency: 10 }; + +export class AlienBullet { + constructor(x, y, style = 'rolling') { + this.x = x; + this.y = y; + this.width = 3; + this.height = 7; + this.isAlive = true; + this.style = style; // 'rolling' | 'plunger' | 'squiggly' + this.age = 0; + } + + update(dt) { + this.age += dt; + this.y += ALIEN_BULLET_SPEED * dt; + if (this.style === 'squiggly') { + this.x += Math.sin(this.age * ALIEN_BULLET_WAVE.frequency) * ALIEN_BULLET_WAVE.amplitude * dt * 10; + } + if (this.y > CANVAS_HEIGHT) this.isAlive = false; + } + + render(ctx) { + ctx.fillStyle = PALETTE.bulletAlien; + ctx.shadowColor = PALETTE.bulletAlien; + ctx.shadowBlur = 3; + const wobble = this.style === 'squiggly' ? Math.sin(this.age * 14) * 1.2 : 0; + ctx.fillRect(this.x + wobble, this.y, this.width, this.height); + ctx.shadowBlur = 0; + } +} diff --git a/space-invaders/js/entities/bunker.js b/space-invaders/js/entities/bunker.js new file mode 100644 index 000000000..78dc99439 --- /dev/null +++ b/space-invaders/js/entities/bunker.js @@ -0,0 +1,94 @@ +import { BUNKER_WIDTH, BUNKER_HEIGHT, PALETTE } from '../constants.js'; + +const DAMAGE_BLOCK = 3; // chunky erase granularity, matches the original's blocky look + +export class Bunker { + constructor(x, y) { + this.x = x; + this.y = y; + this.width = BUNKER_WIDTH; + this.height = BUNKER_HEIGHT; + this.canvas = document.createElement('canvas'); + this.canvas.width = BUNKER_WIDTH; + this.canvas.height = BUNKER_HEIGHT; + this.ctx = this.canvas.getContext('2d', { willReadFrequently: true }); + this.repair(); + } + + // Draws the pristine ornamental bunker — an Art Nouveau arch with carved + // filigree — onto the off-screen bitmap that damage will later erase from. + repair() { + const c = this.ctx; + c.clearRect(0, 0, this.width, this.height); + c.fillStyle = PALETTE.bunker; + c.strokeStyle = PALETTE.bunkerDark; + c.lineWidth = 1; + + c.beginPath(); + c.moveTo(0, this.height); + c.lineTo(0, 6); + c.quadraticCurveTo(0, 0, 6, 0); + c.lineTo(this.width - 6, 0); + c.quadraticCurveTo(this.width, 0, this.width, 6); + c.lineTo(this.width, this.height); + // notch cut from underside like the classic bunker silhouette + c.lineTo(this.width * 0.68, this.height); + c.quadraticCurveTo(this.width * 0.55, this.height - 6, this.width * 0.5, this.height - 6); + c.quadraticCurveTo(this.width * 0.45, this.height - 6, this.width * 0.32, this.height); + c.closePath(); + c.fill(); + c.stroke(); + + // Ornamental filigree veins. + c.strokeStyle = PALETTE.goldDim; + c.lineWidth = 0.5; + c.beginPath(); + c.moveTo(3, this.height - 3); + c.quadraticCurveTo(this.width / 2, 2, this.width - 3, this.height - 3); + c.stroke(); + + this.isDestroyed = false; + } + + getBounds() { + return { x: this.x, y: this.y, width: this.width, height: this.height }; + } + + // Narrow-phase impact: erase a chunky radius of pixels around the impact + // point by zeroing their alpha, then write the modified bitmap back. + applyDamage(worldX, worldY, radius = 4) { + const localX = Math.round(worldX - this.x); + const localY = Math.round(worldY - this.y); + const imageData = this.ctx.getImageData(0, 0, this.width, this.height); + const data = imageData.data; + + for (let by = -radius; by <= radius; by += DAMAGE_BLOCK) { + for (let bx = -radius; bx <= radius; bx += DAMAGE_BLOCK) { + if (bx * bx + by * by > radius * radius) continue; + for (let dy = 0; dy < DAMAGE_BLOCK; dy++) { + for (let dx = 0; dx < DAMAGE_BLOCK; dx++) { + const px = localX + bx + dx; + const py = localY + by + dy; + if (px < 0 || py < 0 || px >= this.width || py >= this.height) continue; + const idx = (py * this.width + px) * 4 + 3; + data[idx] = 0; + } + } + } + } + this.ctx.putImageData(imageData, 0, 0); + } + + // Pixel-perfect hit test used as the narrow-phase check after an AABB hit. + isSolidAt(worldX, worldY) { + const localX = Math.round(worldX - this.x); + const localY = Math.round(worldY - this.y); + if (localX < 0 || localY < 0 || localX >= this.width || localY >= this.height) return false; + const pixel = this.ctx.getImageData(localX, localY, 1, 1).data; + return pixel[3] > 0; + } + + render(ctx) { + ctx.drawImage(this.canvas, this.x, this.y); + } +} diff --git a/space-invaders/js/entities/explosion.js b/space-invaders/js/entities/explosion.js new file mode 100644 index 000000000..f594c2cc3 --- /dev/null +++ b/space-invaders/js/entities/explosion.js @@ -0,0 +1,21 @@ +import { drawExplosion } from '../render.js'; + +export class Explosion { + constructor(x, y, color, duration = 0.35) { + this.x = x; + this.y = y; + this.color = color; + this.age = 0; + this.duration = duration; + this.isAlive = true; + } + + update(dt) { + this.age += dt; + if (this.age >= this.duration) this.isAlive = false; + } + + render(ctx) { + drawExplosion(ctx, this); + } +} diff --git a/space-invaders/js/entities/mysteryShip.js b/space-invaders/js/entities/mysteryShip.js new file mode 100644 index 000000000..8c62f1283 --- /dev/null +++ b/space-invaders/js/entities/mysteryShip.js @@ -0,0 +1,78 @@ +import { + MYSTERY_SHIP_WIDTH, MYSTERY_SHIP_HEIGHT, MYSTERY_SHIP_SPEED, CANVAS_WIDTH, + MYSTERY_SHIP_MIN_DELAY, MYSTERY_SHIP_MAX_DELAY, UFO_SCORE_TABLE, PALETTE, +} from '../constants.js'; + +export class MysteryShip { + constructor(game) { + this.game = game; + this.width = MYSTERY_SHIP_WIDTH; + this.height = MYSTERY_SHIP_HEIGHT; + this.x = 0; + this.y = 12; + this.direction = 1; + this.active = false; + this.spawnTimer = this._randomDelay(); + } + + _randomDelay() { + return MYSTERY_SHIP_MIN_DELAY + Math.random() * (MYSTERY_SHIP_MAX_DELAY - MYSTERY_SHIP_MIN_DELAY); + } + + _spawn() { + this.direction = Math.random() < 0.5 ? 1 : -1; + this.x = this.direction === 1 ? -this.width : CANVAS_WIDTH + this.width; + this.active = true; + this.game.audio.startUfoLoop(); + } + + despawn() { + this.active = false; + this.game.audio.stopUfoLoop(); + this.spawnTimer = this._randomDelay(); + } + + // Score awarded follows the original's hidden deterministic sequence based + // on total shots fired, not randomness. + scoreValue() { + return UFO_SCORE_TABLE[this.game.playerShotCount % UFO_SCORE_TABLE.length]; + } + + update(dt) { + if (!this.active) { + this.spawnTimer -= dt * 1000; + if (this.spawnTimer <= 0) this._spawn(); + return; + } + this.x += this.direction * MYSTERY_SHIP_SPEED * dt; + if (this.direction === 1 && this.x > CANVAS_WIDTH) this.despawn(); + if (this.direction === -1 && this.x + this.width < 0) this.despawn(); + } + + render(ctx) { + if (!this.active) return; + ctx.save(); + ctx.translate(this.x, this.y); + ctx.shadowColor = PALETTE.ufo; + ctx.shadowBlur = 5; + ctx.fillStyle = PALETTE.ufo; + ctx.strokeStyle = PALETTE.gold; + ctx.lineWidth = 0.6; + + ctx.beginPath(); + ctx.moveTo(1, this.height); + ctx.bezierCurveTo(-2, this.height, -2, 2, this.width / 2, 1); + ctx.bezierCurveTo(this.width + 2, 2, this.width + 2, this.height, this.width - 1, this.height); + ctx.bezierCurveTo(this.width - 3, this.height + 2, 3, this.height + 2, 1, this.height); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + // Gem-like canopy jewel, the classic Nouveau "peacock eye" motif. + ctx.fillStyle = PALETTE.goldBright; + ctx.beginPath(); + ctx.ellipse(this.width / 2, this.height - 1, 2.2, 1.6, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); + } +} diff --git a/space-invaders/js/entities/player.js b/space-invaders/js/entities/player.js new file mode 100644 index 000000000..b00ec4525 --- /dev/null +++ b/space-invaders/js/entities/player.js @@ -0,0 +1,90 @@ +import { + PLAYER_WIDTH, PLAYER_HEIGHT, PLAYER_SPEED, CANVAS_WIDTH, CANVAS_HEIGHT, PALETTE, +} from '../constants.js'; +import { PlayerBullet } from './bullet.js'; + +export class Player { + constructor(game) { + this.game = game; + this.width = PLAYER_WIDTH; + this.height = PLAYER_HEIGHT; + this.x = (CANVAS_WIDTH - this.width) / 2; + this.y = CANVAS_HEIGHT - 24; + this.canFire = true; + this.isAlive = true; + this.hitFlashTimer = 0; + } + + update(dt, input) { + if (!this.isAlive) return; + let dx = 0; + if (input.left) dx -= 1; + if (input.right) dx += 1; + this.x += dx * PLAYER_SPEED * dt; + this.x = Math.max(2, Math.min(CANVAS_WIDTH - this.width - 2, this.x)); + + if (input.fire) this.fire(); + if (this.hitFlashTimer > 0) this.hitFlashTimer -= dt; + } + + fire() { + if (!this.canFire || !this.isAlive) return; + const bullet = new PlayerBullet(this.x + this.width / 2 - 0.5, this.y - 4); + this.game.playerBullets.push(bullet); + this.canFire = false; + this.game.playerShotCount += 1; + this.game.audio.playSound('shoot'); + } + + onBulletResolved() { + this.canFire = true; + } + + render(ctx) { + if (!this.isAlive) return; + const cx = this.x + this.width / 2; + const cy = this.y + this.height; + ctx.save(); + ctx.translate(cx, cy); + + // Art Nouveau cannon: a stylised winged/finned dart with jewel core and + // whiplash tendrils sweeping back from the hull, rendered as vector paths. + const glow = this.hitFlashTimer > 0 ? PALETTE.goldBright : PALETTE.player; + ctx.strokeStyle = PALETTE.gold; + ctx.lineWidth = 0.6; + ctx.fillStyle = glow; + ctx.shadowColor = glow; + ctx.shadowBlur = 4; + + ctx.beginPath(); + ctx.moveTo(0, -this.height); + ctx.bezierCurveTo(-2, -this.height + 2, -3, -2, -this.width / 2, 0); + ctx.lineTo(-this.width / 2 + 1, -1); + ctx.bezierCurveTo(-3, -2.5, -1.5, -3.5, 0, -this.height + 1); + ctx.bezierCurveTo(1.5, -3.5, 3, -2.5, this.width / 2 - 1, -1); + ctx.lineTo(this.width / 2, 0); + ctx.bezierCurveTo(3, -2, 2, -this.height + 2, 0, -this.height); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + // Tendril flourishes curling from either side of the hull. + ctx.strokeStyle = PALETTE.goldDim; + ctx.lineWidth = 0.5; + ctx.shadowBlur = 0; + ctx.beginPath(); + ctx.moveTo(-this.width / 2, -1); + ctx.quadraticCurveTo(-this.width / 2 - 3, -3, -this.width / 2 - 1, -6); + ctx.moveTo(this.width / 2, -1); + ctx.quadraticCurveTo(this.width / 2 + 3, -3, this.width / 2 + 1, -6); + ctx.stroke(); + + // Central jewel core. + ctx.fillStyle = PALETTE.goldBright; + ctx.beginPath(); + ctx.ellipse(0, -this.height + 3, 1.4, 1.8, 0, 0, Math.PI * 2); + ctx.fill(); + + ctx.restore(); + } +} diff --git a/space-invaders/js/entities/swarm.js b/space-invaders/js/entities/swarm.js new file mode 100644 index 000000000..a65243df8 --- /dev/null +++ b/space-invaders/js/entities/swarm.js @@ -0,0 +1,127 @@ +import { + ALIEN_ROWS, ALIEN_COLS, ALIEN_H_SPACING, ALIEN_V_SPACING, ALIEN_START_X, ALIEN_START_Y, + ALIEN_DROP_DISTANCE, CANVAS_WIDTH, BASE_MOVE_INTERVAL, MIN_MOVE_INTERVAL, +} from '../constants.js'; +import { Alien } from './alien.js'; +import { AlienFireController } from './alienFireController.js'; +import { drawAlien } from '../render.js'; + +const EDGE_MARGIN = 10; +const STEP_SIZE = 3; + +// The "Hive Mind": a single controller owning the swarm's collective position, +// direction and pace. Individual Aliens never store world coordinates — their +// screen position is always swarm-origin + fixed grid offset. +export class Swarm { + constructor(game) { + this.game = game; + this.x = 0; + this.y = 0; + this.direction = 1; + this.timer = 0; + this.animFrame = 0; + this.marchStep = 0; + this.aliens = []; + this.fireController = new AlienFireController(this); + this._buildGrid(ALIEN_START_Y); + } + + _buildGrid(startY) { + this.aliens = []; + for (let row = 0; row < ALIEN_ROWS; row++) { + for (let col = 0; col < ALIEN_COLS; col++) { + const offsetX = ALIEN_START_X + col * ALIEN_H_SPACING; + const offsetY = startY + row * ALIEN_V_SPACING; + this.aliens.push(new Alien(row, col, offsetX, offsetY)); + } + } + } + + resetForWave(startY) { + this.x = 0; + this.y = 0; + this.direction = 1; + this.timer = 0; + this.marchStep = 0; + this._buildGrid(startY); + this.fireController.reset(); + } + + get aliveCount() { + return this.aliens.reduce((n, a) => n + (a.isAlive ? 1 : 0), 0); + } + + get isDefeated() { + return this.aliveCount === 0; + } + + alienScreenPosition(alien) { + return { x: this.x + alien.offsetX, y: this.y + alien.offsetY }; + } + + lowestAlienInColumn(col) { + let best = null; + for (const a of this.aliens) { + if (!a.isAlive || a.col !== col) continue; + if (!best || a.row > best.row) best = a; + } + return best; + } + + bottomMostY() { + let maxY = -Infinity; + for (const a of this.aliens) { + if (!a.isAlive) continue; + const y = this.y + a.offsetY + a.height; + if (y > maxY) maxY = y; + } + return maxY === -Infinity ? 0 : maxY; + } + + update(dt, alienBullets) { + const total = ALIEN_ROWS * ALIEN_COLS; + const alive = this.aliveCount; + if (alive === 0) return; + + const moveInterval = Math.max(MIN_MOVE_INTERVAL, (alive / total) * BASE_MOVE_INTERVAL); + this.timer += dt * 1000; + + if (this.timer >= moveInterval) { + this.timer = 0; + this._step(); + this.game.audio.playMarchStep(this.marchStep); + this.marchStep += 1; + this.animFrame = 1 - this.animFrame; + } + + this.fireController.update(dt, alienBullets); + } + + _step() { + this.x += this.direction * STEP_SIZE; + + let leftmost = Infinity; + let rightmost = -Infinity; + for (const a of this.aliens) { + if (!a.isAlive) continue; + const left = this.x + a.offsetX; + const right = left + a.width; + if (left < leftmost) leftmost = left; + if (right > rightmost) rightmost = right; + } + if (leftmost === Infinity) return; + + if (rightmost >= CANVAS_WIDTH - EDGE_MARGIN || leftmost <= EDGE_MARGIN) { + this.y += ALIEN_DROP_DISTANCE; + this.direction *= -1; + } + } + + render(ctx) { + for (const alien of this.aliens) { + if (!alien.isAlive) continue; + const pos = this.alienScreenPosition(alien); + drawAlien(ctx, alien, pos.x, pos.y, this.animFrame); + } + } +} diff --git a/space-invaders/js/game.js b/space-invaders/js/game.js new file mode 100644 index 000000000..ea49cdcdf --- /dev/null +++ b/space-invaders/js/game.js @@ -0,0 +1,165 @@ +import { + CANVAS_WIDTH, CANVAS_HEIGHT, ALIEN_START_Y, ALIEN_DROP_DISTANCE, ALIEN_MAX_DEPTH_Y, + BUNKER_COUNT, BUNKER_WIDTH, BUNKER_Y, EXTRA_LIFE_SCORE, PLAYER_START_LIVES, +} from './constants.js'; +import { InputHandler } from './input.js'; +import { AudioManager } from './audio.js'; +import { Player } from './entities/player.js'; +import { Swarm } from './entities/swarm.js'; +import { MysteryShip } from './entities/mysteryShip.js'; +import { Bunker } from './entities/bunker.js'; +import { Explosion } from './entities/explosion.js'; +import { StateMachine } from './state/stateMachine.js'; +import { createAttractState } from './state/attractState.js'; +import { createPlayState } from './state/playState.js'; +import { createGameOverState } from './state/gameOverState.js'; + +const HIGH_SCORE_KEY = 'space-invaders-nouveau-high-score'; + +class Game { + constructor(ctx) { + this.ctx = ctx; + this.input = new InputHandler(); + this.audio = new AudioManager(); + + this.score = 0; + this.highScore = Number(localStorage.getItem(HIGH_SCORE_KEY)) || 0; + this.lives = PLAYER_START_LIVES; + this.wave = 1; + this.playerShotCount = 0; + this.extraLifeAwarded = false; + + this.player = new Player(this); + this.swarm = new Swarm(this); + this.mysteryShip = new MysteryShip(this); + this.bunkers = this._createBunkers(); + this.playerBullets = []; + this.alienBullets = []; + this.explosions = []; + + this.hud = { + score: document.getElementById('score-value'), + lives: document.getElementById('lives-value'), + highScore: document.getElementById('highscore-value'), + }; + + this._updateHud(); + + const unlockOnce = () => { this.audio.unlock(); window.removeEventListener('keydown', unlockOnce); window.removeEventListener('touchstart', unlockOnce); }; + window.addEventListener('keydown', unlockOnce); + window.addEventListener('touchstart', unlockOnce); + } + + _createBunkers() { + const margin = (CANVAS_WIDTH - BUNKER_COUNT * BUNKER_WIDTH) / (BUNKER_COUNT + 1); + const bunkers = []; + for (let i = 0; i < BUNKER_COUNT; i++) { + const x = margin + i * (BUNKER_WIDTH + margin); + bunkers.push(new Bunker(Math.round(x), BUNKER_Y)); + } + return bunkers; + } + + _updateHud() { + this.hud.score.textContent = String(this.score).padStart(4, '0'); + this.hud.lives.textContent = String(Math.max(0, this.lives)); + this.hud.highScore.textContent = String(this.highScore).padStart(4, '0'); + } + + onScoreChanged() { + if (this.score > this.highScore) { + this.highScore = this.score; + localStorage.setItem(HIGH_SCORE_KEY, String(this.highScore)); + } + if (!this.extraLifeAwarded && this.score >= EXTRA_LIFE_SCORE) { + this.extraLifeAwarded = true; + this.lives += 1; + this.audio.playSound('extraLife'); + } + this._updateHud(); + } + + onPlayerHit() { + if (!this.player.isAlive) return; + this.player.isAlive = false; + this.lives -= 1; + this.player.hitFlashTimer = 0.3; + this.audio.playSound('playerExplosion'); + this._updateHud(); + this.explosions.push(new Explosion( + this.player.x + this.player.width / 2, + this.player.y + this.player.height / 2, + '#4fb8c9', + 0.6, + )); + } + + startNewGame() { + this.score = 0; + this.lives = PLAYER_START_LIVES; + this.wave = 1; + this.playerShotCount = 0; + this.extraLifeAwarded = false; + this.playerBullets = []; + this.alienBullets = []; + this.explosions = []; + this.player = new Player(this); + this.mysteryShip = new MysteryShip(this); + this.swarm.resetForWave(ALIEN_START_Y); + for (const b of this.bunkers) b.repair(); + this._updateHud(); + } + + nextWave() { + this.wave += 1; + const depth = Math.min(ALIEN_START_Y + (this.wave - 1) * ALIEN_DROP_DISTANCE, ALIEN_MAX_DEPTH_Y); + this.swarm.resetForWave(depth); + this.playerBullets = []; + this.alienBullets = []; + this.player.isAlive = true; + this.player.canFire = true; + this.player.x = (CANVAS_WIDTH - this.player.width) / 2; + for (const b of this.bunkers) b.repair(); + } +} + +function init() { + const canvas = document.getElementById('game-canvas'); + canvas.width = CANVAS_WIDTH; + canvas.height = CANVAS_HEIGHT; + const ctx = canvas.getContext('2d'); + ctx.imageSmoothingEnabled = false; + + const game = new Game(ctx); + window.__game = game; // debugging hook only + + const machine = new StateMachine({}); + // Build states after machine exists so they can call machine.transition(). + machine.states.attract = createAttractState(game, machine); + machine.states.play = createPlayState(game, machine); + machine.states.gameOver = createGameOverState(game, machine); + machine.start('attract'); + + game.input.bindTouchControls(document.getElementById('touch-controls')); + + let lastTime = performance.now(); + function loop(now) { + let deltaTime = (now - lastTime) / 1000; + lastTime = now; + deltaTime = Math.min(deltaTime, 1 / 20); // clamp to avoid spiral-of-death on tab switch back + + machine.update(deltaTime); + machine.render(ctx); + + requestAnimationFrame(loop); + } + requestAnimationFrame(loop); + + if ('serviceWorker' in navigator) { + window.addEventListener('load', () => { + navigator.serviceWorker.register('./sw.js').catch(() => { /* offline support is best-effort */ }); + }); + } +} + +window.addEventListener('load', init); diff --git a/space-invaders/js/input.js b/space-invaders/js/input.js new file mode 100644 index 000000000..3cabbe3ca --- /dev/null +++ b/space-invaders/js/input.js @@ -0,0 +1,66 @@ +// Polls keyboard + touch input and exposes a single, unified state object. +export class InputHandler { + constructor() { + this.left = false; + this.right = false; + this.fire = false; + // Edge-triggered convenience for menu / restart actions. Latched by the + // raw press event itself (not just polled state) so a tap shorter than + // one animation frame is never dropped. + this.firePressed = false; + this._firePressLatch = false; + + window.addEventListener('keydown', (e) => this._onKey(e, true)); + window.addEventListener('keyup', (e) => this._onKey(e, false)); + } + + _onKey(e, isDown) { + switch (e.code) { + case 'ArrowLeft': + case 'KeyA': + this.left = isDown; + e.preventDefault(); + break; + case 'ArrowRight': + case 'KeyD': + this.right = isDown; + e.preventDefault(); + break; + case 'Space': + case 'ArrowUp': + case 'KeyW': + if (isDown && !this.fire) this._firePressLatch = true; + this.fire = isDown; + e.preventDefault(); + break; + } + } + + bindTouchControls(root) { + const bind = (selector, prop) => { + const el = root.querySelector(selector); + if (!el) return; + const start = (e) => { + e.preventDefault(); + if (prop === 'fire' && !this.fire) this._firePressLatch = true; + this[prop] = true; + }; + const end = (e) => { e.preventDefault(); this[prop] = false; }; + el.addEventListener('touchstart', start, { passive: false }); + el.addEventListener('touchend', end, { passive: false }); + el.addEventListener('touchcancel', end, { passive: false }); + el.addEventListener('mousedown', start); + el.addEventListener('mouseup', end); + el.addEventListener('mouseleave', end); + }; + bind('[data-control="left"]', 'left'); + bind('[data-control="right"]', 'right'); + bind('[data-control="fire"]', 'fire'); + } + + // Call once per frame: drains the latch into a one-frame-wide pulse. + update() { + this.firePressed = this._firePressLatch; + this._firePressLatch = false; + } +} diff --git a/space-invaders/js/render.js b/space-invaders/js/render.js new file mode 100644 index 000000000..939d23a59 --- /dev/null +++ b/space-invaders/js/render.js @@ -0,0 +1,203 @@ +import { CANVAS_WIDTH, CANVAS_HEIGHT, PALETTE } from './constants.js'; + +// --- Alien sprites ----------------------------------------------------- +// Each alien type is a small stained-glass/whiplash-curve creature drawn as +// a mirrored vector path (draw the right half, reflect for the left) so the +// silhouette stays perfectly symmetric like Art Nouveau glasswork. + +function halfPath(type, frame) { + switch (type) { + case 'squid': + return (c) => { + c.moveTo(0, 0); + c.quadraticCurveTo(2, -1, 3, -3); + c.quadraticCurveTo(4, -5, 2, -6); + c.quadraticCurveTo(1, -6.5, 0, -6); + c.lineTo(0, -1); + c.quadraticCurveTo(3, 0, 4, 2); + if (frame === 0) { + c.quadraticCurveTo(4.5, 4, 3, 4); + c.quadraticCurveTo(2.5, 3, 2, 2); + } else { + c.quadraticCurveTo(5, 3, 4, 4.5); + c.quadraticCurveTo(3, 3, 2, 2); + } + c.lineTo(0, 1); + }; + case 'crab': + return (c) => { + c.moveTo(0, -6); + c.quadraticCurveTo(3, -6.5, 4, -4); + c.quadraticCurveTo(4.5, -2, 3, -1); + c.lineTo(5.5, -1); + c.quadraticCurveTo(6, 0, 5, 1); + c.lineTo(3, 1); + if (frame === 0) { + c.lineTo(4, 4); + c.quadraticCurveTo(3, 4.5, 2, 3); + } else { + c.lineTo(2, 4.5); + c.quadraticCurveTo(1.5, 4, 1, 3); + } + c.lineTo(1, 1); + c.lineTo(0, 1); + }; + default: // octopus + return (c) => { + c.moveTo(0, -6); + c.quadraticCurveTo(4, -6.5, 5, -3); + c.quadraticCurveTo(5.5, -1, 4, 0); + c.lineTo(5.5, 0.5); + if (frame === 0) { + c.lineTo(5, 4); + c.lineTo(3.5, 2); + } else { + c.lineTo(3.5, 4.5); + c.lineTo(2.5, 2); + } + c.lineTo(2, 3); + c.lineTo(0, 1); + }; + } +} + +const ALIEN_COLORS = { + squid: [PALETTE.squid, PALETTE.squidDark], + crab: [PALETTE.crab, PALETTE.crabDark], + octopus: [PALETTE.octopus, PALETTE.octopusDark], +}; + +export function drawAlien(ctx, alien, x, y, animFrame) { + const [fill, dark] = ALIEN_COLORS[alien.type]; + const scale = alien.type === 'squid' ? 1 : alien.type === 'crab' ? 1.3 : 1.4; + ctx.save(); + ctx.translate(x + alien.width / 2, y + alien.height / 2 + 1); + ctx.scale(scale, scale); + ctx.fillStyle = fill; + ctx.strokeStyle = dark; + ctx.lineWidth = 0.4; + ctx.shadowColor = fill; + ctx.shadowBlur = 2.5; + + const build = halfPath(alien.type, animFrame); + ctx.beginPath(); + build(ctx); + ctx.scale(-1, 1); + build(ctx); + ctx.closePath(); + ctx.fill(); + ctx.stroke(); + + // Jewel eyes. + ctx.shadowBlur = 0; + ctx.fillStyle = PALETTE.goldBright; + ctx.beginPath(); + ctx.ellipse(-1, -2.5, 0.6, 0.6, 0, 0, Math.PI * 2); + ctx.ellipse(1, -2.5, 0.6, 0.6, 0, 0, Math.PI * 2); + ctx.fill(); + ctx.restore(); +} + +// --- Explosion particles ------------------------------------------------ +// A blooming-flower burst: petals of light radiating and curling outward. +export function drawExplosion(ctx, explosion) { + const { x, y, age, duration, color } = explosion; + const progress = age / duration; + const petals = 8; + const radius = 2 + progress * 7; + const alpha = 1 - progress; + + ctx.save(); + ctx.translate(x, y); + ctx.globalAlpha = alpha; + ctx.fillStyle = color; + ctx.shadowColor = color; + ctx.shadowBlur = 4; + + for (let i = 0; i < petals; i++) { + const angle = (i / petals) * Math.PI * 2 + progress * 1.2; + const px = Math.cos(angle) * radius; + const py = Math.sin(angle) * radius; + ctx.beginPath(); + ctx.ellipse(px, py, 1.6 * (1 - progress * 0.4), 0.8, angle, 0, Math.PI * 2); + ctx.fill(); + } + ctx.beginPath(); + ctx.arc(0, 0, 1.4 * (1 - progress), 0, Math.PI * 2); + ctx.fillStyle = PALETTE.goldBright; + ctx.fill(); + ctx.restore(); +} + +// --- Backdrop ------------------------------------------------------------- +// Deep nebula gradient with slow-drifting whiplash curves (the defining +// Art Nouveau line — an elongated "S" swept across the field). +export function drawBackground(ctx, time) { + const grad = ctx.createLinearGradient(0, 0, 0, CANVAS_HEIGHT); + grad.addColorStop(0, PALETTE.bgTop); + grad.addColorStop(1, PALETTE.bgBottom); + ctx.fillStyle = grad; + ctx.fillRect(0, 0, CANVAS_WIDTH, CANVAS_HEIGHT); + + ctx.save(); + ctx.globalAlpha = 0.12; + ctx.strokeStyle = PALETTE.gold; + ctx.lineWidth = 1.2; + const drift = (time * 0.005) % (Math.PI * 2); + for (let i = 0; i < 3; i++) { + const yBase = 40 + i * 80 + Math.sin(drift + i) * 10; + ctx.beginPath(); + ctx.moveTo(-20, yBase); + ctx.bezierCurveTo( + CANVAS_WIDTH * 0.25, yBase - 30 + Math.sin(drift + i) * 12, + CANVAS_WIDTH * 0.75, yBase + 30 - Math.sin(drift + i) * 12, + CANVAS_WIDTH + 20, yBase, + ); + ctx.stroke(); + } + ctx.restore(); + + // Faint static starfield (deterministic pseudo-random, no per-frame alloc). + ctx.save(); + ctx.fillStyle = PALETTE.gold; + for (let i = 0; i < 40; i++) { + const sx = (i * 53.7) % CANVAS_WIDTH; + const sy = (i * 97.3) % CANVAS_HEIGHT; + const tw = 0.3 + 0.3 * Math.sin(time * 0.002 + i); + ctx.globalAlpha = Math.max(0.05, tw); + ctx.fillRect(sx, sy, 1, 1); + } + ctx.restore(); +} + +// --- Ornamental frame ----------------------------------------------------- +// A gilded proscenium border wrapping the playfield, evoking a Mucha poster +// frame: corner medallions joined by flowing vine linework. +export function drawFrame(ctx) { + ctx.save(); + ctx.strokeStyle = PALETTE.gold; + ctx.lineWidth = 2; + ctx.strokeRect(1, 1, CANVAS_WIDTH - 2, CANVAS_HEIGHT - 2); + + ctx.lineWidth = 0.7; + ctx.globalAlpha = 0.8; + const corners = [ + [0, 0, 1, 1], + [CANVAS_WIDTH, 0, -1, 1], + [0, CANVAS_HEIGHT, 1, -1], + [CANVAS_WIDTH, CANVAS_HEIGHT, -1, -1], + ]; + for (const [cx, cy, sx, sy] of corners) { + ctx.save(); + ctx.translate(cx, cy); + ctx.scale(sx, sy); + ctx.beginPath(); + ctx.moveTo(2, 10); + ctx.quadraticCurveTo(2, 2, 10, 2); + ctx.moveTo(4, 14); + ctx.quadraticCurveTo(14, 14, 14, 4); + ctx.stroke(); + ctx.restore(); + } + ctx.restore(); +} diff --git a/space-invaders/js/state/attractState.js b/space-invaders/js/state/attractState.js new file mode 100644 index 000000000..b2c6ff41e --- /dev/null +++ b/space-invaders/js/state/attractState.js @@ -0,0 +1,85 @@ +import { CANVAS_WIDTH, CANVAS_HEIGHT, PALETTE } from '../constants.js'; +import { drawBackground, drawFrame, drawAlien } from '../render.js'; +import { Alien } from '../entities/alien.js'; + +const LEGEND = [ + { type: 'squid', label: '= 30 PTS' }, + { type: 'crab', label: '= 20 PTS' }, + { type: 'octopus', label: '= 10 PTS' }, +]; + +export function createAttractState(game, machine) { + let time = 0; + let blink = 0; + const legendAliens = LEGEND.map((entry, i) => { + const a = new Alien(i === 0 ? 0 : i === 1 ? 1 : 3, 0, 0, 0); + return a; + }); + + return { + enter() { + time = 0; + game.audio.stopUfoLoop(); + }, + exit() {}, + update(dt) { + time += dt; + blink += dt; + game.input.update(); + if (game.input.firePressed) { + game.audio.unlock(); + game.startNewGame(); + machine.transition('play'); + } + }, + render(ctx) { + drawBackground(ctx, time * 1000); + drawFrame(ctx); + + ctx.save(); + ctx.textAlign = 'center'; + ctx.fillStyle = PALETTE.goldBright; + ctx.shadowColor = PALETTE.gold; + ctx.shadowBlur = 6; + ctx.font = '16px Georgia, serif'; + ctx.fillText('SPACE', CANVAS_WIDTH / 2, 56); + ctx.fillText('INVADERS', CANVAS_WIDTH / 2, 74); + ctx.font = 'italic 6px Georgia, serif'; + ctx.fillStyle = PALETTE.gold; + ctx.shadowBlur = 0; + ctx.fillText('— nouveau —', CANVAS_WIDTH / 2, 84); + ctx.restore(); + + ctx.save(); + ctx.textAlign = 'left'; + ctx.font = '6px Georgia, serif'; + ctx.fillStyle = PALETTE.text; + let ly = 118; + ctx.fillText('* SCORE ADVANCE TABLE *', CANVAS_WIDTH / 2 - 52, ly - 12); + for (let i = 0; i < legendAliens.length; i++) { + const alien = legendAliens[i]; + const x = CANVAS_WIDTH / 2 - 40; + drawAlien(ctx, alien, x, ly - 3, 0); + ctx.fillText(LEGEND[i].label, x + 14, ly); + ly += 16; + } + ctx.restore(); + + if (Math.floor(blink * 2) % 2 === 0) { + ctx.save(); + ctx.textAlign = 'center'; + ctx.fillStyle = PALETTE.goldBright; + ctx.font = '8px Georgia, serif'; + ctx.fillText('PRESS FIRE TO START', CANVAS_WIDTH / 2, CANVAS_HEIGHT - 40); + ctx.restore(); + } + + ctx.save(); + ctx.textAlign = 'center'; + ctx.fillStyle = PALETTE.text; + ctx.font = '6px Georgia, serif'; + ctx.fillText('ARROWS / A-D MOVE SPACE FIRE', CANVAS_WIDTH / 2, CANVAS_HEIGHT - 20); + ctx.restore(); + }, + }; +} diff --git a/space-invaders/js/state/gameOverState.js b/space-invaders/js/state/gameOverState.js new file mode 100644 index 000000000..8ceb12719 --- /dev/null +++ b/space-invaders/js/state/gameOverState.js @@ -0,0 +1,51 @@ +import { CANVAS_WIDTH, CANVAS_HEIGHT, PALETTE } from '../constants.js'; +import { drawBackground, drawFrame } from '../render.js'; + +const MIN_DISPLAY_TIME = 1500; + +export function createGameOverState(game, machine) { + let time = 0; + let blink = 0; + + return { + enter() { + time = 0; + blink = 0; + game.audio.playSound('gameOver'); + game.audio.stopUfoLoop(); + }, + exit() {}, + update(dt) { + time += dt; + blink += dt; + game.input.update(); + if (time * 1000 > MIN_DISPLAY_TIME && game.input.firePressed) { + machine.transition('attract'); + } + }, + render(ctx) { + drawBackground(ctx, time * 1000); + drawFrame(ctx); + + ctx.save(); + ctx.textAlign = 'center'; + ctx.fillStyle = PALETTE.ufo; + ctx.shadowColor = PALETTE.gold; + ctx.shadowBlur = 6; + ctx.font = '18px Georgia, serif'; + ctx.fillText('GAME OVER', CANVAS_WIDTH / 2, 110); + + ctx.font = '8px Georgia, serif'; + ctx.fillStyle = PALETTE.text; + ctx.shadowBlur = 0; + ctx.fillText(`SCORE ${game.score}`, CANVAS_WIDTH / 2, 135); + ctx.fillText(`HIGH SCORE ${game.highScore}`, CANVAS_WIDTH / 2, 150); + + if (Math.floor(blink * 2) % 2 === 0) { + ctx.fillStyle = PALETTE.goldBright; + ctx.fillText('PRESS FIRE TO CONTINUE', CANVAS_WIDTH / 2, CANVAS_HEIGHT - 50); + } + ctx.restore(); + }, + }; +} diff --git a/space-invaders/js/state/playState.js b/space-invaders/js/state/playState.js new file mode 100644 index 000000000..e442e7e1c --- /dev/null +++ b/space-invaders/js/state/playState.js @@ -0,0 +1,128 @@ +import { CANVAS_WIDTH, PALETTE } from '../constants.js'; +import { drawBackground, drawFrame } from '../render.js'; +import { checkCollisions } from '../collision.js'; +import { Explosion } from '../entities/explosion.js'; + +const RESPAWN_DELAY = 1200; +const WAVE_CLEAR_DELAY = 1600; +const GAME_OVER_DELAY = 900; + +export function createPlayState(game, machine) { + let phase = 'running'; // running | respawning | waveClear | dying + let timer = 0; + let bgTime = 0; + + function beginRespawn() { + phase = 'respawning'; + timer = RESPAWN_DELAY; + } + + function beginGameOverTransition() { + phase = 'dying'; + timer = GAME_OVER_DELAY; + } + + function beginWaveClear() { + phase = 'waveClear'; + timer = WAVE_CLEAR_DELAY; + game.audio.playSound('waveClear'); + } + + return { + enter() { + phase = 'running'; + timer = 0; + }, + exit() { + game.audio.stopUfoLoop(); + }, + update(dt) { + bgTime += dt; + game.input.update(); + + if (phase === 'respawning') { + timer -= dt * 1000; + if (timer <= 0) { + game.player.isAlive = true; + game.player.x = (CANVAS_WIDTH - game.player.width) / 2; + game.player.canFire = true; + phase = 'running'; + } + return; + } + + if (phase === 'waveClear') { + timer -= dt * 1000; + if (timer <= 0) { + game.nextWave(); + phase = 'running'; + } + return; + } + + if (phase === 'dying') { + timer -= dt * 1000; + if (timer <= 0) machine.transition('gameOver'); + return; + } + + // --- normal gameplay update --- + game.player.update(dt, game.input); + game.swarm.update(dt, game.alienBullets); + game.mysteryShip.update(dt); + + for (const b of game.playerBullets) b.update(dt); + for (const b of game.alienBullets) b.update(dt); + for (const e of game.explosions) e.update(dt); + game.explosions = game.explosions.filter((e) => e.isAlive); + + checkCollisions(game); + + // Aliens reaching the player's line is an instant loss condition. + if (game.swarm.aliveCount > 0 && game.swarm.bottomMostY() >= game.player.y) { + game.lives = 0; + game.player.isAlive = false; + game.explosions.push(new Explosion( + game.player.x + game.player.width / 2, + game.player.y + game.player.height / 2, + PALETTE.player, + 0.6, + )); + game.audio.playSound('playerExplosion'); + } + + if (!game.player.isAlive) { + if (game.lives <= 0) beginGameOverTransition(); + else beginRespawn(); + return; + } + + if (game.swarm.isDefeated) { + beginWaveClear(); + } + }, + render(ctx) { + drawBackground(ctx, bgTime * 1000); + drawFrame(ctx); + + for (const bunker of game.bunkers) bunker.render(ctx); + game.swarm.render(ctx); + game.mysteryShip.render(ctx); + game.player.render(ctx); + for (const b of game.playerBullets) b.render(ctx); + for (const b of game.alienBullets) b.render(ctx); + for (const e of game.explosions) e.render(ctx); + + if (phase === 'waveClear') { + ctx.save(); + ctx.textAlign = 'center'; + ctx.fillStyle = PALETTE.goldBright; + ctx.font = '10px Georgia, serif'; + ctx.shadowColor = PALETTE.gold; + ctx.shadowBlur = 5; + ctx.fillText(`WAVE ${game.wave} CLEARED`, CANVAS_WIDTH / 2, 130); + ctx.restore(); + } + }, + }; +} diff --git a/space-invaders/js/state/stateMachine.js b/space-invaders/js/state/stateMachine.js new file mode 100644 index 000000000..74c6405d6 --- /dev/null +++ b/space-invaders/js/state/stateMachine.js @@ -0,0 +1,24 @@ +export class StateMachine { + constructor(states) { + this.states = states; + this.current = null; + } + + start(initial) { + this.transition(initial); + } + + transition(name) { + if (this.current && this.states[this.current].exit) this.states[this.current].exit(); + this.current = name; + if (this.states[this.current].enter) this.states[this.current].enter(); + } + + update(dt) { + this.states[this.current].update(dt); + } + + render(ctx) { + this.states[this.current].render(ctx); + } +} diff --git a/space-invaders/manifest.webmanifest b/space-invaders/manifest.webmanifest new file mode 100644 index 000000000..434067b28 --- /dev/null +++ b/space-invaders/manifest.webmanifest @@ -0,0 +1,17 @@ +{ + "name": "Space Invaders — Nouveau", + "short_name": "SI Nouveau", + "description": "A modern Art Nouveau reimagining of the 1978 arcade classic, with synthesized sound and a gilded, jewel-toned swarm.", + "start_url": ".", + "scope": ".", + "display": "standalone", + "orientation": "portrait-primary", + "background_color": "#120a24", + "theme_color": "#120a24", + "icons": [ + { "src": "icons/icon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" }, + { "src": "icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, + { "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, + { "src": "icons/icon-maskable-512.png", "sizes": "512x512", "type": "image/png", "purpose": "maskable" } + ] +} diff --git a/space-invaders/sw.js b/space-invaders/sw.js new file mode 100644 index 000000000..7ac0ea4a1 --- /dev/null +++ b/space-invaders/sw.js @@ -0,0 +1,55 @@ +const CACHE_NAME = 'space-invaders-nouveau-v1'; + +const ASSET_LIST = [ + './', + './index.html', + './manifest.webmanifest', + './css/style.css', + './js/game.js', + './js/constants.js', + './js/input.js', + './js/audio.js', + './js/render.js', + './js/collision.js', + './js/entities/alien.js', + './js/entities/alienFireController.js', + './js/entities/bullet.js', + './js/entities/bunker.js', + './js/entities/explosion.js', + './js/entities/mysteryShip.js', + './js/entities/player.js', + './js/entities/swarm.js', + './js/state/stateMachine.js', + './js/state/attractState.js', + './js/state/playState.js', + './js/state/gameOverState.js', + './icons/icon.svg', + './icons/icon-192.png', + './icons/icon-512.png', + './icons/icon-maskable-512.png', +]; + +self.addEventListener('install', (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSET_LIST)), + ); + self.skipWaiting(); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil( + caches.keys().then((names) => Promise.all( + names.filter((name) => name !== CACHE_NAME).map((name) => caches.delete(name)), + )), + ); + self.clients.claim(); +}); + +// Cache-first, falling back to network — ideal for a self-contained game +// where every asset is required for the app to function at all. +self.addEventListener('fetch', (event) => { + if (event.request.method !== 'GET') return; + event.respondWith( + caches.match(event.request).then((cached) => cached || fetch(event.request)), + ); +});