Files
awesome-copilot/extensions/arcade-canvas/game/scenes/CosmicRocks.js
T
Dan Wahlin 2f9d85eef8 Add Agent Arcade canvas extension (#2031)
* Add Agent Arcade canvas extension

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Refine Agent Arcade canvas behavior

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update Agent Arcade canvas credits

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Update Agent Arcade canvas catalog anchor

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* Ignoring the minified file

* Configure codespell to skip minified Phaser file

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Aaron Powell <me@aaron-powell.com>
2026-06-17 19:29:18 +10:00

697 lines
25 KiB
JavaScript

// CosmicRocks — Asteroids-style space shooter.
// Ship rotates and thrusts through space, destroying asteroids that split
// into smaller fragments. Vector-style graphics drawn with Phaser Graphics.
import { BaseScene, W, H } from './BaseScene.js';
/* ------------------------------------------------------------------ */
/* Constants — SCALE/SHIP_SIZE recalculated in create() */
/* ------------------------------------------------------------------ */
let SCALE = Math.min(W / 1920, H / 1080);
let SHIP_SIZE = 20 * Math.max(SCALE, 0.6);
const ROTATE_SPEED = 4; // rad/s
const THRUST = 400; // px/s²
const FRICTION = 0.98;
const BULLET_SPEED = 600;
const BULLET_LIFE = 3000; // ms
const MAX_BULLETS = 4;
const INITIAL_ASTEROIDS = 5;
const INVINCIBLE_TIME = 2000; // ms
const RESPAWN_DELAY = 800; // ms before respawn
const ASTEROID_SIZES = [
{ radius: [40, 60], speed: [40, 80], score: 20 }, // large (size index 0)
{ radius: [25, 40], speed: [60, 120], score: 50 }, // medium (size index 1)
{ radius: [12, 20], speed: [80, 160], score: 100 }, // small (size index 2)
];
const BULLET_COLORS = [0x00ff88, 0xff8800, 0x00ccff];
/* ------------------------------------------------------------------ */
/* Scene */
/* ------------------------------------------------------------------ */
export class CosmicRocksScene extends BaseScene {
/* ship state */
shipGfx;
shipX = 0;
shipY = 0;
shipVx = 0;
shipVy = 0;
shipAngle = -Math.PI / 2; // pointing up
thrustGfx;
/* game objects */
asteroids = [];
bullets = [];
stars = [];
/* UFO */
ufo = null;
ufoBullets = [];
ufoTimer = 0;
/* game state */
wave = 0;
invincibleTimer = 0;
respawnTimer = 0;
shipAlive = true;
gameOver = false;
waveDelay = 0;
/* input */
cursors;
spaceKey;
spaceWasDown = false;
constructor() { super('cosmic-rocks'); }
get displayName() { return 'Cosmic Rocks'; }
getDescription() {
return 'Survive the asteroid field. Shoot rocks to break them apart!';
}
getControls() {
return [
{ key: '← →', action: 'Rotate' },
{ key: '↑', action: 'Thrust' },
{ key: 'SPACE', action: 'Fire' },
];
}
/* ================================================================
LIFECYCLE
================================================================ */
preload() {
this.load.audio('sfx_laser', '../assets/cosmic-rocks/sounds/sfx_laser1.ogg');
this.load.audio('sfx_zap', '../assets/cosmic-rocks/sounds/sfx_explosion.ogg');
this.load.audio('sfx_lose', '../assets/cosmic-rocks/sounds/sfx_lose.ogg');
this.load.audio('sfx_twoTone', '../assets/cosmic-rocks/sounds/sfx_twoTone.ogg');
}
create() {
this.initBase();
// Recalculate screen-dependent constants
SCALE = Math.min(W / 1920, H / 1080);
SHIP_SIZE = 20 * Math.max(SCALE, 0.6);
this.score = 0;
this.lives = 3;
this.wave = 0;
this.shipX = W / 2;
this.shipY = H / 2;
this.shipVx = 0;
this.shipVy = 0;
this.shipAngle = -Math.PI / 2;
this.invincibleTimer = 0;
this.respawnTimer = 0;
this.shipAlive = true;
this.gameOver = false;
this.waveDelay = 0;
this.asteroids = [];
this.bullets = [];
this.stars = [];
this.activeEmitters = [];
this.ufo = null;
this.ufoBullets = [];
this.ufoTimer = 15000 + Math.random() * 10000;
this.ensureSparkTexture();
this.stars = this.createStarfield([
{ count: 40, speed: 15, size: 1, alpha: 0.25 },
{ count: 25, speed: 30, size: 1.5, alpha: 0.35 },
{ count: 15, speed: 55, size: 2, alpha: 0.45 },
]);
this.createShip();
this.cursors = this.input.keyboard.createCursorKeys();
this.spaceKey = this.input.keyboard.addKey('SPACE');
this.spaceWasDown = false;
this.syncLivesToHUD();
this.syncScoreToHUD();
this.loadHighScore();
this.startWithReadyScreen(() => this.startWave());
}
update(_t, dtMs) {
if (this.gameOver || !this.cursors)
return;
const dt = Math.min(dtMs, 33);
const dtSec = dt / 1000;
this.updateStarfield(this.stars, dt);
if (this.respawnTimer > 0) {
this.respawnTimer -= dt;
if (this.respawnTimer <= 0)
this.respawnShip();
}
if (this.shipAlive) {
this.updateShipInput(dtSec);
this.updateShipPhysics(dtSec);
this.drawShip();
}
this.updateBullets(dtSec);
this.updateAsteroids(dtSec);
this.updateUfo(dt, dtSec);
this.checkCollisions();
this.checkUfoCollisions();
if (this.waveDelay > 0) {
this.waveDelay -= dt;
if (this.waveDelay <= 0 && this.asteroids.length === 0)
this.startWave();
}
if (this.invincibleTimer > 0) {
this.invincibleTimer -= dt;
if (this.shipGfx) {
this.shipGfx.setAlpha(Math.sin(performance.now() / 80) > 0 ? 1 : 0.2);
}
}
else if (this.shipGfx) {
this.shipGfx.setAlpha(1);
}
}
/* ================================================================
SHIP
================================================================ */
createShip() {
this.shipGfx = this.add.graphics().setDepth(10);
this.thrustGfx = this.add.graphics().setDepth(9);
this.drawShip();
}
updateShipInput(dtSec) {
if (!this.cursors)
return;
if (this.cursors.left.isDown)
this.shipAngle -= ROTATE_SPEED * dtSec;
if (this.cursors.right.isDown)
this.shipAngle += ROTATE_SPEED * dtSec;
if (this.cursors.up.isDown) {
this.shipVx += Math.cos(this.shipAngle) * THRUST * dtSec;
this.shipVy += Math.sin(this.shipAngle) * THRUST * dtSec;
}
// Fire
const spaceDown = this.spaceKey.isDown;
if (spaceDown && !this.spaceWasDown && this.bullets.length < MAX_BULLETS) {
this.fireBullet();
}
this.spaceWasDown = spaceDown;
}
updateShipPhysics(dtSec) {
// Friction (time-based)
const friction = Math.pow(FRICTION, dtSec / (1 / 60));
this.shipVx *= friction;
this.shipVy *= friction;
this.shipX += this.shipVx * dtSec;
this.shipY += this.shipVy * dtSec;
// Screen wrap
if (this.shipX < -SHIP_SIZE)
this.shipX = W + SHIP_SIZE;
else if (this.shipX > W + SHIP_SIZE)
this.shipX = -SHIP_SIZE;
if (this.shipY < -SHIP_SIZE)
this.shipY = H + SHIP_SIZE;
else if (this.shipY > H + SHIP_SIZE)
this.shipY = -SHIP_SIZE;
}
drawShip() {
const g = this.shipGfx;
g.clear();
g.setPosition(this.shipX, this.shipY);
const cos = Math.cos(this.shipAngle);
const sin = Math.sin(this.shipAngle);
const s = SHIP_SIZE;
// Triangle ship
const nose = { x: cos * s, y: sin * s };
const leftWing = { x: Math.cos(this.shipAngle + 2.4) * s * 0.85, y: Math.sin(this.shipAngle + 2.4) * s * 0.85 };
const rightWing = { x: Math.cos(this.shipAngle - 2.4) * s * 0.85, y: Math.sin(this.shipAngle - 2.4) * s * 0.85 };
// Dark shadow backdrop for visibility on light backgrounds
g.lineStyle(6, 0x000000, 0.5);
g.beginPath();
g.moveTo(nose.x, nose.y);
g.lineTo(leftWing.x, leftWing.y);
g.lineTo(rightWing.x, rightWing.y);
g.closePath();
g.strokePath();
// Outer glow (soft cyan)
g.lineStyle(4, 0x00ffff, 0.2);
g.beginPath();
g.moveTo(nose.x, nose.y);
g.lineTo(leftWing.x, leftWing.y);
g.lineTo(rightWing.x, rightWing.y);
g.closePath();
g.strokePath();
// Solid ship outline (bright cyan)
g.lineStyle(2.5, 0x00ffff, 1);
g.beginPath();
g.moveTo(nose.x, nose.y);
g.lineTo(leftWing.x, leftWing.y);
g.lineTo(rightWing.x, rightWing.y);
g.closePath();
g.strokePath();
// Thrust flame
const tg = this.thrustGfx;
tg.clear();
if (this.cursors && this.cursors.up.isDown) {
tg.setPosition(this.shipX, this.shipY);
const tailLen = s * (0.6 + Math.random() * 0.4);
const tailX = -cos * tailLen;
const tailY = -sin * tailLen;
const spread = 0.4;
const tl = { x: Math.cos(this.shipAngle + Math.PI - spread) * s * 0.35, y: Math.sin(this.shipAngle + Math.PI - spread) * s * 0.35 };
const tr = { x: Math.cos(this.shipAngle + Math.PI + spread) * s * 0.35, y: Math.sin(this.shipAngle + Math.PI + spread) * s * 0.35 };
// Dark shadow for thrust
tg.lineStyle(5, 0x000000, 0.3);
tg.beginPath();
tg.moveTo(tl.x, tl.y);
tg.lineTo(tailX, tailY);
tg.lineTo(tr.x, tr.y);
tg.strokePath();
tg.lineStyle(3, 0xff8800, 0.25);
tg.beginPath();
tg.moveTo(tl.x, tl.y);
tg.lineTo(tailX, tailY);
tg.lineTo(tr.x, tr.y);
tg.strokePath();
tg.lineStyle(2.5, 0xff8800, 0.9);
tg.beginPath();
tg.moveTo(tl.x, tl.y);
tg.lineTo(tailX, tailY);
tg.lineTo(tr.x, tr.y);
tg.strokePath();
}
}
respawnShip() {
this.shipX = W / 2;
this.shipY = H / 2;
this.shipVx = 0;
this.shipVy = 0;
this.shipAngle = -Math.PI / 2;
this.shipAlive = true;
this.invincibleTimer = INVINCIBLE_TIME;
if (this.shipGfx)
this.shipGfx.setVisible(true);
if (this.thrustGfx)
this.thrustGfx.setVisible(true);
}
/* ================================================================
BULLETS
================================================================ */
fireBullet() {
this.sound.play('sfx_laser', { volume: 0.3 });
const color = BULLET_COLORS[Math.floor(Math.random() * BULLET_COLORS.length)];
const gfx = this.add.graphics().setDepth(8);
// Dark backdrop
gfx.fillStyle(0x000000, 0.5);
gfx.fillCircle(0, 0, 10);
// Glow
gfx.fillStyle(color, 0.3);
gfx.fillCircle(0, 0, 8);
// Solid center
gfx.fillStyle(color, 1);
gfx.fillCircle(0, 0, 4);
const bx = this.shipX + Math.cos(this.shipAngle) * SHIP_SIZE;
const by = this.shipY + Math.sin(this.shipAngle) * SHIP_SIZE;
gfx.setPosition(bx, by);
this.bullets.push({
gfx,
x: bx, y: by,
vx: Math.cos(this.shipAngle) * BULLET_SPEED,
vy: Math.sin(this.shipAngle) * BULLET_SPEED,
life: BULLET_LIFE,
color,
});
}
updateBullets(dtSec) {
for (let i = this.bullets.length - 1; i >= 0; i--) {
const b = this.bullets[i];
b.x += b.vx * dtSec;
b.y += b.vy * dtSec;
b.life -= dtSec * 1000;
b.gfx.setPosition(b.x, b.y);
// Destroy bullet when it leaves the screen or expires
if (b.life <= 0 || b.x < 0 || b.x > W || b.y < 0 || b.y > H) {
b.gfx.destroy();
this.bullets.splice(i, 1);
}
}
}
/* ================================================================
ASTEROIDS
================================================================ */
generateAsteroidVertices(radius) {
const verts = [];
const sides = 12;
for (let i = 0; i < sides; i++) {
const angle = (i / sides) * Math.PI * 2;
const r = radius * (0.7 + Math.random() * 0.3);
verts.push({ x: Math.cos(angle) * r, y: Math.sin(angle) * r });
}
return verts;
}
spawnAsteroid(sizeIdx, x, y, aimAtShip = false) {
const info = ASTEROID_SIZES[sizeIdx];
const radius = info.radius[0] + Math.random() * (info.radius[1] - info.radius[0]);
const scaledRadius = radius * Math.max(SCALE, 0.5);
// Position: at edges if not specified
let ax, ay;
if (x !== undefined && y !== undefined) {
ax = x;
ay = y;
}
else {
const edge = Math.floor(Math.random() * 4);
if (edge === 0) {
ax = Math.random() * W;
ay = -scaledRadius;
}
else if (edge === 1) {
ax = Math.random() * W;
ay = H + scaledRadius;
}
else if (edge === 2) {
ax = -scaledRadius;
ay = Math.random() * H;
}
else {
ax = W + scaledRadius;
ay = Math.random() * H;
}
// Make sure not too close to player
const dx = ax - this.shipX;
const dy = ay - this.shipY;
if (Math.sqrt(dx * dx + dy * dy) < 150) {
ax = (ax + W / 2) % W;
ay = (ay + H / 2) % H;
}
}
const speed = info.speed[0] + Math.random() * (info.speed[1] - info.speed[0]);
const speedBoost = Math.random() < 0.4 ? 1.5 : 1.0; // 40% chance of fast asteroid
// Aim toward the ship if requested, otherwise random direction
let angle;
let finalSpeed = speed * speedBoost;
if (aimAtShip) {
angle = Math.atan2(this.shipY - ay, this.shipX - ax);
// Add slight random spread (±15°) so it's not a perfect snipe
angle += (Math.random() - 0.5) * (Math.PI / 6);
// Ensure it arrives in ~3-4s regardless of base speed
const dist = Math.sqrt((this.shipX - ax) ** 2 + (this.shipY - ay) ** 2);
const minSpeed = dist / (3 + Math.random());
finalSpeed = Math.max(finalSpeed, minSpeed);
}
else {
angle = Math.random() * Math.PI * 2;
}
const vertices = this.generateAsteroidVertices(scaledRadius);
const gfx = this.add.graphics().setDepth(5);
this.drawAsteroid(gfx, vertices);
this.asteroids.push({
gfx,
x: ax, y: ay,
vx: Math.cos(angle) * finalSpeed,
vy: Math.sin(angle) * finalSpeed,
radius: scaledRadius,
sizeIdx,
rotation: 0,
rotSpeed: (Math.random() - 0.5) * 2,
vertices,
});
}
drawAsteroid(gfx, vertices) {
gfx.clear();
// Dark shadow backdrop for visibility on light backgrounds
gfx.lineStyle(5, 0x000000, 0.5);
gfx.beginPath();
gfx.moveTo(vertices[0].x, vertices[0].y);
for (let i = 1; i < vertices.length; i++) {
gfx.lineTo(vertices[i].x, vertices[i].y);
}
gfx.closePath();
gfx.strokePath();
// Outer glow (soft green)
gfx.lineStyle(3, 0x44ff44, 0.25);
gfx.beginPath();
gfx.moveTo(vertices[0].x, vertices[0].y);
for (let i = 1; i < vertices.length; i++) {
gfx.lineTo(vertices[i].x, vertices[i].y);
}
gfx.closePath();
gfx.strokePath();
// Solid outline (bright green-white)
gfx.lineStyle(2.5, 0x88ff88, 1);
gfx.beginPath();
gfx.moveTo(vertices[0].x, vertices[0].y);
for (let i = 1; i < vertices.length; i++) {
gfx.lineTo(vertices[i].x, vertices[i].y);
}
gfx.closePath();
gfx.strokePath();
}
updateAsteroids(dtSec) {
for (const a of this.asteroids) {
a.x += a.vx * dtSec;
a.y += a.vy * dtSec;
a.rotation += a.rotSpeed * dtSec;
// Screen wrap
if (a.x < -a.radius)
a.x = W + a.radius;
else if (a.x > W + a.radius)
a.x = -a.radius;
if (a.y < -a.radius)
a.y = H + a.radius;
else if (a.y > H + a.radius)
a.y = -a.radius;
a.gfx.setPosition(a.x, a.y);
a.gfx.setRotation(a.rotation);
}
}
destroyAsteroid(idx) {
const a = this.asteroids[idx];
const info = ASTEROID_SIZES[a.sizeIdx];
this.addScore(info.score, a.x, a.y - 10);
this.spawnExplosion(a.x, a.y);
this.sound.play('sfx_zap', { volume: 0.3 });
// Spawn children
if (a.sizeIdx < 2) {
const childSize = a.sizeIdx + 1;
for (let i = 0; i < 3; i++) {
this.spawnAsteroid(childSize, a.x, a.y);
}
}
a.gfx.destroy();
this.asteroids.splice(idx, 1);
// Check if wave cleared
if (this.asteroids.length === 0 && this.waveDelay <= 0) {
this.waveDelay = 2000;
}
}
/* ================================================================
COLLISIONS (manual rect/circle overlap — same pattern as Galaxy)
================================================================ */
checkCollisions() {
// Bullets vs asteroids
for (let bi = this.bullets.length - 1; bi >= 0; bi--) {
const b = this.bullets[bi];
for (let ai = this.asteroids.length - 1; ai >= 0; ai--) {
const a = this.asteroids[ai];
const dx = b.x - a.x;
const dy = b.y - a.y;
if (dx * dx + dy * dy < a.radius * a.radius) {
b.gfx.destroy();
this.bullets.splice(bi, 1);
this.destroyAsteroid(ai);
break;
}
}
}
// Ship vs asteroids
if (this.shipAlive && this.invincibleTimer <= 0) {
for (let ai = this.asteroids.length - 1; ai >= 0; ai--) {
const a = this.asteroids[ai];
const dx = this.shipX - a.x;
const dy = this.shipY - a.y;
const dist = Math.sqrt(dx * dx + dy * dy);
if (dist < a.radius + SHIP_SIZE * 0.6) {
this.hitShip();
break;
}
}
}
}
/* ================================================================
SHIP DEATH / LIVES
================================================================ */
hitShip() {
this.lives--;
this.syncLivesToHUD();
this.spawnExplosion(this.shipX, this.shipY);
this.sound.play('sfx_zap', { volume: 0.5 });
this.sound.play('sfx_lose', { volume: 0.4 });
if (this.lives <= 0) {
this.shipAlive = false;
if (this.shipGfx)
this.shipGfx.setVisible(false);
if (this.thrustGfx)
this.thrustGfx.setVisible(false);
this.gameOver = true;
this.time.delayedCall(1000, () => {
this.showGameOver(this.score, () => this.scene.restart());
});
}
else {
this.shipAlive = false;
if (this.shipGfx)
this.shipGfx.setVisible(false);
if (this.thrustGfx)
this.thrustGfx.setVisible(false);
this.respawnTimer = RESPAWN_DELAY;
}
}
/* ================================================================
PARTICLES
================================================================ */
spawnExplosion(x, y) {
this.spawnParticleExplosion(x, y, 0xffffff, 8);
}
/* ================================================================
UFO ENEMY
================================================================ */
spawnUfo() {
const fromRight = Math.random() < 0.5;
const x = fromRight ? W + 30 : -30;
const y = H * (0.15 + Math.random() * 0.3);
const vx = (fromRight ? -1 : 1) * (120 + Math.random() * 80);
const gfx = this.add.graphics().setDepth(12);
this.drawUfo(gfx);
gfx.setPosition(x, y);
this.ufo = { gfx, x, y, vx, shootTimer: 1500 + Math.random() * 1000, active: true };
}
drawUfo(gfx) {
gfx.clear();
const s = SHIP_SIZE * 1.2;
// Dark shadow backdrop
gfx.lineStyle(5, 0x000000, 0.5);
gfx.strokeEllipse(0, 0, s * 2, s * 0.7);
gfx.strokeEllipse(0, -s * 0.2, s, s * 0.5);
// Outer glow (soft magenta)
gfx.lineStyle(3, 0xff44ff, 0.25);
gfx.strokeEllipse(0, 0, s * 2, s * 0.7);
gfx.strokeEllipse(0, -s * 0.2, s, s * 0.5);
// Solid
gfx.lineStyle(2.5, 0xff88ff, 1);
gfx.strokeEllipse(0, 0, s * 2, s * 0.7);
gfx.strokeEllipse(0, -s * 0.2, s, s * 0.5);
}
updateUfo(dt, dtSec) {
// Spawn timer
if (!this.ufo) {
this.ufoTimer -= dt;
if (this.ufoTimer <= 0) {
this.spawnUfo();
this.ufoTimer = 15000 + Math.random() * 10000;
}
// Update UFO bullets even when no UFO
this.updateUfoBullets(dtSec);
return;
}
const u = this.ufo;
u.x += u.vx * dtSec;
u.gfx.setPosition(u.x, u.y);
// Off-screen — remove
if ((u.vx > 0 && u.x > W + 60) || (u.vx < 0 && u.x < -60)) {
u.gfx.destroy();
this.ufo = null;
return;
}
// Shoot at player
u.shootTimer -= dt;
if (u.shootTimer <= 0 && this.shipAlive) {
u.shootTimer = 1200 + Math.random() * 800;
const angle = Math.atan2(this.shipY - u.y, this.shipX - u.x);
const speed = 250;
const bGfx = this.add.graphics().setDepth(8);
bGfx.fillStyle(0x000000, 0.5);
bGfx.fillCircle(0, 0, 9);
bGfx.fillStyle(0xff44ff, 0.3);
bGfx.fillCircle(0, 0, 7);
bGfx.fillStyle(0xff88ff, 1);
bGfx.fillCircle(0, 0, 3);
bGfx.setPosition(u.x, u.y);
this.ufoBullets.push({
gfx: bGfx, x: u.x, y: u.y,
vx: Math.cos(angle) * speed,
vy: Math.sin(angle) * speed,
life: 3000,
});
}
this.updateUfoBullets(dtSec);
}
updateUfoBullets(dtSec) {
for (let i = this.ufoBullets.length - 1; i >= 0; i--) {
const b = this.ufoBullets[i];
b.x += b.vx * dtSec;
b.y += b.vy * dtSec;
b.life -= dtSec * 1000;
b.gfx.setPosition(b.x, b.y);
if (b.life <= 0 || b.x < -50 || b.x > W + 50 || b.y < -50 || b.y > H + 50) {
b.gfx.destroy();
this.ufoBullets.splice(i, 1);
}
}
}
checkUfoCollisions() {
if (!this.ufo)
return;
const u = this.ufo;
// Player bullets vs UFO
for (let bi = this.bullets.length - 1; bi >= 0; bi--) {
const b = this.bullets[bi];
const dx = b.x - u.x;
const dy = b.y - u.y;
if (dx * dx + dy * dy < (SHIP_SIZE * 1.5) ** 2) {
b.gfx.destroy();
this.bullets.splice(bi, 1);
this.addScore(500, u.x, u.y - 10);
this.spawnExplosion(u.x, u.y);
this.sound.play('sfx_zap', { volume: 0.4 });
u.gfx.destroy();
this.ufo = null;
return;
}
}
// UFO bullets vs player
if (this.shipAlive && this.invincibleTimer <= 0) {
for (let i = this.ufoBullets.length - 1; i >= 0; i--) {
const b = this.ufoBullets[i];
const dx = b.x - this.shipX;
const dy = b.y - this.shipY;
if (dx * dx + dy * dy < (SHIP_SIZE * 0.8) ** 2) {
b.gfx.destroy();
this.ufoBullets.splice(i, 1);
this.hitShip();
return;
}
}
}
// UFO body vs player
if (this.shipAlive && this.invincibleTimer <= 0) {
const dx = this.shipX - u.x;
const dy = this.shipY - u.y;
if (dx * dx + dy * dy < (SHIP_SIZE * 1.8) ** 2) {
this.spawnExplosion(u.x, u.y);
u.gfx.destroy();
this.ufo = null;
this.hitShip();
}
}
}
/* ================================================================
WAVE SYSTEM
================================================================ */
startWave() {
this.wave++;
this.syncLevelToHUD(this.wave);
this.sound.play('sfx_twoTone', { volume: 0.3 });
const count = INITIAL_ASTEROIDS + (this.wave - 1) * 2;
// Aim the first 2 asteroids at the ship so the player must act quickly
for (let i = 0; i < count; i++) {
this.spawnAsteroid(0, undefined, undefined, i < 2);
}
this.showWaveBanner(this.wave);
}
/* ================================================================
CLEANUP
================================================================ */
shutdown() {
super.shutdown();
if (this.ufo) {
this.ufo.gfx.destroy();
this.ufo = null;
}
this.ufoBullets.forEach(b => b.gfx.destroy());
this.ufoBullets = [];
const banner = document.getElementById('wave-banner');
if (banner)
banner.remove();
}
}
//# sourceMappingURL=CosmicRocks.js.map