class BootScene extends Phaser.Scene { constructor() { super('BootScene'); } preload() { // Load any minimal assets here } create() { this.scene.start('PreloaderScene'); } } class PreloaderScene extends Phaser.Scene { constructor() { super('PreloaderScene'); } preload() { this.load.image('background', 'assets/background.png'); // Load individual character sprites with NEW NAMES this.load.image('loki', 'assets/loki.png'); this.load.image('mio', 'assets/mio.png'); this.load.image('nike', 'assets/nike.png'); this.load.image('svea', 'assets/svea.png'); } create() { // Create a simple texture for particles const graphics = this.make.graphics({ x: 0, y: 0, add: false }); graphics.fillStyle(0xffaa00, 1); graphics.fillCircle(4, 4, 4); graphics.generateTexture('flare', 8, 8); // Create a texture for the meteor const meteorGraphics = this.make.graphics({ x: 0, y: 0, add: false }); meteorGraphics.fillStyle(0x8B4513, 1); // Brownish meteorGraphics.fillCircle(32, 32, 30); meteorGraphics.fillStyle(0xFFA500, 1); // Orange craters/spots meteorGraphics.fillCircle(20, 20, 8); meteorGraphics.fillCircle(45, 40, 6); meteorGraphics.fillCircle(25, 45, 5); meteorGraphics.generateTexture('meteor', 64, 64); this.scene.start('MenuScene'); } } class MenuScene extends Phaser.Scene { constructor() { super('MenuScene'); } create() { this.add.image(180, 320, 'background').setDisplaySize(360, 640); this.add.text(180, 100, 'COUSIN CRISIS', { fontSize: '32px', fill: '#fff', fontStyle: 'bold' }) .setOrigin(0.5) .setShadow(2, 2, '#000', 2, true, true); this.add.text(180, 150, 'Select Character', { fontSize: '24px', fill: '#fff' }) .setOrigin(0.5) .setShadow(2, 2, '#000', 2, true, true); const characters = [ { name: 'Loki', key: 'loki' }, { name: 'Mio', key: 'mio' }, { name: 'Nike', key: 'nike' }, { name: 'Svea', key: 'svea' } ]; let x = 60; let y = 300; characters.forEach((char, index) => { const sprite = this.add.sprite(x + (index * 80), y, char.key).setInteractive(); // Force resizing in code to be safe sprite.setDisplaySize(64, 64); this.add.text(x + (index * 80), y + 60, char.name, { fontSize: '16px', fill: '#fff' }) .setOrigin(0.5) .setShadow(1, 1, '#000', 1, true, true); sprite.on('pointerdown', () => { this.scene.start('GameScene', { character: char.key }); }); sprite.on('pointerover', () => sprite.setTint(0xcccccc)); sprite.on('pointerout', () => sprite.clearTint()); }); } } class GameScene extends Phaser.Scene { constructor() { super('GameScene'); } init(data) { this.selectedCharKey = data.character; this.score = 0; this.gameOver = false; } create() { console.log("Game Created"); this.bg = this.add.tileSprite(180, 320, 360, 640, 'background'); this.player = this.physics.add.sprite(180, 550, this.selectedCharKey); this.player.setCollideWorldBounds(true); // Force scaling because ImageMagick convert might have failed this.player.setDisplaySize(64, 64); // Since we are scaling a potentially huge image, we MUST update the body // But wait, setDisplaySize DOES update the body size visually, but for Arcade physics // 2. FIX BODY: Reset body to match the 64x64 visual. // Arcade Physics body size usually matches texture size by default, but let's be explicit. this.player.body.setSize(40, 40); // Slightly smaller hitbox than visual this.player.body.setOffset(12, 12); // Center it ( (64-40)/2 = 12 ) console.log("Player Body:", this.player.body); this.meteors = this.physics.add.group(); this.cursors = this.input.keyboard.createCursorKeys(); this.scoreText = this.add.text(16, 16, 'Score: 0', { fontSize: '24px', fill: '#fff' }) .setShadow(2, 2, '#000', 2, true, true); this.time.addEvent({ delay: 1000, callback: this.spawnMeteor, callbackScope: this, loop: true }); this.time.addEvent({ delay: 100, callback: () => { if (!this.gameOver) { this.score += 1; this.scoreText.setText('Score: ' + this.score); } }, loop: true }); // Re-enable collision this.physics.add.collider(this.player, this.meteors, this.hitMeteor, null, this); } update() { if (this.gameOver) return; // DEBUG: Log body size occasionally if (this.score % 10 === 0 && this.score > 0) { console.log(`Score: ${this.score}, Player Body:`, this.player.body.width, this.player.body.height, this.player.body.x, this.player.body.y); } this.bg.tilePositionY -= 2; this.player.setVelocityX(0); const pointer = this.input.activePointer; const isLeftTouch = pointer.isDown && pointer.x < 180; const isRightTouch = pointer.isDown && pointer.x >= 180; if (this.cursors.left.isDown || isLeftTouch) { this.player.setVelocityX(-250); } else if (this.cursors.right.isDown || isRightTouch) { this.player.setVelocityX(250); } this.meteors.children.iterate((meteor) => { if (meteor && meteor.y > 650) { // Modern Phaser particle cleanup if we add it back if (meteor.particleEmitter) { meteor.particleEmitter.stop(); meteor.particleEmitter.destroy(); // Cleanup emitter } meteor.destroy(); } }); } spawnMeteor() { if (this.gameOver) return; try { const x = Phaser.Math.Between(30, 330); const meteor = this.meteors.create(x, -50, 'meteor'); meteor.setDisplaySize(48, 48); meteor.body.setSize(40, 40); // Meteor hitbox meteor.body.setOffset(4, 4); meteor.setVelocityY(Phaser.Math.Between(200, 400)); meteor.setVelocityX(Phaser.Math.Between(-50, 50)); meteor.setAngularVelocity(Phaser.Math.Between(-100, 100)); // FIX CRASH: The previous particle code was for Phaser < 3.60. // The CDN is likely serving Phaser 3.80+. // Simplified particle creation for modern Phaser: const emitter = this.add.particles(0, 0, 'flare', { speed: 50, scale: { start: 0.6, end: 0 }, alpha: { start: 1, end: 0 }, lifespan: 600, blendMode: 'ADD', follow: meteor }); // Attach emitter to meteor so we can destroy it later meteor.particleEmitter = emitter; } catch (e) { console.error("Error spawning meteor:", e); } } hitMeteor(player, meteor) { console.log("HIT METEOR!"); this.physics.pause(); player.setTint(0xff0000); this.gameOver = true; // Simple explosion using new syntax this.add.particles(player.x, player.y, 'flare', { speed: { min: -100, max: 100 }, angle: { min: 0, max: 360 }, scale: { start: 1, end: 0 }, blendMode: 'SCREEN', lifespan: 1000, quantity: 30, emitting: false }).explode(); this.time.delayedCall(1500, () => { this.scene.start('GameOverScene', { score: this.score }); }); } } class GameOverScene extends Phaser.Scene { constructor() { super('GameOverScene'); } init(data) { this.score = data.score; } create() { this.add.image(180, 320, 'background').setDisplaySize(360, 640).setTint(0x555555); this.add.text(180, 200, 'GAME OVER', { fontSize: '40px', fill: '#ff0000', fontStyle: 'bold' }) .setOrigin(0.5) .setShadow(2, 2, '#000', 2, true, true); this.add.text(180, 300, 'Score: ' + this.score, { fontSize: '32px', fill: '#fff' }) .setOrigin(0.5) .setShadow(2, 2, '#000', 2, true, true); const restartBtn = this.add.text(180, 400, 'Restart', { fontSize: '28px', fill: '#0f0' }) .setOrigin(0.5) .setShadow(2, 2, '#000', 2, true, true) .setInteractive() .on('pointerdown', () => this.scene.start('MenuScene')); restartBtn.on('pointerover', () => restartBtn.setStyle({ fill: '#fff' })); restartBtn.on('pointerout', () => restartBtn.setStyle({ fill: '#0f0' })); } } const config = { type: Phaser.AUTO, width: 360, height: 640, backgroundColor: "#222", physics: { default: "arcade", arcade: { // Fixed: was 'default' inside arcade gravity: { y: 0 }, debug: false } }, scene: [BootScene, PreloaderScene, MenuScene, GameScene, GameOverScene] }; const game = new Phaser.Game(config);