# Vector Asteroids — Complete Implementation Blueprint ## 1. Purpose This document is the implementation contract for a polished single-file arcade demo in the style of classic vector Asteroids. The finished deliverable for the game milestone must be a single self-contained `index.html` that can be opened directly from disk and played with no network access, no server, no external files, and no build step. This blueprint specifies: - single-file architecture - game loop - state machine - entity/object model - player movement and combat - asteroid generation and splitting - saucer enemy behavior - collision strategy - scoring, lives, levels, and high score - audio synthesis plan - neon vector visual style - input mapping - responsive canvas behavior - localStorage persistence - implementation sequencing and robustness rules ## 2. Non-negotiable constraints The final implementation must obey these constraints exactly. ### 2.1 Single file The game ships as: ```text index.html ``` No other runtime files are allowed. ### 2.2 Inline CSS and JavaScript only `index.html` may contain: - normal HTML - one or more inline ` ``` Recommended CSS characteristics: - `html, body { margin: 0; width: 100%; height: 100%; overflow: hidden; background: #000; }` - canvas fills the viewport - `touch-action: none` on the canvas - no external fonts; use system monospace or draw text with Canvas The DOM should stay minimal. Game visuals, HUD, menus, controls, title, pause, and game-over screens are rendered on the canvas. ## 4. Internal script organization Even though the implementation is one inline script, it should be organized into clearly separated sections. Recommended order: 1. Constants and tuning tables 2. Math helpers 3. Random helpers 4. Safe storage helpers 5. Input manager 6. Audio manager 7. Canvas/viewport manager 8. Entity factories 9. Game state initialization 10. Update systems 11. Collision systems 12. Rendering helpers 13. Screen/state renderers 14. Main loop 15. Bootstrapping event listeners A single immediately-invoked function expression avoids global leakage: ```js (() => { 'use strict'; const CFG = { /* tuning */ }; // helpers, managers, systems, loop })(); ``` No ES module syntax should be used. ## 5. Coordinate system and timing conventions ### 5.1 Units Use CSS pixels as world units. - `x`, `y`: CSS pixels - `vx`, `vy`: CSS pixels per second - `angle`: radians - `spin`: radians per second - `dt`: seconds - `life`, `cooldown`, `timer`: seconds The world dimensions match the visible canvas CSS size: ```text world.w = viewport CSS width world.h = viewport CSS height ``` This keeps gameplay responsive to any viewport without needing an off-screen world map. ### 5.2 Device pixel ratio The canvas backing store should scale with device pixel ratio, clamped for performance: ```js const dpr = Math.max(1, Math.min(2, window.devicePixelRatio || 1)); canvas.width = Math.floor(cssWidth * dpr); canvas.height = Math.floor(cssHeight * dpr); canvas.style.width = cssWidth + 'px'; canvas.style.height = cssHeight + 'px'; ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ``` A DPR cap of `2` preserves glow quality while avoiding excessive fill cost on high-density screens. ### 5.3 Frame-rate independence Use `requestAnimationFrame`. Compute delta time from the previous frame in seconds. Clamp large deltas after tab switches or breakpoints: ```js let dt = (now - lastNow) / 1000; dt = Math.min(dt, 0.05); ``` For collision robustness, large frame deltas should be split into substeps: ```js const stepCount = Math.min(4, Math.max(1, Math.ceil(dt / (1 / 60)))); const stepDt = dt / stepCount; for (let i = 0; i < stepCount; i++) { updateSimulation(stepDt); } ``` Rendering happens once after all simulation substeps. ## 6. Game state machine Use a simple string or numeric enum. Recommended states: | State | Purpose | |---|---| | `TITLE` | Attract/title screen with animated background, controls, high score, and “press space/enter” prompt | | `READY` | Short pre-wave or respawn countdown; player appears invulnerable | | `PLAYING` | Normal gameplay | | `PAUSED` | Frozen gameplay with pause overlay | | `PLAYER_DYING` | Death explosion and life decrement flow | | `WAVE_COMPLETE` | Short celebration/transition before next level | | `GAME_OVER` | Final score, high score update, restart prompt | ### 6.1 Initial boot On page load: 1. create canvas and input listeners 2. read high score/settings using safe storage 3. initialize starfield 4. enter `TITLE` 5. start `requestAnimationFrame` Audio is not started until the first user gesture. ### 6.2 State transition rules | From | Trigger | To | Actions | |---|---|---|---| | `TITLE` | Space or Enter | `READY` | unlock audio, reset run, spawn level 1 asteroids, reset timers | | `READY` | countdown reaches 0 | `PLAYING` | enable controls, keep short invulnerability | | `PLAYING` | `P` or Escape | `PAUSED` | suspend gameplay updates, keep render animation subtle | | `PAUSED` | `P` or Escape | `PLAYING` | resume | | `PLAYING` | player collision while vulnerable | `PLAYER_DYING` | spawn explosion, decrement lives after death sequence | | `PLAYER_DYING` | death timer ends and lives remain | `READY` | respawn player in safe center, invulnerability | | `PLAYER_DYING` | death timer ends and no lives remain | `GAME_OVER` | save high score | | `PLAYING` | all asteroids destroyed | `WAVE_COMPLETE` | clear enemy bullets, award transition effects | | `WAVE_COMPLETE` | transition timer ends | `READY` | increment level, spawn next wave | | `GAME_OVER` | Space or Enter | `READY` | reset run and start level 1 | | any active state | `M` | same | toggle mute and persist setting | | active gameplay | tab hidden | `PAUSED` | prevent unfair deaths and huge delta jumps | ### 6.3 Attract/title behavior The title screen should not be static. It should show: - parallax stars - drifting asteroids - occasional sparkle particles - glowing game title - high score - controls - flashing “PRESS SPACE / ENTER” - audio prompt such as “SOUND STARTS ON FIRST INPUT” The title simulation can reuse asteroid/star rendering but should not require gameplay collision. ## 7. Top-level game state object Use a central mutable state object. Example shape: ```js const game = { mode: 'TITLE', modeTimer: 0, level: 1, score: 0, highScore: 0, lives: 3, nextExtraLifeScore: 10000, shake: 0, shakeX: 0, shakeY: 0, time: 0, waveAge: 0, saucerTimer: 0, beatTimer: 0, beatIndex: 0, muted: false, world: { w: 0, h: 0, dpr: 1 }, player: null, bullets: [], saucerBullets: [], asteroids: [], saucers: [], particles: [], sparks: [], textPops: [], stars: [] }; ``` Arrays are acceptable because entity counts are small. Object pools are optional but useful for particles and bullets. ## 8. Configuration and tuning All gameplay constants should live in one configuration object so difficulty and feel can be tuned without searching through the code. Recommended starting values: ```js const CFG = { player: { radius: 13, turnRate: 4.9, thrustAccel: 520, dragPer60Hz: 0.992, maxSpeed: 470, spawnInvuln: 2.6, respawnCountdown: 1.2, fireCooldown: 0.16, muzzleOffset: 17, hyperspaceCooldown: 6.5, hyperspaceInvuln: 0.9 }, bullet: { radius: 2.5, speed: 740, life: 0.82, maxPlayerBullets: 5 }, saucerBullet: { radius: 2.8, speed: 390, life: 2.4, maxBullets: 8 }, asteroid: { large: { radiusMin: 50, radiusMax: 76, score: 20, vertsMin: 11, vertsMax: 15 }, medium: { radiusMin: 27, radiusMax: 42, score: 50, vertsMin: 9, vertsMax: 13 }, small: { radiusMin: 15, radiusMax: 24, score: 100, vertsMin: 8, vertsMax: 11 } }, saucer: { firstDelayMin: 12, firstDelayMax: 20, repeatDelayMin: 16, repeatDelayMax: 28, largeScore: 200, smallScore: 1000, largeRadius: 18, smallRadius: 12 }, scoring: { startLives: 3, extraLifeEvery: 10000 }, particles: { max: 420 }, screen: { wrapMargin: 80, shakeDecay: 9 } }; ``` These values are intentionally arcade-fast but still controllable. Final tuning can adjust numbers, but the relationships should remain. ## 9. Entity model Use plain JavaScript objects with explicit fields. Classes are optional; factories are simpler and work well in one file. ### 9.1 Player ```js { x, y, vx, vy, angle, radius, alive, visible, invuln, fireTimer, hyperspaceTimer, thrusting, blinkTimer } ``` Rules: - Position starts at screen center. - Angle starts pointing up: `-Math.PI / 2`. - `invuln` counts down after spawn/hyperspace. - During invulnerability, render ship blinking but still allow movement. - `fireTimer` prevents bullet spam. - `hyperspaceTimer` prevents repeated teleport. ### 9.2 Player bullets ```js { x, y, prevX, prevY, vx, vy, life, radius, owner: 'player', dead } ``` Rules: - Max active player bullets: 5. - Bullets inherit player velocity. - Bullets screen-wrap. - `prevX/prevY` are used for segment collision against asteroid circles. - When a bullet wraps, reset `prevX/prevY` to the new wrapped position to avoid a false long segment across the screen. ### 9.3 Asteroids ```js { id, x, y, vx, vy, angle, spin, size: 'large' | 'medium' | 'small', radius, verts, seed, dead } ``` `verts` is an array of normalized local polygon points: ```js [ { x: 0.8, y: -0.2 }, { x: 0.4, y: 0.7 }, ... ] ``` At render time each vertex is scaled by `radius` and rotated by `angle`. ### 9.4 Saucer ```js { x, y, vx, vy, radius, type: 'large' | 'small', fireTimer, courseTimer, age, dead, exiting } ``` Only one saucer should be active at a time. ### 9.5 Saucer bullets Same shape as player bullets, with: ```js owner: 'saucer' ``` Saucer bullets can hit the player and asteroids. If they destroy asteroids, the player does not receive points. ### 9.6 Particles ```js { x, y, vx, vy, life, maxLife, size, color, kind: 'spark' | 'smoke' | 'fragment' | 'thrust', angle, spin } ``` Particles are visual only and do not collide. Use a cap such as `CFG.particles.max`. If the cap is exceeded, remove oldest particles. ### 9.7 Text pops Optional but recommended for polish: ```js { x, y, text, life, maxLife, color, vy } ``` Used for `+100`, `EXTRA SHIP`, `WAVE 3`, etc. ### 9.8 Stars ```js { x, y, layer, speed, size, twinkle, phase } ``` Stars are generated at boot and repositioned on resize if needed. Use three parallax layers: | Layer | Count formula | Color | Movement | |---|---:|---|---| | far | viewport area / 9000 | dim blue | very slow | | mid | viewport area / 14000 | cyan-white | slow | | near | viewport area / 22000 | bright white/cyan | visible drift | Star count should be clamped for performance. ## 10. Math and utility helpers Recommended helpers: ```js const TAU = Math.PI * 2; function clamp(v, min, max) {} function lerp(a, b, t) {} function rand(min, max) {} function randInt(min, maxInclusive) {} function chance(p) {} function wrap(v, max) {} function wrapPos(entity, w, h, margin) {} function angleToVector(a) {} function distanceSq(a, b) {} function toroidalDelta(a, b, size) {} function toroidalDistanceSq(ax, ay, bx, by, w, h) {} function segmentCircleHit(x1, y1, x2, y2, cx, cy, r) {} ``` ### 10.1 Toroidal distance Because objects wrap, collision should account for screen edges: ```js function toroidalDelta(a, b, size) { let d = a - b; if (d > size / 2) d -= size; if (d < -size / 2) d += size; return d; } ``` Use this for circle checks between wrapping objects. ### 10.2 Segment-circle bullet collision For fast bullets, use previous and current bullet positions: 1. compute vector from previous to current bullet position 2. project circle center onto segment 3. clamp projection to `[0, 1]` 4. test closest point distance against collision radius For wrapping bullets, reset previous position after wrap to prevent false cross-screen segments. ## 11. Input system ### 11.1 Keyboard mapping | Action | Keys | |---|---| | rotate left | `ArrowLeft`, `KeyA` | | rotate right | `ArrowRight`, `KeyD` | | thrust | `ArrowUp`, `KeyW` | | fire | `Space`, `KeyJ`, `KeyK` | | hyperspace | `ArrowDown`, `KeyS`, `ShiftLeft`, `ShiftRight`, `KeyH` | | start/restart | `Enter`, `Space` | | pause | `KeyP`, `Escape` | | mute | `KeyM` | Use `event.code` instead of `event.key` for layout-stable gameplay controls. ### 11.2 Input state shape ```js const input = { left: false, right: false, thrust: false, fire: false, hyperspace: false, pausePressed: false, mutePressed: false, startPressed: false, firePressedThisFrame: false, hyperspacePressedThisFrame: false }; ``` Held controls such as rotation and thrust stay true while the key is down. Single-action controls use edge detection. ### 11.3 Preventing browser interference For game keys, call `preventDefault()` on keydown/keyup unless `ctrlKey`, `metaKey`, or `altKey` is active. This prevents: - Space scrolling - arrow key scrolling - accidental page navigation behavior ### 11.4 Audio unlock Browsers require user activation for Web Audio. The first keydown or pointerdown should call: 1. create `AudioContext` if it does not exist 2. call `audioContext.resume()` 3. start persistent loops such as thrust noise and saucer hum only when needed The game must remain playable if audio initialization fails. ### 11.5 Optional touch controls Touch support is a bonus but should be designed cleanly. Recommended layout: - left third of screen: - drag left/right controls rotation - drag upward or hold upper area controls thrust - right side: - large fire button - smaller hyperspace button - top corner: - pause button Implementation approach: - use Pointer Events on the canvas - track multiple active pointer IDs - draw virtual controls on the canvas only when recent touch input is detected - keep keyboard controls always active Touch controls must not be required for acceptance because keyboard is primary. ## 12. Player mechanics ### 12.1 Rotation During `PLAYING` and active portions of `READY`: ```js if (input.left) player.angle -= CFG.player.turnRate * dt; if (input.right) player.angle += CFG.player.turnRate * dt; ``` If both are held, they cancel out. ### 12.2 Thrust and inertia When thrusting: ```js player.vx += Math.cos(player.angle) * CFG.player.thrustAccel * dt; player.vy += Math.sin(player.angle) * CFG.player.thrustAccel * dt; ``` Apply drag every frame using exponential scaling: ```js const drag = Math.pow(CFG.player.dragPer60Hz, dt * 60); player.vx *= drag; player.vy *= drag; ``` Clamp max speed: ```js const speed = Math.hypot(player.vx, player.vy); if (speed > CFG.player.maxSpeed) { const s = CFG.player.maxSpeed / speed; player.vx *= s; player.vy *= s; } ``` ### 12.3 Engine flame and trail When thrusting: - spawn 2–4 small thrust particles per frame/substep - particles originate behind the ship - particle velocity is opposite the ship’s facing direction plus random spread - flame length flickers using random variation and a sine wave - play/raise continuous thrust audio Visually, the flame should be orange/yellow/white inside the cyan ship outline. ### 12.4 Firing Fire if: - fire input is held or pressed - `fireTimer <= 0` - active player bullets below `CFG.bullet.maxPlayerBullets` - player is alive and visible Spawn bullet at the muzzle: ```js const dir = angleToVector(player.angle); bullet.x = player.x + dir.x * CFG.player.muzzleOffset; bullet.y = player.y + dir.y * CFG.player.muzzleOffset; bullet.vx = player.vx + dir.x * CFG.bullet.speed; bullet.vy = player.vy + dir.y * CFG.bullet.speed; bullet.life = CFG.bullet.life; ``` Set `player.fireTimer = CFG.player.fireCooldown`. Play a short laser sound and spawn a muzzle spark. ### 12.5 Screen wrapping Wrap player, bullets, asteroids, and saucer bullets. Use a margin so entities fully leave one edge before entering the other: ```js if (entity.x < -margin) entity.x = world.w + margin; if (entity.x > world.w + margin) entity.x = -margin; if (entity.y < -margin) entity.y = world.h + margin; if (entity.y > world.h + margin) entity.y = -margin; ``` For the saucer, prefer classic behavior: enter from one side and exit the opposite side. It may use horizontal off-screen margins rather than endless wrapping. ### 12.6 Hyperspace Hyperspace is optional in the original request but should be included for arcade polish. Activation conditions: - player alive - not in `PLAYER_DYING` - `hyperspaceTimer <= 0` - hyperspace key pressed this frame Behavior: 1. spawn flash/ring particles at current position 2. play hyperspace sound 3. choose a random safe position 4. set velocity to a reduced random value or zero 5. set `invuln = CFG.player.hyperspaceInvuln` 6. set `hyperspaceTimer = CFG.player.hyperspaceCooldown` Safe position selection: - attempt up to 24 random positions - reject positions within: - `asteroid.radius + 90` of any asteroid - `saucer.radius + 120` of the saucer - 100 pixels of active saucer bullets - use toroidal distance - if all attempts fail, use screen center with invulnerability Optional risk for classic flavor: - from level 5 onward, a small chance such as 5% can add random spin/velocity - do not instantly kill the player; unfair random death should be avoided in a modern demo ### 12.7 Death and respawn When the player is hit while not invulnerable: 1. set mode to `PLAYER_DYING` 2. hide/disable the player 3. spawn ship explosion particles and line fragments 4. play explosion sound 5. add screen shake 6. after death timer: - decrement lives - if lives remain, respawn at center in `READY` - otherwise enter `GAME_OVER` Respawn safety: - clear saucer bullets - respawn at center - if center is occupied by asteroids, keep `READY` countdown and invulnerability long enough to move - blinking ship indicates invulnerability ## 13. Asteroid system ### 13.1 Sizes Asteroids have three sizes. | Size | Radius | Score | Splits into | |---|---:|---:|---| | large | 50–76 px | 20 | 2 medium | | medium | 27–42 px | 50 | 2 small | | small | 15–24 px | 100 | none | ### 13.2 Wave asteroid count For level `L` starting at 1: ```js largeCount = Math.min(12, 3 + L); ``` This produces: | Level | Large asteroids | |---:|---:| | 1 | 4 | | 2 | 5 | | 3 | 6 | | 4 | 7 | | 5 | 8 | | 9+ | capped at 12 | ### 13.3 Difficulty scaling Asteroid speed multiplier: ```js speedMultiplier = 1 + Math.min(level - 1, 12) * 0.08; ``` Asteroid base speed ranges before multiplier: | Size | Speed range | |---|---:| | large | 32–76 px/s | | medium | 55–122 px/s | | small | 85–168 px/s | Spin range: | Size | Spin range | |---|---:| | large | -0.8 to 0.8 rad/s | | medium | -1.3 to 1.3 rad/s | | small | -1.9 to 1.9 rad/s | ### 13.4 Wave spawn placement At wave start: - spawn only large asteroids - avoid a safe radius around the player center - prefer edges/corners so the opening is fair Recommended method: 1. choose random edge: top, right, bottom, left 2. choose position just inside or just outside that edge 3. reject if toroidal distance from player spawn is below `min(180, min(world.w, world.h) * 0.28)` 4. choose velocity roughly toward the playfield with random angular spread 5. generate a unique polygon For very small screens, reduce the safe radius but never below 110 px. ### 13.5 Jagged polygon generation Each asteroid should have a unique outline. Generate once at asteroid creation: 1. choose vertex count by size 2. divide circle by vertex count 3. jitter each vertex angle slightly 4. jitter each vertex radius between `0.72` and `1.22` 5. store normalized local points 6. avoid self-crossing by preserving angular order Example algorithm: ```js function makeAsteroidVerts(sizeConfig) { const count = randInt(sizeConfig.vertsMin, sizeConfig.vertsMax); const verts = []; for (let i = 0; i < count; i++) { const baseAngle = (i / count) * TAU; const angleJitter = rand(-0.16, 0.16); const radiusJitter = rand(0.72, 1.22); const a = baseAngle + angleJitter; verts.push({ x: Math.cos(a) * radiusJitter, y: Math.sin(a) * radiusJitter }); } return verts; } ``` For extra personality, occasionally add one or two sharper notches by forcing adjacent radius jitter values to differ more strongly. ### 13.6 Splitting rules When a player bullet hits an asteroid: 1. mark bullet dead 2. mark asteroid dead 3. add score according to size 4. spawn particle explosion 5. screen shake based on size 6. if asteroid is large or medium, spawn two child asteroids Child spawn: ```text large -> 2 medium medium -> 2 small small -> none ``` Child position: - near parent center - offset by a random direction times `parent.radius * 0.2` Child velocity: - based partially on parent velocity - add random outward impulse - ensure minimum speed for child size Recommended formula: ```js child.vx = parent.vx * 0.45 + Math.cos(angle) * childSpeed; child.vy = parent.vy * 0.45 + Math.sin(angle) * childSpeed; ``` Use two child angles separated by roughly 120–180 degrees with random variation so the split looks explosive. Children should get fresh unique polygon vertices. ### 13.7 Asteroid destruction by saucer bullets Saucer bullets may destroy asteroids to make the world feel reactive. Rules: - asteroid still splits - explosion still plays - player receives no score - saucer bullet is removed This can occasionally help or harm the player and makes the saucer feel dangerous. ## 14. Saucer enemy ### 14.1 Spawn timing Use one active saucer at most. At each wave start: ```js saucerTimer = random(firstDelayMin, firstDelayMax); ``` After a saucer despawns or is destroyed: ```js saucerTimer = random(repeatDelayMin, repeatDelayMax) * levelFactor; ``` Suggested level factor: ```js levelFactor = Math.max(0.65, 1 - (level - 1) * 0.035); ``` Do not spawn a saucer during: - title screen - game over - pause - player death sequence - wave transition - the first few seconds of level 1 if it would overwhelm the player ### 14.2 Saucer type selection Two types: | Type | Radius | Points | Behavior | |---|---:|---:|---| | large | 18 | 200 | slower, inaccurate shots | | small | 12 | 1000 | faster, more accurate shots | Selection probability for small saucer: ```js smallChance = clamp(0.12 + (level - 2) * 0.08 + score / 120000, 0, 0.72); ``` Level 1 should almost always spawn a large saucer. ### 14.3 Movement Spawn just off the left or right edge. ```text x = -margin or world.w + margin y = random(world.h * 0.18, world.h * 0.72) vx = direction * random(80, 145) vy = random(-45, 45) ``` Every `courseTimer` seconds, adjust `vy`: - random new vertical drift - avoid top/bottom edges - slight bias away from nearby large asteroids - small saucer changes course more often If the saucer travels beyond the opposite side by the margin, it despawns and schedules the next saucer. ### 14.4 Shooting Saucer fires periodically while visible. Fire cadence: | Type | Interval | |---|---:| | large | 1.25–1.9 s | | small | 0.85–1.35 s | Aim modes: Large saucer: - 50% random direction - 50% rough aim at player with error ±30–45 degrees Small saucer: - aim at predicted player position - error shrinks with level - minimum error around ±5 degrees so it remains dodgeable Prediction: ```js const travelTime = distanceToPlayer / CFG.saucerBullet.speed; targetX = player.x + player.vx * travelTime * 0.65; targetY = player.y + player.vy * travelTime * 0.65; ``` Use toroidal distance when estimating player direction if player and saucer are near opposite edges. Saucer bullets: - screen-wrap - limited lifetime - collide with player and asteroids - render as magenta/orange glowing dots or short line segments ### 14.5 Saucer audio While a saucer is active, play a looped siren/hum. Large saucer: - lower pitch - slower wobble Small saucer: - higher pitch - faster wobble Fade in over 0.15 seconds and fade out when destroyed/despawned. ### 14.6 Saucer destruction Player bullet hitting saucer: 1. mark bullet and saucer dead 2. add score based on saucer type 3. spawn bright magenta/cyan explosion 4. add text pop 5. add screen shake 6. stop saucer hum 7. schedule next saucer Player colliding with saucer: - both player and saucer explode - player loses life unless invulnerable - player should receive saucer points only if a player bullet caused the kill, not for collision ## 15. Collision strategy ### 15.1 Overall approach Entity counts are small, so simple pairwise checks are sufficient and robust: - player bullets × asteroids - player bullets × saucers - saucer bullets × player - saucer bullets × asteroids - asteroids × player - saucers × player No spatial grid is necessary for this scale. ### 15.2 Collision primitives Use circles for broad and final collision. The game is vector-outline styled, but circle collision is appropriate for arcade fairness. Collision radii: | Entity | Radius | |---|---:| | player | 13 | | player bullet | 2.5 | | saucer bullet | 2.8 | | asteroid | entity radius | | saucer | 12 or 18 | For slightly more accurate player collision, optionally test three small circles: - nose - left wing - right wing However, one fair radius around the ship center is acceptable and less error-prone. ### 15.3 Bullet tunneling protection Use segment-circle collision for bullets: ```text bullet previous position -> bullet current position against asteroid/saucer circle ``` Expand target radius by bullet radius. For toroidal wrapping, there are two acceptable approaches: 1. Reset bullet previous position after wrapping and rely on substeps. 2. Test segment against nearby wrapped copies of targets. The recommended simpler approach is: - substep simulation - reset previous bullet point on wrap - use toroidal circle checks for current position as a fallback ### 15.4 Collision ordering Within each simulation substep: 1. update movement and timers 2. wrap entities 3. player bullets vs asteroids 4. player bullets vs saucer 5. saucer bullets vs asteroids 6. saucer bullets vs player 7. asteroids vs player 8. saucer vs player 9. remove dead entities 10. check wave clear This ordering gives player bullets priority and prevents one entity from scoring multiple times in one frame. ### 15.5 Dead flags and cleanup Do not splice arrays while deeply nested unless carefully controlled. Prefer: ```js entity.dead = true; ``` Then compact arrays after collision passes: ```js array = array.filter(e => !e.dead); ``` Because entity counts are low, filtering is acceptable. ### 15.6 Invulnerability If `player.invuln > 0`: - player does not die from collisions - collisions may still produce small shield sparks for feedback - ship blinks visually Player bullets should still function during invulnerability. ### 15.7 Screen shake values Suggested shake intensity: | Event | Shake | |---|---:| | small asteroid destroyed | 1.5 | | medium asteroid destroyed | 3 | | large asteroid destroyed | 5 | | player death | 8 | | saucer destroyed | 5 | | wave complete | 2 | Shake decays exponentially: ```js game.shake = Math.max(0, game.shake - CFG.screen.shakeDecay * dt); ``` During render, offset the world by random values in `[-shake, shake]`. ## 16. Scoring, lives, waves, and progression ### 16.1 Score table | Target | Score | |---|---:| | large asteroid | 20 | | medium asteroid | 50 | | small asteroid | 100 | | large saucer | 200 | | small saucer | 1000 | ### 16.2 Lives Start with: ```js lives = 3 ``` HUD should show lives as small ship icons, not just a number, for arcade feel. If space is limited, show icons for up to five lives and a numeric `xN` beyond that. ### 16.3 Extra life Award one extra life every 10,000 points. Use a rolling threshold: ```js nextExtraLifeScore = 10000; function addScore(points) { score += points; while (score >= nextExtraLifeScore) { lives++; nextExtraLifeScore += 10000; spawnExtraLifeEffect(); playExtraLifeSound(); } } ``` This handles large score jumps correctly. ### 16.4 High score High score updates when: - score exceeds stored high score during play - game over occurs Persist immediately after a new high score is reached, using safe storage wrappers. ### 16.5 Wave completion Wave is complete when: ```js asteroids.length === 0 ``` On wave completion: - enter `WAVE_COMPLETE` - clear saucer bullets - despawn active saucer with a warp/flee effect or remove it immediately - show `WAVE CLEAR` - play short chime - after 1.4–2.0 seconds, increment level and spawn next wave ### 16.6 Difficulty ramp As levels rise: - more large asteroids - faster asteroid speeds - shorter saucer delays - higher chance of small saucer - small saucer aim improves slightly Avoid making the game impossible too quickly. The demo should remain fun for casual players through at least waves 1–5. ## 17. Rendering and visual style guide ### 17.1 Overall aesthetic Visual direction: - black background - neon vector outlines - cyan/white player ship - blue-white asteroids - magenta/red saucer - yellow/orange thrust and explosions - subtle glow/bloom - crisp line art - no raster images The game should look like an idealized arcade vector monitor rather than a flat debug canvas. ### 17.2 Render order Render in this order: 1. background black fade/clear 2. parallax starfield 3. distant decorative particles 4. screen shake transform begins 5. asteroids 6. saucers 7. bullets 8. player 9. explosions/sparks/thrust particles 10. text pops 11. screen shake transform ends 12. HUD 13. state overlays such as title, pause, game over 14. optional scanline/vignette overlay ### 17.3 Background clear Two viable styles: Strict crisp vector: ```js ctx.fillStyle = '#000'; ctx.fillRect(0, 0, w, h); ``` Subtle persistence: ```js ctx.fillStyle = 'rgba(0, 0, 0, 0.42)'; ctx.fillRect(0, 0, w, h); ``` For gameplay readability, use full clear or very mild persistence. Heavy trails make collision hard to read. ### 17.4 Glow helper Use repeated strokes and `shadowBlur`. Recommended helper behavior: 1. save context 2. set `lineJoin = 'round'` 3. set `lineCap = 'round'` 4. draw wide translucent glow stroke 5. draw medium glow stroke 6. draw crisp bright core stroke 7. restore context Example style parameters: | Object | Core | Glow | |---|---|---| | player | `#d9ffff` | `#00eaff` | | asteroid | `#d8f7ff` | `#3dbbff` | | saucer | `#ffd6ff` | `#ff3df2` | | player bullet | `#ffffff` | `#5cf7ff` | | saucer bullet | `#ffe0a0` | `#ff4d7d` | | thrust | `#fff2aa` | `#ff8a00` | | explosion | mixed | mixed | ### 17.5 Asteroid rendering For each asteroid: 1. translate to asteroid position 2. rotate by `angle` 3. begin path 4. move through local vertices scaled by `radius` 5. close path 6. stroke with glow 7. optionally draw one or two internal fracture lines for large asteroids, but keep it vector-clean Render wrapped duplicates near edges so objects do not pop. For any entity near an edge, draw copies shifted by `±world.w` and/or `±world.h`. ### 17.6 Player rendering Ship shape points in local coordinates, facing positive X if using `angle` as usual: ```text nose: (16, 0) left: (-11, -9) rear: (-6, 0) right: (-11, 9) ``` Draw as connected lines: ```text nose -> left -> rear -> right -> nose ``` When thrusting, draw flame from rear: - base at `(-10, 0)` - flicker endpoint around `(-24 to -34, 0)` - side jitter `±5` If invulnerable, blink: ```js visible = Math.floor(invuln * 10) % 2 === 0 ``` But keep HUD and particles visible. ### 17.7 Saucer rendering Large/small saucer is a layered vector shape: - central dome arc/line - flat elliptical body approximated with line segments - underside line - small blinking lights Use magenta/pink glow. The small saucer should be visibly smaller and slightly brighter. ### 17.8 Bullets Render bullets as short glowing streaks rather than plain circles: - use velocity direction - draw from current position back 5–9 pixels - player bullets cyan-white - saucer bullets orange/magenta ### 17.9 Particles Particle types: | Type | Visual | |---|---| | spark | small glowing line segment | | fragment | short rotating vector shard | | thrust | fading orange/yellow dot/line | | smoke | dim blue/gray fading dot | | hyperspace | expanding cyan ring shards | Particles fade by life fraction: ```js alpha = particle.life / particle.maxLife ``` Use additive-looking colors via alpha and glow. Canvas 2D does not need actual additive blending, but `globalCompositeOperation = 'lighter'` can be used briefly for particles and restored after. ### 17.10 HUD HUD elements: - score top left - high score top center or top right - level indicator - lives as ship icons - mute indicator if muted - pause text when paused Use Canvas text with a monospace system font: ```js ctx.font = '16px ui-monospace, SFMono-Regular, Menlo, Consolas, monospace'; ``` This does not load external fonts. Add glow by drawing text twice: 1. blurred translucent color 2. crisp white/cyan core ### 17.11 Title screen Title screen must show: - large glowing `VECTOR ASTEROIDS` - subtitle such as `SINGLE FILE ARCADE DEMO` - controls table - high score - press start prompt - animated starfield and drifting asteroids behind text Text should be centered and responsive. On small screens, reduce font sizes. ### 17.12 Game-over screen Game over overlay: - dark translucent panel or full-screen dim - `GAME OVER` - final score - high score, with `NEW HIGH SCORE` if applicable - `PRESS SPACE / ENTER TO RESTART` - controls reminder for pause/mute ### 17.13 Pause screen Pause overlay: - freeze gameplay simulation - optionally continue very subtle star twinkle - draw `PAUSED` - draw `P / ESC TO RESUME` - keep score/lives visible ## 18. Starfield and parallax The camera in Asteroids is fixed, but parallax can still add depth. Use star layers that drift slowly and respond slightly opposite the player velocity: ```js star.x += (-player.vx * layer.parallax + layer.driftX) * dt; star.y += (-player.vy * layer.parallax + layer.driftY) * dt; ``` Layer examples: | Layer | Parallax | Drift | Size | |---|---:|---:|---:| | far | 0.006 | tiny | 0.7 px | | mid | 0.014 | slow | 1.0 px | | near | 0.026 | visible | 1.4 px | Stars wrap at screen edges. Twinkle: ```js alpha = baseAlpha + Math.sin(game.time * twinkle + phase) * 0.15; ``` Do not overdo twinkle; the playfield should remain readable. ## 19. Audio synthesis plan ### 19.1 Audio lifecycle Because browser autoplay policies block audio until user interaction: - do not create or resume loud audio on page load - first keydown/pointerdown unlocks audio - if audio fails, continue silently - mute toggle must work even before audio is unlocked Audio manager state: ```js const audio = { ctx: null, master: null, sfx: null, music: null, unlocked: false, muted: false, thrustNode: null, saucerNode: null }; ``` ### 19.2 Master graph Recommended graph: ```text sources -> per-effect gains -> sfxGain -> masterGain -> destination beat sources -> musicGain -> masterGain -> destination ``` Gain levels: - master: 0.65 when unmuted, 0 when muted - sfx: 0.9 - music/beat: 0.25 Use short envelopes to avoid clicks. ### 19.3 Noise generation For explosion and thrust, create noise buffers at runtime: ```js const buffer = audioContext.createBuffer(1, sampleRate * duration, sampleRate); const data = buffer.getChannelData(0); for (let i = 0; i < data.length; i++) { data[i] = Math.random() * 2 - 1; } ``` This is generated code/data, not an external asset. ### 19.4 Sound recipes #### Player fire - oscillator type: `square` or `sawtooth` - frequency sweep: ~880 Hz down to ~260 Hz over 0.07 s - gain envelope: quick attack, rapid decay - optional highpass filter - stereo pan slightly based on x-position Effect length: 0.08–0.10 s. #### Thrust - looping noise buffer - lowpass filter around 700–1200 Hz - gain rises while thrust key is held and falls when released - optional low oscillator at 55–90 Hz for rumble - subtle random filter modulation Must not restart every frame. Keep a persistent node or recreate only on thrust start/end. #### Asteroid explosion Large asteroid: - noise burst 0.45–0.65 s - lowpass filter sweeps downward - low sine boom 80 Hz down to 35 Hz - many visual particles Medium/small: - shorter noise burst - less low boom - shorter decay #### Player death - stronger explosion - descending oscillator - noise burst - brief silence/dip in beat if implemented #### Saucer hum - two oscillators or one oscillator with periodic frequency changes - large saucer: 260/390 Hz alternating - small saucer: 520/780 Hz alternating - tremolo via gain modulation - fade in/out #### Saucer shot - sharper, more aggressive than player shot - oscillator 420 Hz to 150 Hz - magenta/orange visual spark - duration 0.12 s #### Extra life - three ascending tones - example frequencies: 523, 659, 784 Hz - short bell-like envelope #### Hyperspace - fast upward or downward sweep - optional stereo pan swirl - short noise sparkle #### Beat Classic Asteroids-style heartbeat is recommended. Behavior: - alternate two low blips - interval decreases as the wave becomes more dangerous or asteroids are cleared - muted in title unless title audio is desired after unlock - pause beat during `PAUSED` and `GAME_OVER` Beat interval formula: ```js const remaining = asteroids.length; const maxExpected = Math.max(1, waveStartingAsteroidSlots); const pressure = 1 - clamp(remaining / maxExpected, 0, 1); const interval = lerp(0.82, 0.28, pressure); ``` This makes the beat accelerate as the player clears the wave, matching classic tension. ### 19.5 Audio robustness Every audio method should start with: ```js if (!audio.unlocked || audio.muted || !audio.ctx) return; ``` When creating nodes: - connect all nodes - schedule envelopes using `audio.ctx.currentTime` - stop oscillators/buffer sources after their envelope ends - disconnect on `ended` where practical - avoid unbounded node creation loops ## 20. Persistence plan ### 20.1 Keys Use versioned namespaced keys: ```js const STORAGE_KEYS = { highScore: 'vectorAsteroids.highScore.v1', settings: 'vectorAsteroids.settings.v1' }; ``` ### 20.2 High score format Store high score as a base-10 integer string. Example: ```text 12540 ``` Read behavior: - parse integer - reject `NaN`, negative, and non-finite values - fallback to 0 Write behavior: - write only if score is greater than current stored high score - wrap in `try/catch` ### 20.3 Settings format Store JSON: ```json { "muted": false, "touchControls": "auto" } ``` Read behavior: - parse JSON safely - validate each field - fallback to defaults ### 20.4 Safe wrappers All storage calls must tolerate errors: ```js function safeGet(key) { try { return window.localStorage.getItem(key); } catch (err) { return null; } } function safeSet(key, value) { try { window.localStorage.setItem(key, value); return true; } catch (err) { return false; } } ``` This is required for file mode, sandboxed browsers, private browsing, and storage-disabled contexts. ## 21. Responsive canvas behavior ### 21.1 Resize events On resize: 1. measure viewport CSS size 2. update canvas backing store using DPR 3. update world dimensions 4. regenerate star counts or reposition stars if needed 5. clamp or wrap existing entities into the new world 6. keep player visible Use `window.innerWidth` and `window.innerHeight`. Minimum playability target: - 320 × 480 portrait - 480 × 320 landscape - desktop full-screen For very small screens: - reduce HUD font sizes - reduce title text - keep collision/game units unchanged where possible - optionally lower star count ### 21.2 Entity handling on resize When dimensions change: - player: clamp into bounds or center if invalid - asteroids: wrap into new bounds - bullets: wrap or remove if outside by too much - particles: leave and expire naturally - saucer: allow it to continue, but clamp y away from HUD edges ### 21.3 Safe areas For mobile browsers with notches, CSS should use full viewport. Since all graphics are canvas, avoid placing HUD text flush to extreme corners. Use HUD padding: ```js const hudPad = Math.max(14, Math.min(world.w, world.h) * 0.025); ``` ## 22. Game loop details ### 22.1 Frame function Recommended structure: ```js function frame(nowMs) { requestAnimationFrame(frame); const now = nowMs / 1000; let dt = lastTime ? now - lastTime : 1 / 60; lastTime = now; dt = Math.min(dt, 0.05); input.beginFrame(); updateAlways(dt); if (game.mode !== 'PAUSED') { const steps = Math.min(4, Math.max(1, Math.ceil(dt / (1 / 60)))); const stepDt = dt / steps; for (let i = 0; i < steps; i++) { updateGame(stepDt); } } else { updatePausedVisuals(dt); } render(); input.endFrame(); } ``` `updateAlways` handles: - title star drift - input edge transitions - mute toggles - audio master gain - resize flag handling `updateGame` handles state-specific simulation. ### 22.2 State update dispatch ```js switch (game.mode) { case 'TITLE': updateTitle(dt); break; case 'READY': updateReady(dt); break; case 'PLAYING': updatePlaying(dt); break; case 'PLAYER_DYING': updateDeath(dt); break; case 'WAVE_COMPLETE': updateWaveComplete(dt); break; case 'GAME_OVER': updateGameOver(dt); break; } ``` `PAUSED` should not advance gameplay timers, bullets, asteroids, saucer shots, or collisions. ### 22.3 Update order during play For `PLAYING`: 1. increment global time/wave age 2. handle pause/mute/start edge events 3. update player controls 4. update bullets 5. update asteroids 6. update saucer spawn timer and saucer AI 7. update saucer bullets 8. update particles/text pops 9. run collisions 10. compact dead arrays 11. update beat/audio pressure 12. test wave clear ## 23. Entity update details ### 23.1 Bullets For each bullet: ```js bullet.prevX = bullet.x; bullet.prevY = bullet.y; bullet.x += bullet.vx * dt; bullet.y += bullet.vy * dt; bullet.life -= dt; wrapBullet(bullet); if (bullet.life <= 0) bullet.dead = true; ``` Player bullet lifetime should be short enough to require aiming but long enough to feel good. ### 23.2 Asteroids ```js asteroid.x += asteroid.vx * dt; asteroid.y += asteroid.vy * dt; asteroid.angle += asteroid.spin * dt; wrapPos(asteroid, world.w, world.h, CFG.screen.wrapMargin); ``` ### 23.3 Particles ```js particle.x += particle.vx * dt; particle.y += particle.vy * dt; particle.vx *= Math.pow(0.98, dt * 60); particle.vy *= Math.pow(0.98, dt * 60); particle.angle += particle.spin * dt; particle.life -= dt; if (particle.life <= 0) particle.dead = true; ``` Particles can wrap for style or fade at edges. Thrust/explosion particles usually look better if they do not wrap and simply expire. ### 23.4 Saucer ```js saucer.age += dt; saucer.courseTimer -= dt; saucer.fireTimer -= dt; if (saucer.courseTimer <= 0) chooseNewSaucerCourse(saucer); saucer.x += saucer.vx * dt; saucer.y += saucer.vy * dt; bounceOrBiasSaucerY(saucer); if (saucer.fireTimer <= 0) saucerFire(saucer); if (saucerExitedScreen(saucer)) saucer.dead = true; ``` ## 24. Juice and feedback systems ### 24.1 Explosions Asteroid explosion particles: | Asteroid size | Particle count | Colors | Shake | |---|---:|---|---:| | large | 34–48 | cyan, blue, white | 5 | | medium | 20–32 | cyan, white | 3 | | small | 10–18 | white, cyan | 1.5 | Saucer explosion: - 36–56 particles - magenta, pink, cyan, white - a few ring shards - shake 5 Player explosion: - 45–70 particles - cyan, white, orange - triangular ship fragments - shake 8 ### 24.2 Bullet impact sparks When a bullet hits: - spawn 6–12 tiny sparks - direct sparks roughly opposite bullet velocity - lifetime 0.12–0.28 s - draw as bright line flecks ### 24.3 Screen flash Use very subtle full-screen flash for: - hyperspace - player death - wave complete - extra life Implementation: ```js game.flash = Math.max(game.flash, intensity); game.flash = Math.max(0, game.flash - dt * decay); ``` Render as translucent colored overlay. ### 24.4 Text pops Text pops add arcade reward clarity: - asteroid points are optional because many hits can clutter - saucer points should pop - extra life should pop - wave clear should pop Use upward drift and fade. ## 25. Acceptance-oriented implementation sequence A competent engineer should build in this order to keep the single file stable: 1. HTML shell, CSS, full-screen canvas 2. resize manager with DPR scaling 3. main loop with delta clamp 4. keyboard input and state transitions 5. starfield/title render 6. player movement, wrapping, thrust rendering 7. bullets and firing sound 8. asteroid generation/rendering/wrapping 9. bullet-asteroid collisions and splitting 10. scoring/lives/HUD 11. player collisions/death/respawn 12. waves and difficulty scaling 13. saucer movement/rendering 14. saucer shooting/collisions/scoring 15. particles, sparks, shake, flash, text pops 16. audio manager and all synthesized effects 17. high score/settings persistence 18. pause/game-over polish 19. responsive/mobile/touch polish if included 20. final static-host/file audit At each stage, keep the game runnable by opening `index.html` directly. ## 26. Robustness requirements The finished implementation should handle: - reloads - browser back/forward cache - tab hidden/visible changes - audio context suspended/resumed - localStorage blocked - very small and very large viewports - high-DPI displays - long pauses without physics explosions - rapid key repeat - simultaneous opposite direction keys - many particles without runaway memory use ## 27. Source cleanliness rules for the final `index.html` The final source should not contain unused systems, debug-only controls, or dead code that suggests incomplete implementation. Allowed comments are fine, but avoid shipping: - references to external assets - commented-out CDN links - test fetches - unused module loader code - unfinished debug menus - placeholder art or placeholder sound paths The implementation should be self-contained by design, not merely by accident. ## 28. Definition of architecture completeness This blueprint is complete when the final implementer can answer all of these from the document without inventing systems: - how the game starts, pauses, resumes, ends, and restarts - how entities are represented and updated - how player physics work - how bullets behave and collide - how asteroid shapes are generated - how asteroids split - how waves scale - how the saucer spawns, moves, aims, shoots, scores, and despawns - how lives, extra lives, score, and high score work - how audio is synthesized - how vector glow rendering is achieved - how input maps to actions - how canvas resizing and DPR are handled - how localStorage is used safely - how to verify the final game is truly single-file and network-free