From 33d340ef101bb838794d0394d2948163ad57bd00 Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Fri, 20 Feb 2026 22:16:39 -0700 Subject: [PATCH] =?UTF-8?q?Add=20alien=E2=80=91ship=20warp=E2=80=91out=20e?= =?UTF-8?q?ffect=20and=20bullet=20ownership=20handling?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added `warpOut()` to `AlienShip` with spin‑shrink animation and black‑hole ring visual. - Modified `spawnAlienBullet` to pass the firing ship to `Bullet`. - Extended `Bullet` constructor with `ownerEntity` to store the originating `AlienShip`. - Updated collision callbacks in `GameScene` to trigger `warpOut()` on the owning ship when its bullet hits the player or when the player collides with an alien, and to remove the ship from the alien list. - Fixed overlap order for alien bullets vs. player. These changes give alien ships a dramatic disappearance animation and ensure proper cleanup after collisions. --- js/entities/AlienShip.js | 64 +++++++++++++++++++++++++++++++++++++++- js/entities/Bullet.js | 3 +- js/scenes/GameScene.js | 17 ++++++++--- 3 files changed, 78 insertions(+), 6 deletions(-) diff --git a/js/entities/AlienShip.js b/js/entities/AlienShip.js index b845f35..a54c600 100644 --- a/js/entities/AlienShip.js +++ b/js/entities/AlienShip.js @@ -37,13 +37,75 @@ export default class AlienShip { if (time - this.lastFired > ALIEN_FIRE_RATE) { this.lastFired = time; const angle = Phaser.Math.RadToDeg(Math.atan2(dy, dx)); - this.scene.spawnAlienBullet(this.sprite.x, this.sprite.y, angle); + this.scene.spawnAlienBullet(this.sprite.x, this.sprite.y, angle, this); } // Wrap around screen this.scene.physics.world.wrap(this.sprite, 40); } + warpOut() { + if (!this.alive) return; + this.alive = false; + this.sprite.body.setVelocity(0, 0); + + const scene = this.scene; + const x = this.sprite.x; + const y = this.sprite.y; + + // Spin and shrink the sprite into the singularity + scene.tweens.add({ + targets: this.sprite, + scaleX: 0, + scaleY: 0, + angle: this.sprite.angle + 720, + duration: 500, + ease: 'Cubic.In', + onComplete: () => { + if (this.sprite && this.sprite.active) this.sprite.destroy(); + } + }); + + // Black-hole ring effect + const gfx = scene.add.graphics(); + const counter = { t: 0 }; + scene.tweens.add({ + targets: counter, + t: 1, + duration: 650, + 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; + gfx.lineStyle(1, 0x88ffff, a * 0.7); + gfx.beginPath(); + gfx.moveTo(x + Math.cos(ang) * r * 0.55, y + Math.sin(ang) * r * 0.55); + gfx.lineTo(x + Math.cos(ang) * r, y + Math.sin(ang) * r); + gfx.strokePath(); + } + }, + onComplete: () => gfx.destroy() + }); + } + destroy() { if (!this.alive) return; this.alive = false; diff --git a/js/entities/Bullet.js b/js/entities/Bullet.js index ffc1e8e..0082fbd 100644 --- a/js/entities/Bullet.js +++ b/js/entities/Bullet.js @@ -3,9 +3,10 @@ const ALIEN_BULLET_SPEED = 350; // pixels/sec – noticeably slower than player const BULLET_LIFETIME = 1800; // ms export default class Bullet { - constructor(scene, x, y, angle, owner, group) { + 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.alive = true; const textureKey = owner === 'player' ? 'bullet' : 'alien_bullet'; diff --git a/js/scenes/GameScene.js b/js/scenes/GameScene.js index eedfa3d..043e2e2 100644 --- a/js/scenes/GameScene.js +++ b/js/scenes/GameScene.js @@ -140,7 +140,7 @@ export default class GameScene extends Phaser.Scene { this.onPlayerBulletHitAlien, null, this ); this.physics.add.overlap( - this.alienBulletsGroup, this.player.sprite, + this.player.sprite, this.alienBulletsGroup, this.onAlienBulletHitPlayer, null, this ); this.physics.add.overlap( @@ -239,9 +239,9 @@ export default class GameScene extends Phaser.Scene { ); } - spawnAlienBullet(x, y, angle) { + spawnAlienBullet(x, y, angle, ownerShip = null) { this.alienBullets.push( - new Bullet(this, x, y, angle, 'alien', this.alienBulletsGroup) + new Bullet(this, x, y, angle, 'alien', this.alienBulletsGroup, ownerShip) ); } @@ -276,14 +276,20 @@ export default class GameScene extends Phaser.Scene { this.playerBullets = this.playerBullets.filter(b => b !== bullet); } - onAlienBulletHitPlayer(alienBulletSprite, playerSprite) { + onAlienBulletHitPlayer(playerSprite, alienBulletSprite) { if (!alienBulletSprite.active) return; const bullet = alienBulletSprite.gameEntity; if (!bullet || !bullet.alive) return; + const ownerShip = bullet.ownerEntity; bullet.destroy(); this.alienBullets = this.alienBullets.filter(b => b !== bullet); this.handlePlayerHit(); + + if (ownerShip && ownerShip.alive) { + ownerShip.warpOut(); + this.aliens = this.aliens.filter(a => a !== ownerShip); + } } onPlayerHitAsteroid(playerSprite, asteroidSprite) { @@ -297,7 +303,10 @@ export default class GameScene extends Phaser.Scene { if (!alienSprite.active) return; const alien = alienSprite.gameEntity; if (!alien || !alien.alive) return; + this.handlePlayerHit(); + alien.warpOut(); + this.aliens = this.aliens.filter(a => a !== alien); } handlePlayerHit() {