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:
parent
dc7f18c17c
commit
f194886ed9
|
|
@ -1,18 +1,52 @@
|
|||
const ALIEN_SPEED = 130; // pixels/sec
|
||||
const ALIEN_FIRE_RATE = 2500; // ms between shots
|
||||
// Speed and fire-rate constants per alien type
|
||||
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 {
|
||||
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.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);
|
||||
this.sprite.gameEntity = this;
|
||||
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.lastFired = 0;
|
||||
}
|
||||
|
|
@ -25,25 +59,44 @@ export default class AlienShip {
|
|||
const dx = player.sprite.x - this.sprite.x;
|
||||
const dy = player.sprite.y - this.sprite.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
const speed = SPEED[this.type];
|
||||
|
||||
if (dist > 5) {
|
||||
this.sprite.setVelocity(
|
||||
(dx / dist) * ALIEN_SPEED,
|
||||
(dy / dist) * ALIEN_SPEED
|
||||
(dx / dist) * speed,
|
||||
(dy / dist) * speed
|
||||
);
|
||||
}
|
||||
|
||||
// Fire at player periodically
|
||||
if (time - this.lastFired > ALIEN_FIRE_RATE) {
|
||||
// Fire periodically
|
||||
if (time - this.lastFired > FIRE_RATE[this.type]) {
|
||||
this.lastFired = time;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap around screen
|
||||
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() {
|
||||
if (!this.alive) return;
|
||||
this.alive = false;
|
||||
|
|
@ -76,22 +129,17 @@ export default class AlienShip {
|
|||
onUpdate: () => {
|
||||
const t = counter.t;
|
||||
gfx.clear();
|
||||
// Ring expands 0→38px over first 30%, then collapses 38→0px
|
||||
const r = t < 0.3
|
||||
? (t / 0.3) * 38
|
||||
: ((1 - t) / 0.7) * 38;
|
||||
if (r < 1) return;
|
||||
const a = Math.max(0, 1 - t * 0.7);
|
||||
// Dark singularity core
|
||||
gfx.fillStyle(0x000000, a);
|
||||
gfx.fillCircle(x, y, r * 0.55);
|
||||
// Outer cyan ring
|
||||
gfx.lineStyle(3, 0x00ffff, a);
|
||||
gfx.strokeCircle(x, y, r);
|
||||
// Inner white ring
|
||||
gfx.lineStyle(1, 0xffffff, a * 0.6);
|
||||
gfx.strokeCircle(x, y, r * 0.55);
|
||||
// Rotating distortion spokes
|
||||
const off = t * Math.PI * 6;
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const ang = (i / 6) * Math.PI * 2 + off;
|
||||
|
|
@ -114,14 +162,15 @@ export default class AlienShip {
|
|||
const scene = this.scene;
|
||||
const x = this.sprite.x;
|
||||
const y = this.sprite.y;
|
||||
const color = EXPLODE_COLOR[this.type];
|
||||
|
||||
// Brief cyan flash before implosion
|
||||
this.sprite.setTint(0x00ffff);
|
||||
// Brief flash before implosion
|
||||
this.sprite.setTint(color);
|
||||
scene.time.delayedCall(60, () => {
|
||||
if (this.sprite && this.sprite.active) this.sprite.clearTint();
|
||||
});
|
||||
|
||||
// Phase 1 (0–400ms): sprite collapses and spins 360°
|
||||
// Phase 1: sprite collapses and spins
|
||||
scene.tweens.add({
|
||||
targets: this.sprite,
|
||||
scaleX: 0,
|
||||
|
|
@ -134,7 +183,7 @@ export default class AlienShip {
|
|||
}
|
||||
});
|
||||
|
||||
// Phase 2 (200–700ms): cyan shockwave ring expands and fades
|
||||
// Phase 2: expanding shockwave ring
|
||||
scene.time.delayedCall(200, () => {
|
||||
const gfx = scene.add.graphics();
|
||||
gfx.setDepth(5);
|
||||
|
|
@ -148,7 +197,7 @@ export default class AlienShip {
|
|||
gfx.clear();
|
||||
const radius = t * 120;
|
||||
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);
|
||||
},
|
||||
onComplete: () => gfx.destroy()
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ export const ASTEROID_CONFIG = {
|
|||
};
|
||||
|
||||
export default class Asteroid {
|
||||
constructor(scene, x, y, size, group) {
|
||||
constructor(scene, x, y, size, group, speedMultiplier = 1) {
|
||||
this.scene = scene;
|
||||
this.size = size;
|
||||
this.alive = true;
|
||||
|
|
@ -21,8 +21,8 @@ export default class Asteroid {
|
|||
const offset = cfg.texSize / 2 - cfg.radius;
|
||||
this.sprite.body.setCircle(cfg.radius, offset, offset);
|
||||
|
||||
// Random velocity and slow spin
|
||||
const speed = cfg.minSpeed + Math.random() * (cfg.maxSpeed - cfg.minSpeed);
|
||||
// Random velocity and slow spin; speedMultiplier scales up per level
|
||||
const speed = (cfg.minSpeed + Math.random() * (cfg.maxSpeed - cfg.minSpeed)) * speedMultiplier;
|
||||
const angle = Math.random() * Math.PI * 2;
|
||||
this.sprite.setVelocity(Math.cos(angle) * speed, Math.sin(angle) * speed);
|
||||
this.sprite.setAngularVelocity((Math.random() - 0.5) * 60);
|
||||
|
|
|
|||
|
|
@ -1,49 +1,94 @@
|
|||
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 MISSILE_LIFETIME = 5000; // ms
|
||||
|
||||
export default class Bullet {
|
||||
constructor(scene, x, y, angle, owner, group, ownerEntity = null) {
|
||||
this.scene = scene;
|
||||
this.owner = owner; // 'player' or 'alien'
|
||||
this.ownerEntity = ownerEntity; // AlienShip instance that fired this bullet, or null
|
||||
this.owner = owner; // 'player', 'alien', or 'missile'
|
||||
this.ownerEntity = ownerEntity;
|
||||
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);
|
||||
group.add(this.sprite);
|
||||
this.sprite.gameEntity = this;
|
||||
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);
|
||||
this.sprite.setVelocity(
|
||||
Math.cos(rad) * speed,
|
||||
Math.sin(rad) * speed
|
||||
);
|
||||
this.sprite.setVelocity(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.lifetime = this.isHoming ? MISSILE_LIFETIME : BULLET_LIFETIME;
|
||||
}
|
||||
|
||||
update(time) {
|
||||
if (!this.alive) return false;
|
||||
|
||||
// Expire by lifetime
|
||||
if (time - this.createdAt > BULLET_LIFETIME) {
|
||||
if (time - this.createdAt > this.lifetime) {
|
||||
this.destroy();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Expire if far off-screen
|
||||
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();
|
||||
return false;
|
||||
}
|
||||
|
||||
// Homing steering — updates velocity and rotation every frame
|
||||
if (this.isHoming) {
|
||||
this._steerTowardPlayer();
|
||||
}
|
||||
|
||||
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() {
|
||||
if (!this.alive) return;
|
||||
this.alive = false;
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import BootScene from './scenes/BootScene.js';
|
||||
import MenuScene from './scenes/MenuScene.js';
|
||||
import GameScene from './scenes/GameScene.js';
|
||||
import CRTPipeline from './pipelines/CRTPipeline.js';
|
||||
|
||||
const config = {
|
||||
type: Phaser.AUTO,
|
||||
type: Phaser.WEBGL, // Required for PostFXPipeline (CRT shader)
|
||||
width: 1600,
|
||||
height: 900,
|
||||
backgroundColor: '#000011',
|
||||
pipeline: { CRTPipeline }, // Registers the pipeline by class name
|
||||
physics: {
|
||||
default: 'arcade',
|
||||
arcade: {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -79,6 +79,72 @@ export default class BootScene extends Phaser.Scene {
|
|||
g.fillCircle(4, 4, 4);
|
||||
g.generateTexture('alien_bullet', 8, 8);
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ export default class GameScene extends Phaser.Scene {
|
|||
this.lives = STARTING_LIVES;
|
||||
this.gameOver = false;
|
||||
this.levelComplete = false;
|
||||
this.levelSpeedMult = 1;
|
||||
|
||||
// Physics groups (used for overlap detection)
|
||||
this.asteroidsGroup = this.physics.add.group();
|
||||
|
|
@ -42,6 +43,23 @@ export default class GameScene extends Phaser.Scene {
|
|||
callbackScope: this,
|
||||
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 ─────────────────────────────────────────────────────
|
||||
|
|
@ -109,12 +127,20 @@ export default class GameScene extends Phaser.Scene {
|
|||
// Respawn player at center
|
||||
this.player.respawn(800, 450);
|
||||
|
||||
// Spawn large asteroids – more per level
|
||||
const count = 3 + this.level;
|
||||
for (let i = 0; i < count; i++) {
|
||||
// Progressive asteroid difficulty: more rocks, mixed sizes, faster per level
|
||||
this.levelSpeedMult = Math.min(1 + (this.level - 1) * 0.12, 2.0);
|
||||
|
||||
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();
|
||||
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.showMessage(`LEVEL ${this.level}`, 2000);
|
||||
|
|
@ -146,7 +172,17 @@ export default class GameScene extends Phaser.Scene {
|
|||
if (this.gameOver || this.levelComplete) return;
|
||||
if (this.aliens.length >= 2) return;
|
||||
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) {
|
||||
|
|
@ -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 ─────────────────────────────────────────────────
|
||||
|
||||
onPlayerBulletHitAsteroid(bulletSprite, asteroidSprite) {
|
||||
|
|
@ -255,11 +301,11 @@ export default class GameScene extends Phaser.Scene {
|
|||
const count = Phaser.Math.Between(1, 3);
|
||||
for (let i = 0; i < count; i++) {
|
||||
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') {
|
||||
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
|
||||
|
|
@ -347,6 +393,11 @@ export default class GameScene extends Phaser.Scene {
|
|||
// ─── Main Update Loop ────────────────────────────────────────────────────
|
||||
|
||||
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;
|
||||
|
||||
this.player.update(time, delta);
|
||||
|
|
|
|||
|
|
@ -41,6 +41,11 @@ export default class MenuScene extends Phaser.Scene {
|
|||
this._drawTitle();
|
||||
this._drawControlsPanel();
|
||||
this._drawStartButton();
|
||||
|
||||
// Apply CRT post-processing (scanlines + screen curvature)
|
||||
if (this.renderer.type === Phaser.WEBGL) {
|
||||
this.cameras.main.setPostPipeline('CRTPipeline');
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
|
|
|
|||
Loading…
Reference in New Issue