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>
812 lines
32 KiB
JavaScript
812 lines
32 KiB
JavaScript
// AlienOnslaught — Space Invaders-style arcade shooter.
|
|
// Rows of aliens march across the screen, descending as they reach the
|
|
// edges. The player defends from the bottom with destructible shields.
|
|
// All graphics are procedural (Phaser Graphics) — no external sprite sheets.
|
|
import { BaseScene, W, H } from './BaseScene.js';
|
|
/* ------------------------------------------------------------------ */
|
|
/* Constants — recalculated in create() for responsive sizing */
|
|
/* ------------------------------------------------------------------ */
|
|
let SCALE = Math.min(W / 1920, H / 1080);
|
|
// Grid layout
|
|
const ALIEN_COLS = 11;
|
|
const ALIEN_ROWS = 5;
|
|
// Alien types per row (top → bottom): squid, crab, crab, octopus, octopus
|
|
const ALIEN_TYPES = [
|
|
{ name: 'squid', points: 30, color: 0xff4444 }, // row 0 (top)
|
|
{ name: 'crab', points: 20, color: 0x44ff44 }, // rows 1-2
|
|
{ name: 'octopus', points: 10, color: 0x44aaff }, // rows 3-4
|
|
];
|
|
// Timing / speeds
|
|
const BASE_MARCH_INTERVAL = 700; // ms between march steps at full grid
|
|
const MIN_MARCH_INTERVAL = 60; // fastest march with few aliens left
|
|
const MARCH_DROP = 0; // calculated in create()
|
|
const PLAYER_SPEED = 350; // px/s
|
|
const PLAYER_BULLET_SPEED = 500; // px/s
|
|
const ALIEN_BULLET_SPEED = 250; // px/s
|
|
const ALIEN_FIRE_INTERVAL = 1200; // ms between alien shots (base)
|
|
const MYSTERY_INTERVAL_MIN = 15000;
|
|
const MYSTERY_INTERVAL_MAX = 30000;
|
|
const MYSTERY_SPEED = 150; // px/s
|
|
const INVINCIBLE_TIME = 2000; // ms
|
|
// Shield config
|
|
const SHIELD_COUNT = 4;
|
|
const SHIELD_BLOCK_COLS = 22;
|
|
const SHIELD_BLOCK_ROWS = 16;
|
|
/* ------------------------------------------------------------------ */
|
|
/* Scene */
|
|
/* ------------------------------------------------------------------ */
|
|
export class AlienOnslaughtScene extends BaseScene {
|
|
/* player */
|
|
playerGfx;
|
|
playerX = 0;
|
|
playerY = 0;
|
|
playerAlive = true;
|
|
/* aliens */
|
|
aliens = [];
|
|
alienCellW = 0;
|
|
alienCellH = 0;
|
|
alienGridX = 0; // grid origin
|
|
alienGridY = 0;
|
|
marchDir = 1; // 1 = right, -1 = left
|
|
marchTimer = 0;
|
|
marchInterval = BASE_MARCH_INTERVAL;
|
|
marchStepX = 0;
|
|
marchDrop = 0;
|
|
/* bullets */
|
|
playerBullets = [];
|
|
alienBullets = [];
|
|
alienFireTimer = 0;
|
|
/* mystery ship */
|
|
mystery = null;
|
|
mysteryTimer = 0;
|
|
/* shields */
|
|
shields = []; // [shieldIdx][blockIdx]
|
|
/* starfield */
|
|
stars = [];
|
|
/* game state */
|
|
wave = 0;
|
|
invincibleTimer = 0;
|
|
respawnTimer = 0;
|
|
gameOverFlag = false;
|
|
waveDelay = 0;
|
|
/* input */
|
|
cursors;
|
|
spaceKey;
|
|
spaceWasDown = false;
|
|
/* sizing (calculated in create) */
|
|
alienW = 0;
|
|
alienH = 0;
|
|
playerW = 0;
|
|
playerH = 0;
|
|
bulletW = 0;
|
|
bulletH = 0;
|
|
constructor() { super('alien-onslaught'); }
|
|
get displayName() { return 'Alien Onslaught'; }
|
|
getDescription() {
|
|
return 'Blast waves of descending aliens before they reach the bottom!';
|
|
}
|
|
getControls() {
|
|
return [
|
|
{ key: '← →', action: 'Move Left / Right' },
|
|
{ key: 'SPACE', action: 'Fire' },
|
|
];
|
|
}
|
|
/* ================================================================
|
|
LIFECYCLE
|
|
================================================================ */
|
|
preload() {
|
|
// Reuse existing sound effects
|
|
this.load.audio('ao_laser', '../assets/galaxy-blaster/sounds/sfx_laser1.ogg');
|
|
this.load.audio('ao_explosion', '../assets/galaxy-blaster/sounds/sfx_explosion.ogg');
|
|
this.load.audio('ao_lose', '../assets/cosmic-rocks/sounds/sfx_lose.ogg');
|
|
this.load.audio('ao_twoTone', '../assets/cosmic-rocks/sounds/sfx_twoTone.ogg');
|
|
this.load.audio('ao_shieldHit', '../assets/galaxy-blaster/sounds/sfx_zap.ogg');
|
|
this.load.audio('ao_mystery', '../assets/galaxy-blaster/sounds/sfx_twoTone.ogg');
|
|
}
|
|
create() {
|
|
this.initBase();
|
|
// Responsive sizing — scale the grid to fill ~70% of screen width
|
|
SCALE = Math.min(W / 1920, H / 1080);
|
|
const s = Math.max(SCALE, 0.5);
|
|
// Size the grid relative to screen, not a fixed pixel size
|
|
this.alienCellW = Math.round(W * 0.055); // ~85% of original — tighter grid
|
|
this.alienCellH = Math.round(this.alienCellW * 0.8);
|
|
this.alienW = Math.round(this.alienCellW * 0.6);
|
|
this.alienH = Math.round(this.alienCellH * 0.55);
|
|
this.playerW = Math.round(this.alienCellW * 0.85);
|
|
this.playerH = Math.round(this.playerW * 0.55);
|
|
this.bulletW = Math.round(4 * s);
|
|
this.bulletH = Math.round(12 * s);
|
|
this.marchStepX = Math.round(this.alienCellW * 0.25); // bigger steps → hit edges sooner
|
|
this.marchDrop = Math.round(this.alienCellH * 0.6); // bigger drops → descend faster
|
|
// Reset state
|
|
this.score = 0;
|
|
this.lives = 3;
|
|
this.wave = 0;
|
|
this.playerAlive = true;
|
|
this.gameOverFlag = false;
|
|
this.invincibleTimer = INVINCIBLE_TIME;
|
|
this.respawnTimer = 0;
|
|
this.waveDelay = 0;
|
|
this.marchDir = 1;
|
|
this.marchTimer = 0;
|
|
this.marchInterval = BASE_MARCH_INTERVAL;
|
|
this.playerBullets = [];
|
|
this.alienBullets = [];
|
|
this.aliens = [];
|
|
this.shields = [];
|
|
this.mystery = null;
|
|
this.mysteryTimer = MYSTERY_INTERVAL_MIN + Math.random() * (MYSTERY_INTERVAL_MAX - MYSTERY_INTERVAL_MIN);
|
|
this.stars = [];
|
|
this.ensureSparkTexture();
|
|
// Starfield
|
|
this.stars = this.createStarfield([
|
|
{ count: 40, speed: 10, size: 1, alpha: 0.2 },
|
|
{ count: 25, speed: 20, size: 1.5, alpha: 0.3 },
|
|
{ count: 10, speed: 40, size: 2, alpha: 0.4 },
|
|
]);
|
|
// Player position — bottom of screen with padding
|
|
this.playerX = W / 2;
|
|
this.playerY = H * 0.92;
|
|
this.playerGfx = this.add.graphics().setDepth(10);
|
|
this.drawPlayer();
|
|
// Input
|
|
this.cursors = this.input.keyboard.createCursorKeys();
|
|
this.spaceKey = this.input.keyboard.addKey('SPACE');
|
|
this.spaceWasDown = false;
|
|
// HUD
|
|
this.syncLivesToHUD();
|
|
this.syncScoreToHUD();
|
|
this.loadHighScore();
|
|
this.startWithReadyScreen(() => this.startWave());
|
|
}
|
|
update(_t, dtMs) {
|
|
if (this.gameOverFlag || !this.cursors)
|
|
return;
|
|
const dt = Math.min(dtMs, 33);
|
|
const dtSec = dt / 1000;
|
|
this.updateStarfield(this.stars, dt);
|
|
// Respawn delay
|
|
if (this.respawnTimer > 0) {
|
|
this.respawnTimer -= dt;
|
|
if (this.respawnTimer <= 0)
|
|
this.respawnPlayer();
|
|
}
|
|
// Player input
|
|
if (this.playerAlive) {
|
|
this.updatePlayerInput(dtSec);
|
|
}
|
|
// Invincibility flicker
|
|
if (this.invincibleTimer > 0) {
|
|
this.invincibleTimer -= dt;
|
|
if (this.playerGfx) {
|
|
this.playerGfx.setAlpha(Math.sin(performance.now() / 80) > 0 ? 1 : 0.2);
|
|
}
|
|
}
|
|
else if (this.playerGfx) {
|
|
this.playerGfx.setAlpha(1);
|
|
}
|
|
// Alien march
|
|
this.marchTimer += dt;
|
|
if (this.marchTimer >= this.marchInterval) {
|
|
this.marchTimer = 0;
|
|
this.marchAliens();
|
|
}
|
|
// Alien shooting
|
|
this.alienFireTimer += dt;
|
|
const fireInterval = Math.max(400, ALIEN_FIRE_INTERVAL - this.wave * 80);
|
|
if (this.alienFireTimer >= fireInterval) {
|
|
this.alienFireTimer = 0;
|
|
this.alienShoot();
|
|
}
|
|
// Update bullets
|
|
this.updatePlayerBullets(dtSec);
|
|
this.updateAlienBullets(dtSec);
|
|
// Mystery ship
|
|
this.updateMystery(dt, dtSec);
|
|
// Collisions
|
|
this.checkCollisions();
|
|
// Wave clear
|
|
if (this.waveDelay > 0) {
|
|
this.waveDelay -= dt;
|
|
if (this.waveDelay <= 0)
|
|
this.startWave();
|
|
}
|
|
else if (this.aliens.filter(a => a.alive).length === 0 && this.waveDelay <= 0) {
|
|
this.waveDelay = 1500;
|
|
}
|
|
}
|
|
/* ================================================================
|
|
PLAYER
|
|
================================================================ */
|
|
drawPlayer() {
|
|
const g = this.playerGfx;
|
|
g.clear();
|
|
g.setPosition(this.playerX, this.playerY);
|
|
const hw = this.playerW / 2;
|
|
const hh = this.playerH / 2;
|
|
const turretW = hw * 0.2;
|
|
const turretH = hh * 0.6;
|
|
// Shadow
|
|
g.fillStyle(0x000000, 0.5);
|
|
g.fillRect(-hw - 1, -hh - 1, this.playerW + 2, this.playerH + 2);
|
|
g.fillRect(-turretW - 1, -hh - turretH - 1, turretW * 2 + 2, turretH + 2);
|
|
// Body (bright green)
|
|
g.fillStyle(0x00ff66, 1);
|
|
g.fillRect(-hw, -hh, this.playerW, this.playerH);
|
|
// Turret
|
|
g.fillStyle(0x00ff66, 1);
|
|
g.fillRect(-turretW, -hh - turretH, turretW * 2, turretH);
|
|
// Cockpit highlight
|
|
g.fillStyle(0xaaffcc, 0.6);
|
|
g.fillRect(-hw * 0.3, -hh * 0.5, hw * 0.6, hh * 0.6);
|
|
}
|
|
updatePlayerInput(dtSec) {
|
|
if (!this.cursors)
|
|
return;
|
|
if (this.cursors.left.isDown) {
|
|
this.playerX -= PLAYER_SPEED * dtSec;
|
|
}
|
|
if (this.cursors.right.isDown) {
|
|
this.playerX += PLAYER_SPEED * dtSec;
|
|
}
|
|
// Clamp to screen
|
|
const hw = this.playerW / 2;
|
|
this.playerX = Math.max(hw, Math.min(W - hw, this.playerX));
|
|
this.drawPlayer();
|
|
// Fire
|
|
const spaceDown = this.spaceKey.isDown;
|
|
if (spaceDown && !this.spaceWasDown && this.playerBullets.length < 2) {
|
|
this.firePlayerBullet();
|
|
}
|
|
this.spaceWasDown = spaceDown;
|
|
}
|
|
respawnPlayer() {
|
|
this.playerX = W / 2;
|
|
this.playerAlive = true;
|
|
this.invincibleTimer = INVINCIBLE_TIME;
|
|
if (this.playerGfx) {
|
|
this.playerGfx.setVisible(true);
|
|
}
|
|
this.drawPlayer();
|
|
}
|
|
/* ================================================================
|
|
PLAYER BULLETS
|
|
================================================================ */
|
|
firePlayerBullet() {
|
|
this.sound.play('ao_laser', { volume: 0.3 });
|
|
const gfx = this.add.graphics().setDepth(8);
|
|
const bx = this.playerX;
|
|
const by = this.playerY - this.playerH / 2;
|
|
// Glow
|
|
gfx.fillStyle(0x00ffff, 0.3);
|
|
gfx.fillRect(-this.bulletW, -this.bulletH, this.bulletW * 2, this.bulletH * 2);
|
|
// Solid
|
|
gfx.fillStyle(0x00ffff, 1);
|
|
gfx.fillRect(-this.bulletW / 2, -this.bulletH / 2, this.bulletW, this.bulletH);
|
|
gfx.setPosition(bx, by);
|
|
this.playerBullets.push({ gfx, x: bx, y: by });
|
|
}
|
|
updatePlayerBullets(dtSec) {
|
|
for (let i = this.playerBullets.length - 1; i >= 0; i--) {
|
|
const b = this.playerBullets[i];
|
|
b.y -= PLAYER_BULLET_SPEED * dtSec;
|
|
b.gfx.setPosition(b.x, b.y);
|
|
if (b.y < -this.bulletH) {
|
|
b.gfx.destroy();
|
|
this.playerBullets.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
/* ================================================================
|
|
ALIENS
|
|
================================================================ */
|
|
startWave() {
|
|
this.wave++;
|
|
this.level = this.wave;
|
|
this.syncLevelToHUD();
|
|
this.showWaveBanner(this.wave);
|
|
// Clear leftover bullets
|
|
for (const b of this.playerBullets)
|
|
b.gfx.destroy();
|
|
this.playerBullets = [];
|
|
for (const b of this.alienBullets)
|
|
b.gfx.destroy();
|
|
this.alienBullets = [];
|
|
// Reset march
|
|
this.marchDir = 1;
|
|
this.marchTimer = 0;
|
|
this.alienFireTimer = 0;
|
|
// Calculate grid start position (centered)
|
|
const gridW = ALIEN_COLS * this.alienCellW;
|
|
this.alienGridX = (W - gridW) / 2;
|
|
this.alienGridY = Math.max(H * 0.20, 120);
|
|
// Create aliens
|
|
for (const a of this.aliens)
|
|
a.gfx.destroy();
|
|
this.aliens = [];
|
|
for (let row = 0; row < ALIEN_ROWS; row++) {
|
|
const typeIdx = row === 0 ? 0 : row <= 2 ? 1 : 2;
|
|
for (let col = 0; col < ALIEN_COLS; col++) {
|
|
const x = this.alienGridX + col * this.alienCellW + this.alienCellW / 2;
|
|
const y = this.alienGridY + row * this.alienCellH + this.alienCellH / 2;
|
|
const gfx = this.add.graphics().setDepth(5);
|
|
const alien = { gfx, row, col, type: typeIdx, alive: true, x, y, frame: 0 };
|
|
this.drawAlien(alien);
|
|
this.aliens.push(alien);
|
|
}
|
|
}
|
|
this.updateMarchInterval();
|
|
// Recreate shields on first wave only
|
|
if (this.wave === 1) {
|
|
this.createShields();
|
|
}
|
|
}
|
|
drawAlien(alien) {
|
|
const g = alien.gfx;
|
|
g.clear();
|
|
g.setPosition(alien.x, alien.y);
|
|
const type = ALIEN_TYPES[alien.type];
|
|
const hw = this.alienW / 2;
|
|
const hh = this.alienH / 2;
|
|
const px = Math.max(2, Math.round(this.alienW / 10)); // pixel unit size
|
|
// Draw pixel-art alien based on type
|
|
g.fillStyle(type.color, 1);
|
|
if (type.name === 'squid') {
|
|
// Squid alien — narrow top, wider middle
|
|
g.fillRect(-px, -hh, px * 2, px); // top antenna
|
|
g.fillRect(-px * 2, -hh + px, px * 4, px); // head top
|
|
g.fillRect(-px * 3, -hh + px * 2, px * 6, px * 2); // head body
|
|
g.fillRect(-px * 4, -hh + px * 4, px * 8, px); // wider
|
|
g.fillRect(-px * 3, -hh + px * 5, px * 6, px); // middle
|
|
if (alien.frame === 0) {
|
|
// legs out
|
|
g.fillRect(-px * 4, -hh + px * 6, px * 2, px);
|
|
g.fillRect(px * 2, -hh + px * 6, px * 2, px);
|
|
}
|
|
else {
|
|
// legs in
|
|
g.fillRect(-px * 2, -hh + px * 6, px * 2, px);
|
|
g.fillRect(0, -hh + px * 6, px * 2, px);
|
|
}
|
|
}
|
|
else if (type.name === 'crab') {
|
|
// Crab alien — classic shape with claws
|
|
g.fillRect(-px, -hh, px * 2, px); // antenna
|
|
g.fillRect(-px * 3, -hh + px, px * 6, px); // top
|
|
g.fillRect(-px * 4, -hh + px * 2, px * 8, px * 2); // body
|
|
g.fillRect(-px * 5, -hh + px * 4, px * 10, px); // wide row
|
|
g.fillRect(-px * 4, -hh + px * 5, px * 8, px); // narrower
|
|
// Eyes (dark cutouts)
|
|
g.fillStyle(0x000000, 1);
|
|
g.fillRect(-px * 2, -hh + px * 2, px, px);
|
|
g.fillRect(px, -hh + px * 2, px, px);
|
|
g.fillStyle(type.color, 1);
|
|
if (alien.frame === 0) {
|
|
g.fillRect(-px * 5, -hh + px * 5, px, px * 2);
|
|
g.fillRect(px * 4, -hh + px * 5, px, px * 2);
|
|
}
|
|
else {
|
|
g.fillRect(-px * 3, -hh + px * 6, px * 2, px);
|
|
g.fillRect(px, -hh + px * 6, px * 2, px);
|
|
}
|
|
}
|
|
else {
|
|
// Octopus alien — round with tentacles
|
|
g.fillRect(-px * 2, -hh, px * 4, px); // top
|
|
g.fillRect(-px * 4, -hh + px, px * 8, px * 2); // upper body
|
|
g.fillRect(-px * 5, -hh + px * 3, px * 10, px * 2); // body
|
|
g.fillRect(-px * 4, -hh + px * 5, px * 8, px); // lower
|
|
// Eyes
|
|
g.fillStyle(0x000000, 1);
|
|
g.fillRect(-px * 3, -hh + px * 2, px * 2, px);
|
|
g.fillRect(px, -hh + px * 2, px * 2, px);
|
|
g.fillStyle(type.color, 1);
|
|
if (alien.frame === 0) {
|
|
// tentacles down/out
|
|
g.fillRect(-px * 5, -hh + px * 6, px * 2, px);
|
|
g.fillRect(-px * 2, -hh + px * 6, px, px);
|
|
g.fillRect(px, -hh + px * 6, px, px);
|
|
g.fillRect(px * 3, -hh + px * 6, px * 2, px);
|
|
}
|
|
else {
|
|
// tentacles up/in
|
|
g.fillRect(-px * 4, -hh + px * 6, px * 2, px);
|
|
g.fillRect(-px, -hh + px * 6, px * 2, px);
|
|
g.fillRect(px * 2, -hh + px * 6, px * 2, px);
|
|
}
|
|
}
|
|
}
|
|
marchAliens() {
|
|
const alive = this.aliens.filter(a => a.alive);
|
|
if (alive.length === 0)
|
|
return;
|
|
// Check if any alien hit the edge
|
|
let hitEdge = false;
|
|
const margin = this.alienCellW * 0.3;
|
|
for (const a of alive) {
|
|
if (this.marchDir === 1 && a.x + this.alienW / 2 + this.marchStepX > W - margin) {
|
|
hitEdge = true;
|
|
break;
|
|
}
|
|
if (this.marchDir === -1 && a.x - this.alienW / 2 - this.marchStepX < margin) {
|
|
hitEdge = true;
|
|
break;
|
|
}
|
|
}
|
|
if (hitEdge) {
|
|
// Drop down and reverse
|
|
this.marchDir *= -1;
|
|
for (const a of alive) {
|
|
a.y += this.marchDrop;
|
|
a.frame = 1 - a.frame;
|
|
this.drawAlien(a);
|
|
// Check if aliens reached player row — instant game over (classic rules)
|
|
if (a.y + this.alienH / 2 >= this.playerY - this.playerH / 2) {
|
|
this.triggerGameOver();
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
// March sideways
|
|
for (const a of alive) {
|
|
a.x += this.marchStepX * this.marchDir;
|
|
a.frame = 1 - a.frame;
|
|
this.drawAlien(a);
|
|
}
|
|
}
|
|
// Play march sound (alternate tone)
|
|
this.sound.play('ao_twoTone', { volume: 0.15 });
|
|
}
|
|
updateMarchInterval() {
|
|
const aliveCount = this.aliens.filter(a => a.alive).length;
|
|
const total = ALIEN_COLS * ALIEN_ROWS;
|
|
if (total === 0)
|
|
return;
|
|
// Exponential speed-up as aliens are destroyed
|
|
const ratio = aliveCount / total;
|
|
this.marchInterval = MIN_MARCH_INTERVAL + (BASE_MARCH_INTERVAL - MIN_MARCH_INTERVAL) * ratio;
|
|
// Wave speed bonus
|
|
this.marchInterval = Math.max(MIN_MARCH_INTERVAL, this.marchInterval - this.wave * 20);
|
|
}
|
|
/* ================================================================
|
|
ALIEN SHOOTING
|
|
================================================================ */
|
|
alienShoot() {
|
|
const alive = this.aliens.filter(a => a.alive);
|
|
if (alive.length === 0)
|
|
return;
|
|
// Find bottommost alien in each column, then pick one at random
|
|
const bottomAliens = [];
|
|
for (let col = 0; col < ALIEN_COLS; col++) {
|
|
const colAliens = alive.filter(a => a.col === col);
|
|
if (colAliens.length > 0) {
|
|
colAliens.sort((a, b) => b.row - a.row);
|
|
bottomAliens.push(colAliens[0]);
|
|
}
|
|
}
|
|
if (bottomAliens.length === 0)
|
|
return;
|
|
const shooter = bottomAliens[Math.floor(Math.random() * bottomAliens.length)];
|
|
const gfx = this.add.graphics().setDepth(7);
|
|
// Alien bullet — different color (yellow/red)
|
|
gfx.fillStyle(0xffaa00, 0.4);
|
|
gfx.fillRect(-this.bulletW, -this.bulletH / 2, this.bulletW * 2, this.bulletH);
|
|
gfx.fillStyle(0xff4444, 1);
|
|
gfx.fillRect(-this.bulletW / 2, -this.bulletH / 2, this.bulletW, this.bulletH);
|
|
gfx.setPosition(shooter.x, shooter.y + this.alienH / 2);
|
|
this.alienBullets.push({ gfx, x: shooter.x, y: shooter.y + this.alienH / 2 });
|
|
}
|
|
updateAlienBullets(dtSec) {
|
|
for (let i = this.alienBullets.length - 1; i >= 0; i--) {
|
|
const b = this.alienBullets[i];
|
|
b.y += ALIEN_BULLET_SPEED * dtSec;
|
|
b.gfx.setPosition(b.x, b.y);
|
|
if (b.y > H + this.bulletH) {
|
|
b.gfx.destroy();
|
|
this.alienBullets.splice(i, 1);
|
|
}
|
|
}
|
|
}
|
|
/* ================================================================
|
|
MYSTERY SHIP
|
|
================================================================ */
|
|
spawnMystery() {
|
|
const dir = Math.random() < 0.5 ? 1 : -1;
|
|
const x = dir === 1 ? -40 : W + 40;
|
|
// Position just above the alien grid, below the HUD
|
|
const y = this.alienGridY - this.alienCellH * 1.2;
|
|
const gfx = this.add.graphics().setDepth(12);
|
|
this.mystery = { gfx, x, y, direction: dir, active: true };
|
|
this.drawMystery();
|
|
this.sound.play('ao_mystery', { volume: 0.2 });
|
|
}
|
|
drawMystery() {
|
|
if (!this.mystery)
|
|
return;
|
|
const g = this.mystery.gfx;
|
|
g.clear();
|
|
g.setPosition(this.mystery.x, this.mystery.y);
|
|
const s = Math.max(SCALE, 0.5);
|
|
const w = 30 * s;
|
|
const h = 12 * s;
|
|
// Saucer shape
|
|
g.fillStyle(0x000000, 0.5);
|
|
g.fillEllipse(0, 0, w * 2 + 2, h + 2);
|
|
g.fillStyle(0xff00ff, 0.8);
|
|
g.fillEllipse(0, 0, w * 2, h);
|
|
// Dome
|
|
g.fillStyle(0xff66ff, 1);
|
|
g.fillEllipse(0, -h * 0.4, w, h * 0.7);
|
|
// Lights
|
|
g.fillStyle(0xffff00, 1);
|
|
g.fillCircle(-w * 0.5, 0, 2 * s);
|
|
g.fillCircle(0, 0, 2 * s);
|
|
g.fillCircle(w * 0.5, 0, 2 * s);
|
|
}
|
|
updateMystery(dt, dtSec) {
|
|
if (this.mystery && this.mystery.active) {
|
|
this.mystery.x += MYSTERY_SPEED * this.mystery.direction * dtSec;
|
|
this.drawMystery();
|
|
// Off screen?
|
|
if ((this.mystery.direction === 1 && this.mystery.x > W + 60) ||
|
|
(this.mystery.direction === -1 && this.mystery.x < -60)) {
|
|
this.mystery.gfx.destroy();
|
|
this.mystery = null;
|
|
}
|
|
}
|
|
else {
|
|
this.mysteryTimer -= dt;
|
|
if (this.mysteryTimer <= 0) {
|
|
this.mysteryTimer = MYSTERY_INTERVAL_MIN + Math.random() * (MYSTERY_INTERVAL_MAX - MYSTERY_INTERVAL_MIN);
|
|
this.spawnMystery();
|
|
}
|
|
}
|
|
}
|
|
/* ================================================================
|
|
SHIELDS
|
|
================================================================ */
|
|
createShields() {
|
|
// Clear existing
|
|
for (const shield of this.shields) {
|
|
for (const block of shield) {
|
|
if (block.gfx)
|
|
block.gfx.destroy();
|
|
}
|
|
}
|
|
this.shields = [];
|
|
const s = Math.max(SCALE, 0.5);
|
|
// Original shields were ~6% of screen height tall; derive block size from that
|
|
const targetShieldH = H * 0.055;
|
|
const blockH = Math.max(2, Math.round(targetShieldH / SHIELD_BLOCK_ROWS));
|
|
const blockW = blockH;
|
|
const shieldW = SHIELD_BLOCK_COLS * blockW;
|
|
const shieldH = SHIELD_BLOCK_ROWS * blockH;
|
|
const totalShieldsW = SHIELD_COUNT * shieldW;
|
|
const gap = (W - totalShieldsW) / (SHIELD_COUNT + 1);
|
|
const shieldY = this.playerY - this.playerH - shieldH - 20;
|
|
// Classic shield shape mask (inverted U)
|
|
const shieldMask = this.generateShieldMask();
|
|
for (let si = 0; si < SHIELD_COUNT; si++) {
|
|
const shieldX = gap + si * (shieldW + gap);
|
|
const blocks = [];
|
|
for (let r = 0; r < SHIELD_BLOCK_ROWS; r++) {
|
|
for (let c = 0; c < SHIELD_BLOCK_COLS; c++) {
|
|
if (!shieldMask[r][c])
|
|
continue;
|
|
const bx = shieldX + c * blockW;
|
|
const by = shieldY + r * blockH;
|
|
const gfx = this.add.graphics().setDepth(6);
|
|
gfx.fillStyle(0x00ff66, 1);
|
|
gfx.fillRect(0, 0, blockW, blockH);
|
|
gfx.setPosition(bx, by);
|
|
blocks.push({ gfx, x: bx, y: by, w: blockW, h: blockH, alive: true });
|
|
}
|
|
}
|
|
this.shields.push(blocks);
|
|
}
|
|
}
|
|
generateShieldMask() {
|
|
const mask = [];
|
|
for (let r = 0; r < SHIELD_BLOCK_ROWS; r++) {
|
|
mask[r] = [];
|
|
for (let c = 0; c < SHIELD_BLOCK_COLS; c++) {
|
|
// Round top
|
|
if (r < 4) {
|
|
const center = SHIELD_BLOCK_COLS / 2;
|
|
const dist = Math.abs(c - center + 0.5);
|
|
const maxDist = (SHIELD_BLOCK_COLS / 2) * (1 - r * 0.05);
|
|
mask[r][c] = dist < maxDist;
|
|
}
|
|
// Middle — solid
|
|
else if (r < SHIELD_BLOCK_ROWS - 5) {
|
|
mask[r][c] = true;
|
|
}
|
|
// Bottom — cut out arch
|
|
else {
|
|
const center = SHIELD_BLOCK_COLS / 2;
|
|
const dist = Math.abs(c - center + 0.5);
|
|
const archRow = r - (SHIELD_BLOCK_ROWS - 5);
|
|
const archWidth = 3 + archRow * 0.8;
|
|
mask[r][c] = dist > archWidth;
|
|
}
|
|
}
|
|
}
|
|
return mask;
|
|
}
|
|
/* ================================================================
|
|
COLLISIONS
|
|
================================================================ */
|
|
checkCollisions() {
|
|
// Player bullets vs aliens
|
|
for (let bi = this.playerBullets.length - 1; bi >= 0; bi--) {
|
|
const b = this.playerBullets[bi];
|
|
let hit = false;
|
|
for (const a of this.aliens) {
|
|
if (!a.alive)
|
|
continue;
|
|
if (this.rectOverlap(b.x - this.bulletW / 2, b.y - this.bulletH / 2, this.bulletW, this.bulletH, a.x - this.alienW / 2, a.y - this.alienH / 2, this.alienW, this.alienH)) {
|
|
a.alive = false;
|
|
a.gfx.setVisible(false);
|
|
this.addScore(ALIEN_TYPES[a.type].points, a.x, a.y);
|
|
this.spawnExplosion(a.x, a.y, ALIEN_TYPES[a.type].color);
|
|
this.sound.play('ao_explosion', { volume: 0.25 });
|
|
this.updateMarchInterval();
|
|
hit = true;
|
|
break;
|
|
}
|
|
}
|
|
// Player bullets vs mystery
|
|
if (!hit && this.mystery && this.mystery.active) {
|
|
const mw = 30 * Math.max(SCALE, 0.5);
|
|
const mh = 12 * Math.max(SCALE, 0.5);
|
|
if (this.rectOverlap(b.x - this.bulletW / 2, b.y - this.bulletH / 2, this.bulletW, this.bulletH, this.mystery.x - mw, this.mystery.y - mh / 2, mw * 2, mh)) {
|
|
const mysteryPoints = [50, 100, 150, 300][Math.floor(Math.random() * 4)];
|
|
this.addScore(mysteryPoints, this.mystery.x, this.mystery.y);
|
|
this.spawnExplosion(this.mystery.x, this.mystery.y, 0xff00ff);
|
|
this.sound.play('ao_explosion', { volume: 0.3 });
|
|
this.mystery.gfx.destroy();
|
|
this.mystery = null;
|
|
hit = true;
|
|
}
|
|
}
|
|
// Player bullets vs shields
|
|
if (!hit) {
|
|
for (const shield of this.shields) {
|
|
for (const block of shield) {
|
|
if (!block.alive)
|
|
continue;
|
|
if (this.rectOverlap(b.x - this.bulletW / 2, b.y - this.bulletH / 2, this.bulletW, this.bulletH, block.x, block.y, block.w, block.h)) {
|
|
block.alive = false;
|
|
block.gfx.destroy();
|
|
hit = true;
|
|
break;
|
|
}
|
|
}
|
|
if (hit)
|
|
break;
|
|
}
|
|
}
|
|
if (hit) {
|
|
b.gfx.destroy();
|
|
this.playerBullets.splice(bi, 1);
|
|
}
|
|
}
|
|
// Alien bullets vs player
|
|
if (this.playerAlive && this.invincibleTimer <= 0) {
|
|
for (let bi = this.alienBullets.length - 1; bi >= 0; bi--) {
|
|
const b = this.alienBullets[bi];
|
|
if (this.rectOverlap(b.x - this.bulletW / 2, b.y - this.bulletH / 2, this.bulletW, this.bulletH, this.playerX - this.playerW / 2, this.playerY - this.playerH / 2, this.playerW, this.playerH)) {
|
|
b.gfx.destroy();
|
|
this.alienBullets.splice(bi, 1);
|
|
this.playerHit();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// Alien bullets vs shields
|
|
for (let bi = this.alienBullets.length - 1; bi >= 0; bi--) {
|
|
const b = this.alienBullets[bi];
|
|
let hit = false;
|
|
for (const shield of this.shields) {
|
|
for (const block of shield) {
|
|
if (!block.alive)
|
|
continue;
|
|
if (this.rectOverlap(b.x - this.bulletW / 2, b.y - this.bulletH / 2, this.bulletW, this.bulletH, block.x, block.y, block.w, block.h)) {
|
|
block.alive = false;
|
|
block.gfx.destroy();
|
|
hit = true;
|
|
break;
|
|
}
|
|
}
|
|
if (hit)
|
|
break;
|
|
}
|
|
if (hit) {
|
|
b.gfx.destroy();
|
|
this.alienBullets.splice(bi, 1);
|
|
}
|
|
}
|
|
// Aliens vs shields (aliens marching into shields)
|
|
for (const a of this.aliens) {
|
|
if (!a.alive)
|
|
continue;
|
|
for (const shield of this.shields) {
|
|
for (const block of shield) {
|
|
if (!block.alive)
|
|
continue;
|
|
if (this.rectOverlap(a.x - this.alienW / 2, a.y - this.alienH / 2, this.alienW, this.alienH, block.x, block.y, block.w, block.h)) {
|
|
block.alive = false;
|
|
block.gfx.destroy();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
rectOverlap(x1, y1, w1, h1, x2, y2, w2, h2) {
|
|
return x1 < x2 + w2 && x1 + w1 > x2 && y1 < y2 + h2 && y1 + h1 > y2;
|
|
}
|
|
playerHit() {
|
|
if (this.gameOverFlag)
|
|
return;
|
|
this.lives--;
|
|
this.syncLivesToHUD();
|
|
this.sound.play('ao_lose', { volume: 0.4 });
|
|
this.spawnExplosion(this.playerX, this.playerY, 0x00ff66);
|
|
if (this.lives <= 0) {
|
|
this.triggerGameOver();
|
|
}
|
|
else {
|
|
this.playerAlive = false;
|
|
this.playerGfx.setVisible(false);
|
|
this.respawnTimer = 1200;
|
|
}
|
|
}
|
|
triggerGameOver() {
|
|
this.gameOverFlag = true;
|
|
this.playerAlive = false;
|
|
this.playerGfx.setVisible(false);
|
|
// Clear all bullets
|
|
for (const b of this.playerBullets)
|
|
b.gfx.destroy();
|
|
this.playerBullets = [];
|
|
for (const b of this.alienBullets)
|
|
b.gfx.destroy();
|
|
this.alienBullets = [];
|
|
this.showGameOver(this.score, () => {
|
|
this.gameOverFlag = false;
|
|
this.scene.restart();
|
|
});
|
|
}
|
|
/* ================================================================
|
|
EFFECTS
|
|
================================================================ */
|
|
spawnExplosion(x, y, color) {
|
|
this.spawnParticleExplosion(x, y, color, 10);
|
|
}
|
|
/* ================================================================
|
|
SHUTDOWN
|
|
================================================================ */
|
|
shutdown() {
|
|
super.shutdown();
|
|
// Clean up transient DOM
|
|
const banner = document.getElementById('wave-banner');
|
|
if (banner)
|
|
banner.remove();
|
|
// Clean up graphics
|
|
for (const a of this.aliens)
|
|
a.gfx?.destroy();
|
|
for (const b of this.playerBullets)
|
|
b.gfx?.destroy();
|
|
for (const b of this.alienBullets)
|
|
b.gfx?.destroy();
|
|
if (this.mystery)
|
|
this.mystery.gfx?.destroy();
|
|
for (const shield of this.shields) {
|
|
for (const block of shield)
|
|
block.gfx?.destroy();
|
|
}
|
|
}
|
|
}
|
|
//# sourceMappingURL=AlienOnslaught.js.map
|