mirror of
https://github.com/github/awesome-copilot.git
synced 2026-06-18 13:41:26 +00:00
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>
This commit is contained in:
@@ -0,0 +1,697 @@
|
||||
// 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
|
||||
Reference in New Issue
Block a user