Add alien variants, homing missiles, asteroid speed scaling, CRT pipeline, and custom reticle

- New alien types (basic, spread, missile) with distinct speed, fire‑rate, explosion colors and textures.
- Implement spread‑shot pattern and homing‑missile behavior with steering logic.
- Asteroid constructor now accepts a speed multiplier; level speed increases each level.
- Extend Bullet to support homing missiles (new texture, lifetime, turn rate, steering).
- Introduce CRTPipeline post‑FX (barrel distortion, scanlines, vignette) and register it in the game config.
- Apply CRT pipeline to GameScene and MenuScene.
- Add custom reticle cursor graphics (rotating outer/inner layers).
- Update BootScene to generate textures for new alien types and reticle.
This commit is contained in:
Brian Fertig 2026-02-21 10:24:03 -07:00
parent dc7f18c17c
commit f194886ed9
8 changed files with 323 additions and 50 deletions

View File

@ -1,18 +1,52 @@
const ALIEN_SPEED = 130; // pixels/sec // Speed and fire-rate constants per alien type
const ALIEN_FIRE_RATE = 2500; // ms between shots const SPEED = {
basic: 130,
spread: 155,
missile: 110
};
const FIRE_RATE = {
basic: 2500,
spread: 3200,
missile: 4000
};
// Explosion ring/flash color per type
const EXPLODE_COLOR = {
basic: 0x00ffff,
spread: 0xff4400,
missile: 0xcc00ff
};
export default class AlienShip { export default class AlienShip {
constructor(scene, x, y, group) { /**
* @param {Phaser.Scene} scene
* @param {number} x
* @param {number} y
* @param {Phaser.Physics.Arcade.Group} group
* @param {'basic'|'spread'|'missile'} type
*/
constructor(scene, x, y, group, type = 'basic') {
this.scene = scene; this.scene = scene;
this.alive = true; this.alive = true;
this.type = type;
this.sprite = scene.physics.add.sprite(x, y, 'alien'); // Select texture by type
const texKey = type === 'spread' ? 'alien_spread'
: type === 'missile' ? 'alien_missile'
: 'alien';
this.sprite = scene.physics.add.sprite(x, y, texKey);
group.add(this.sprite); group.add(this.sprite);
this.sprite.gameEntity = this; this.sprite.gameEntity = this;
this.sprite.body.setAllowGravity(false); this.sprite.body.setAllowGravity(false);
// Circular hitbox: texture is 64x48, use radius 20 centered at (32,24) // Circular hitbox — missile alien is slightly larger
if (type === 'missile') {
this.sprite.body.setCircle(22, 18, 8);
} else if (type === 'spread') {
this.sprite.body.setCircle(20, 16, 7);
} else {
this.sprite.body.setCircle(20, 12, 4); this.sprite.body.setCircle(20, 12, 4);
}
this.lastFired = 0; this.lastFired = 0;
} }
@ -25,25 +59,44 @@ export default class AlienShip {
const dx = player.sprite.x - this.sprite.x; const dx = player.sprite.x - this.sprite.x;
const dy = player.sprite.y - this.sprite.y; const dy = player.sprite.y - this.sprite.y;
const dist = Math.sqrt(dx * dx + dy * dy); const dist = Math.sqrt(dx * dx + dy * dy);
const speed = SPEED[this.type];
if (dist > 5) { if (dist > 5) {
this.sprite.setVelocity( this.sprite.setVelocity(
(dx / dist) * ALIEN_SPEED, (dx / dist) * speed,
(dy / dist) * ALIEN_SPEED (dy / dist) * speed
); );
} }
// Fire at player periodically // Fire periodically
if (time - this.lastFired > ALIEN_FIRE_RATE) { if (time - this.lastFired > FIRE_RATE[this.type]) {
this.lastFired = time; this.lastFired = time;
const angle = Phaser.Math.RadToDeg(Math.atan2(dy, dx)); const angle = Phaser.Math.RadToDeg(Math.atan2(dy, dx));
if (this.type === 'spread') {
this._fireSpread(angle);
} else if (this.type === 'missile') {
this.scene.spawnHomingMissile(this.sprite.x, this.sprite.y, this);
} else {
this.scene.spawnAlienBullet(this.sprite.x, this.sprite.y, angle, this); this.scene.spawnAlienBullet(this.sprite.x, this.sprite.y, angle, this);
} }
}
// Wrap around screen // Wrap around screen
this.scene.physics.world.wrap(this.sprite, 40); this.scene.physics.world.wrap(this.sprite, 40);
} }
_fireSpread(centerAngle) {
// 5 bullets fanning out ±40° around the target direction
[-40, -20, 0, 20, 40].forEach(offset => {
this.scene.spawnAlienBullet(
this.sprite.x, this.sprite.y,
centerAngle + offset,
this
);
});
}
warpOut() { warpOut() {
if (!this.alive) return; if (!this.alive) return;
this.alive = false; this.alive = false;
@ -76,22 +129,17 @@ export default class AlienShip {
onUpdate: () => { onUpdate: () => {
const t = counter.t; const t = counter.t;
gfx.clear(); gfx.clear();
// Ring expands 0→38px over first 30%, then collapses 38→0px
const r = t < 0.3 const r = t < 0.3
? (t / 0.3) * 38 ? (t / 0.3) * 38
: ((1 - t) / 0.7) * 38; : ((1 - t) / 0.7) * 38;
if (r < 1) return; if (r < 1) return;
const a = Math.max(0, 1 - t * 0.7); const a = Math.max(0, 1 - t * 0.7);
// Dark singularity core
gfx.fillStyle(0x000000, a); gfx.fillStyle(0x000000, a);
gfx.fillCircle(x, y, r * 0.55); gfx.fillCircle(x, y, r * 0.55);
// Outer cyan ring
gfx.lineStyle(3, 0x00ffff, a); gfx.lineStyle(3, 0x00ffff, a);
gfx.strokeCircle(x, y, r); gfx.strokeCircle(x, y, r);
// Inner white ring
gfx.lineStyle(1, 0xffffff, a * 0.6); gfx.lineStyle(1, 0xffffff, a * 0.6);
gfx.strokeCircle(x, y, r * 0.55); gfx.strokeCircle(x, y, r * 0.55);
// Rotating distortion spokes
const off = t * Math.PI * 6; const off = t * Math.PI * 6;
for (let i = 0; i < 6; i++) { for (let i = 0; i < 6; i++) {
const ang = (i / 6) * Math.PI * 2 + off; const ang = (i / 6) * Math.PI * 2 + off;
@ -114,14 +162,15 @@ export default class AlienShip {
const scene = this.scene; const scene = this.scene;
const x = this.sprite.x; const x = this.sprite.x;
const y = this.sprite.y; const y = this.sprite.y;
const color = EXPLODE_COLOR[this.type];
// Brief cyan flash before implosion // Brief flash before implosion
this.sprite.setTint(0x00ffff); this.sprite.setTint(color);
scene.time.delayedCall(60, () => { scene.time.delayedCall(60, () => {
if (this.sprite && this.sprite.active) this.sprite.clearTint(); if (this.sprite && this.sprite.active) this.sprite.clearTint();
}); });
// Phase 1 (0400ms): sprite collapses and spins 360° // Phase 1: sprite collapses and spins
scene.tweens.add({ scene.tweens.add({
targets: this.sprite, targets: this.sprite,
scaleX: 0, scaleX: 0,
@ -134,7 +183,7 @@ export default class AlienShip {
} }
}); });
// Phase 2 (200700ms): cyan shockwave ring expands and fades // Phase 2: expanding shockwave ring
scene.time.delayedCall(200, () => { scene.time.delayedCall(200, () => {
const gfx = scene.add.graphics(); const gfx = scene.add.graphics();
gfx.setDepth(5); gfx.setDepth(5);
@ -148,7 +197,7 @@ export default class AlienShip {
gfx.clear(); gfx.clear();
const radius = t * 120; const radius = t * 120;
if (radius < 1) return; if (radius < 1) return;
gfx.lineStyle(3, 0x00ffff, Math.max(0, 1 - t)); gfx.lineStyle(3, color, Math.max(0, 1 - t));
gfx.strokeCircle(x, y, radius); gfx.strokeCircle(x, y, radius);
}, },
onComplete: () => gfx.destroy() onComplete: () => gfx.destroy()

View File

@ -6,7 +6,7 @@ export const ASTEROID_CONFIG = {
}; };
export default class Asteroid { export default class Asteroid {
constructor(scene, x, y, size, group) { constructor(scene, x, y, size, group, speedMultiplier = 1) {
this.scene = scene; this.scene = scene;
this.size = size; this.size = size;
this.alive = true; this.alive = true;
@ -21,8 +21,8 @@ export default class Asteroid {
const offset = cfg.texSize / 2 - cfg.radius; const offset = cfg.texSize / 2 - cfg.radius;
this.sprite.body.setCircle(cfg.radius, offset, offset); this.sprite.body.setCircle(cfg.radius, offset, offset);
// Random velocity and slow spin // Random velocity and slow spin; speedMultiplier scales up per level
const speed = cfg.minSpeed + Math.random() * (cfg.maxSpeed - cfg.minSpeed); const speed = (cfg.minSpeed + Math.random() * (cfg.maxSpeed - cfg.minSpeed)) * speedMultiplier;
const angle = Math.random() * Math.PI * 2; const angle = Math.random() * Math.PI * 2;
this.sprite.setVelocity(Math.cos(angle) * speed, Math.sin(angle) * speed); this.sprite.setVelocity(Math.cos(angle) * speed, Math.sin(angle) * speed);
this.sprite.setAngularVelocity((Math.random() - 0.5) * 60); this.sprite.setAngularVelocity((Math.random() - 0.5) * 60);

View File

@ -1,49 +1,94 @@
const PLAYER_BULLET_SPEED = 700; // pixels/sec const PLAYER_BULLET_SPEED = 700; // pixels/sec
const ALIEN_BULLET_SPEED = 350; // pixels/sec noticeably slower than player shots const ALIEN_BULLET_SPEED = 350; // pixels/sec
const MISSILE_SPEED = 180; // pixels/sec — slow enough for the player to outrun
const MISSILE_TURN_RATE = 120; // degrees/sec max steering
const BULLET_LIFETIME = 1800; // ms const BULLET_LIFETIME = 1800; // ms
const MISSILE_LIFETIME = 5000; // ms
export default class Bullet { export default class Bullet {
constructor(scene, x, y, angle, owner, group, ownerEntity = null) { constructor(scene, x, y, angle, owner, group, ownerEntity = null) {
this.scene = scene; this.scene = scene;
this.owner = owner; // 'player' or 'alien' this.owner = owner; // 'player', 'alien', or 'missile'
this.ownerEntity = ownerEntity; // AlienShip instance that fired this bullet, or null this.ownerEntity = ownerEntity;
this.alive = true; this.alive = true;
this.isHoming = (owner === 'missile');
const textureKey = owner === 'player' ? 'bullet'
: owner === 'missile' ? 'homing_missile'
: 'alien_bullet';
const textureKey = owner === 'player' ? 'bullet' : 'alien_bullet';
this.sprite = scene.physics.add.sprite(x, y, textureKey); this.sprite = scene.physics.add.sprite(x, y, textureKey);
group.add(this.sprite); group.add(this.sprite);
this.sprite.gameEntity = this; this.sprite.gameEntity = this;
this.sprite.body.setAllowGravity(false); this.sprite.body.setAllowGravity(false);
const speed = owner === 'player' ? PLAYER_BULLET_SPEED : ALIEN_BULLET_SPEED; const speed = owner === 'player' ? PLAYER_BULLET_SPEED
: owner === 'missile' ? MISSILE_SPEED
: ALIEN_BULLET_SPEED;
const rad = Phaser.Math.DegToRad(angle); const rad = Phaser.Math.DegToRad(angle);
this.sprite.setVelocity( this.sprite.setVelocity(Math.cos(rad) * speed, Math.sin(rad) * speed);
Math.cos(rad) * speed,
Math.sin(rad) * speed // Track current heading for homing missiles; rotate sprite to face direction of travel
); this.currentAngleDeg = angle;
if (this.isHoming) {
this.sprite.setAngle(angle);
}
this.createdAt = scene.time.now; this.createdAt = scene.time.now;
this.lifetime = this.isHoming ? MISSILE_LIFETIME : BULLET_LIFETIME;
} }
update(time) { update(time) {
if (!this.alive) return false; if (!this.alive) return false;
// Expire by lifetime // Expire by lifetime
if (time - this.createdAt > BULLET_LIFETIME) { if (time - this.createdAt > this.lifetime) {
this.destroy(); this.destroy();
return false; return false;
} }
// Expire if far off-screen // Expire if far off-screen
const { x, y } = this.sprite; const { x, y } = this.sprite;
if (x < -60 || x > 1660 || y < -60 || y > 960) { if (x < -80 || x > 1680 || y < -80 || y > 980) {
this.destroy(); this.destroy();
return false; return false;
} }
// Homing steering — updates velocity and rotation every frame
if (this.isHoming) {
this._steerTowardPlayer();
}
return true; return true;
} }
_steerTowardPlayer() {
const player = this.scene.player;
if (!player || !player.alive || !player.sprite.active) return;
// Cap dt to avoid huge first-frame jumps
const dt = Math.min(this.scene.game.loop.delta / 1000, 0.1);
const targetAngle = Phaser.Math.RadToDeg(
Math.atan2(
player.sprite.y - this.sprite.y,
player.sprite.x - this.sprite.x
)
);
// Rotate toward target at limited turn rate
const maxTurn = MISSILE_TURN_RATE * dt;
const diff = Phaser.Math.Angle.ShortestBetween(this.currentAngleDeg, targetAngle);
this.currentAngleDeg += Phaser.Math.Clamp(diff, -maxTurn, maxTurn);
// Apply updated velocity and rotate sprite to face direction of travel
const rad = Phaser.Math.DegToRad(this.currentAngleDeg);
this.sprite.setVelocity(Math.cos(rad) * MISSILE_SPEED, Math.sin(rad) * MISSILE_SPEED);
this.sprite.setAngle(this.currentAngleDeg);
}
destroy() { destroy() {
if (!this.alive) return; if (!this.alive) return;
this.alive = false; this.alive = false;

View File

@ -1,12 +1,14 @@
import BootScene from './scenes/BootScene.js'; import BootScene from './scenes/BootScene.js';
import MenuScene from './scenes/MenuScene.js'; import MenuScene from './scenes/MenuScene.js';
import GameScene from './scenes/GameScene.js'; import GameScene from './scenes/GameScene.js';
import CRTPipeline from './pipelines/CRTPipeline.js';
const config = { const config = {
type: Phaser.AUTO, type: Phaser.WEBGL, // Required for PostFXPipeline (CRT shader)
width: 1600, width: 1600,
height: 900, height: 900,
backgroundColor: '#000011', backgroundColor: '#000011',
pipeline: { CRTPipeline }, // Registers the pipeline by class name
physics: { physics: {
default: 'arcade', default: 'arcade',
arcade: { arcade: {

View File

@ -0,0 +1,55 @@
// CRTPipeline PostFX pipeline that simulates a classic CRT monitor:
// - Barrel distortion (curved screen edges)
// - Scanlines (every other row darkened)
// - Vignette (edge darkening)
const fragShader = `
precision mediump float;
uniform sampler2D uMainSampler;
uniform vec2 uResolution;
varying vec2 outTexCoord;
void main (void) {
// --- Screen curvature (barrel distortion) ---
// Remap UV from [0,1] to centred [-0.5,0.5], distort, remap back
vec2 uv = outTexCoord;
vec2 dc = uv - 0.5;
float strength = 0.18;
uv = uv + dc * dot(dc, dc) * strength;
// Pixels pushed outside [0,1] become black (the CRT bezel)
if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) {
gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0);
return;
}
// --- Base colour ---
vec4 color = texture2D(uMainSampler, uv);
// --- Scanlines ---
// Every other screen-pixel row is darkened by ~28%
float lineY = uv.y * uResolution.y;
float scanline = mod(floor(lineY), 2.0);
color.rgb *= mix(1.0, 0.72, scanline);
// --- Vignette ---
float vig = 1.0 - dot(dc * 1.8, dc * 1.8);
vig = clamp(vig, 0.0, 1.0);
vig = pow(vig, 0.45);
color.rgb *= vig;
gl_FragColor = color;
}
`;
export default class CRTPipeline extends Phaser.Renderer.WebGL.Pipelines.PostFXPipeline {
constructor(game) {
super({ game, name: 'CRTPipeline', fragShader });
}
onPreRender() {
this.set2f('uResolution', this.game.scale.width, this.game.scale.height);
}
}

View File

@ -79,6 +79,72 @@ export default class BootScene extends Phaser.Scene {
g.fillCircle(4, 4, 4); g.fillCircle(4, 4, 4);
g.generateTexture('alien_bullet', 8, 8); g.generateTexture('alien_bullet', 8, 8);
g.destroy(); g.destroy();
// Spread-shot alien 72×54, orange/red menacing saucer
g = this.make.graphics({ add: false });
g.lineStyle(2, 0xff4400, 1);
g.strokeEllipse(36, 34, 62, 18); // wide flat hull
g.strokeEllipse(36, 24, 38, 22); // taller upper dome
g.fillStyle(0xff8800, 1);
g.fillCircle(36, 26, 5); // central cannon port
g.lineStyle(2, 0xff2200, 1);
g.beginPath(); g.moveTo(5, 34); g.lineTo(0, 27); g.strokePath(); // left spike
g.beginPath(); g.moveTo(67, 34); g.lineTo(72, 27); g.strokePath(); // right spike
g.generateTexture('alien_spread', 72, 54);
g.destroy();
// Homing-missile alien 80×60, purple/magenta angular diamond
g = this.make.graphics({ add: false });
g.lineStyle(2, 0xcc00ff, 1);
g.strokePoints([
{ x: 40, y: 5 },
{ x: 72, y: 30 },
{ x: 40, y: 55 },
{ x: 8, y: 30 }
], true);
g.lineStyle(1, 0x9900cc, 1);
g.strokePoints([
{ x: 40, y: 18 },
{ x: 58, y: 30 },
{ x: 40, y: 42 },
{ x: 22, y: 30 }
], true);
g.fillStyle(0xff00ff, 1);
g.fillCircle(40, 30, 5);
g.generateTexture('alien_missile', 80, 60);
g.destroy();
// Homing missile projectile 14×6, magenta body + orange nose
g = this.make.graphics({ add: false });
g.fillStyle(0xff00ff, 1);
g.fillRect(0, 1, 10, 4);
g.fillStyle(0xff8800, 1);
g.fillRect(10, 0, 4, 6);
g.generateTexture('homing_missile', 14, 6);
g.destroy();
// Reticle outer layer outer ring + diagonal ticks (rotates one way)
g = this.make.graphics({ add: false });
g.lineStyle(1.5, 0x00ff44, 1);
g.strokeCircle(32, 32, 28);
g.lineStyle(1, 0x00ff44, 0.5);
g.beginPath(); g.moveTo(12, 12); g.lineTo(18, 18); g.strokePath();
g.beginPath(); g.moveTo(52, 12); g.lineTo(46, 18); g.strokePath();
g.beginPath(); g.moveTo(12, 52); g.lineTo(18, 46); g.strokePath();
g.beginPath(); g.moveTo(52, 52); g.lineTo(46, 46); g.strokePath();
g.generateTexture('reticle_outer', 64, 64);
g.destroy();
// Reticle inner layer inner ring + cross lines (rotates opposite way)
g = this.make.graphics({ add: false });
g.lineStyle(1.5, 0x00ff44, 1);
g.strokeCircle(32, 32, 6);
g.beginPath(); g.moveTo(32, 2); g.lineTo(32, 24); g.strokePath(); // top
g.beginPath(); g.moveTo(32, 40); g.lineTo(32, 62); g.strokePath(); // bottom
g.beginPath(); g.moveTo( 2, 32); g.lineTo(24, 32); g.strokePath(); // left
g.beginPath(); g.moveTo(40, 32); g.lineTo(62, 32); g.strokePath(); // right
g.generateTexture('reticle_inner', 64, 64);
g.destroy();
} }
_asteroidPoints(cx, cy, radius, numPoints, offsets) { _asteroidPoints(cx, cy, radius, numPoints, offsets) {

View File

@ -17,6 +17,7 @@ export default class GameScene extends Phaser.Scene {
this.lives = STARTING_LIVES; this.lives = STARTING_LIVES;
this.gameOver = false; this.gameOver = false;
this.levelComplete = false; this.levelComplete = false;
this.levelSpeedMult = 1;
// Physics groups (used for overlap detection) // Physics groups (used for overlap detection)
this.asteroidsGroup = this.physics.add.group(); this.asteroidsGroup = this.physics.add.group();
@ -42,6 +43,23 @@ export default class GameScene extends Phaser.Scene {
callbackScope: this, callbackScope: this,
loop: true loop: true
}); });
// Apply CRT post-processing (scanlines + screen curvature)
if (this.renderer.type === Phaser.WEBGL) {
this.cameras.main.setPostPipeline('CRTPipeline');
}
// Custom reticle cursor hide the OS cursor and draw our own
this.game.canvas.style.cursor = 'none';
this.reticleOuter = this.add.image(800, 450, 'reticle_outer').setDepth(20);
this.reticleInner = this.add.image(800, 450, 'reticle_inner').setDepth(20);
this.tweens.add({ targets: this.reticleOuter, angle: 360, duration: 8000, repeat: -1, ease: 'Linear' });
this.tweens.add({ targets: this.reticleInner, angle: -360, duration: 12000, repeat: -1, ease: 'Linear' });
this.events.once('shutdown', () => {
this.game.canvas.style.cursor = 'default';
this.reticleOuter.destroy();
this.reticleInner.destroy();
});
} }
// ─── Collision Setup ───────────────────────────────────────────────────── // ─── Collision Setup ─────────────────────────────────────────────────────
@ -109,12 +127,20 @@ export default class GameScene extends Phaser.Scene {
// Respawn player at center // Respawn player at center
this.player.respawn(800, 450); this.player.respawn(800, 450);
// Spawn large asteroids more per level // Progressive asteroid difficulty: more rocks, mixed sizes, faster per level
const count = 3 + this.level; this.levelSpeedMult = Math.min(1 + (this.level - 1) * 0.12, 2.0);
for (let i = 0; i < count; i++) {
const largeCount = Math.max(3, 2 + this.level);
const mediumCount = Math.floor(this.level / 2);
const smallCount = Math.floor(this.level / 3);
const spawnOne = (size) => {
const { x, y } = this.edgePosition(); const { x, y } = this.edgePosition();
this.asteroids.push(new Asteroid(this, x, y, 'large', this.asteroidsGroup)); this.asteroids.push(new Asteroid(this, x, y, size, this.asteroidsGroup, this.levelSpeedMult));
} };
for (let i = 0; i < largeCount; i++) spawnOne('large');
for (let i = 0; i < mediumCount; i++) spawnOne('medium');
for (let i = 0; i < smallCount; i++) spawnOne('small');
this.updateUI(); this.updateUI();
this.showMessage(`LEVEL ${this.level}`, 2000); this.showMessage(`LEVEL ${this.level}`, 2000);
@ -146,7 +172,17 @@ export default class GameScene extends Phaser.Scene {
if (this.gameOver || this.levelComplete) return; if (this.gameOver || this.levelComplete) return;
if (this.aliens.length >= 2) return; if (this.aliens.length >= 2) return;
const { x, y } = this.edgePosition(); const { x, y } = this.edgePosition();
this.aliens.push(new AlienShip(this, x, y, this.aliensGroup));
// Select alien type by level: missile aliens at 5+, spread at 3+
let type = 'basic';
if (this.level >= 5) {
const r = Math.random();
type = r < 0.40 ? 'missile' : r < 0.75 ? 'spread' : 'basic';
} else if (this.level >= 3) {
type = Math.random() < 0.5 ? 'spread' : 'basic';
}
this.aliens.push(new AlienShip(this, x, y, this.aliensGroup, type));
} }
spawnPlayerBullet(x, y, angle) { spawnPlayerBullet(x, y, angle) {
@ -161,6 +197,16 @@ export default class GameScene extends Phaser.Scene {
); );
} }
spawnHomingMissile(x, y, ownerShip) {
const player = this.player;
const angle = Phaser.Math.RadToDeg(
Math.atan2(player.sprite.y - y, player.sprite.x - x)
);
this.alienBullets.push(
new Bullet(this, x, y, angle, 'missile', this.alienBulletsGroup, ownerShip)
);
}
// ─── Collision Callbacks ───────────────────────────────────────────────── // ─── Collision Callbacks ─────────────────────────────────────────────────
onPlayerBulletHitAsteroid(bulletSprite, asteroidSprite) { onPlayerBulletHitAsteroid(bulletSprite, asteroidSprite) {
@ -255,11 +301,11 @@ export default class GameScene extends Phaser.Scene {
const count = Phaser.Math.Between(1, 3); const count = Phaser.Math.Between(1, 3);
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
const size = Math.random() < 0.5 ? 'medium' : 'small'; const size = Math.random() < 0.5 ? 'medium' : 'small';
this.asteroids.push(new Asteroid(this, x, y, size, this.asteroidsGroup)); this.asteroids.push(new Asteroid(this, x, y, size, this.asteroidsGroup, this.levelSpeedMult));
} }
} else if (asteroid.size === 'medium') { } else if (asteroid.size === 'medium') {
for (let i = 0; i < 2; i++) { for (let i = 0; i < 2; i++) {
this.asteroids.push(new Asteroid(this, x, y, 'small', this.asteroidsGroup)); this.asteroids.push(new Asteroid(this, x, y, 'small', this.asteroidsGroup, this.levelSpeedMult));
} }
} }
// small: fully destroyed no fragments // small: fully destroyed no fragments
@ -347,6 +393,11 @@ export default class GameScene extends Phaser.Scene {
// ─── Main Update Loop ──────────────────────────────────────────────────── // ─── Main Update Loop ────────────────────────────────────────────────────
update(time, delta) { update(time, delta) {
// Track reticle to mouse pointer (always, even on game over)
const ptr = this.input.activePointer;
this.reticleOuter.setPosition(ptr.x, ptr.y);
this.reticleInner.setPosition(ptr.x, ptr.y);
if (this.gameOver) return; if (this.gameOver) return;
this.player.update(time, delta); this.player.update(time, delta);

View File

@ -41,6 +41,11 @@ export default class MenuScene extends Phaser.Scene {
this._drawTitle(); this._drawTitle();
this._drawControlsPanel(); this._drawControlsPanel();
this._drawStartButton(); this._drawStartButton();
// Apply CRT post-processing (scanlines + screen curvature)
if (this.renderer.type === Phaser.WEBGL) {
this.cameras.main.setPostPipeline('CRTPipeline');
}
} }
update() { update() {