mirror of
https://github.com/github/awesome-copilot.git
synced 2026-06-18 13:41:26 +00:00
2f9d85eef8
* 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>
697 lines
25 KiB
JavaScript
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
|