// 360 degrees per 2 seconds = 180 deg/sec const ROTATION_SPEED = 180; // Acceleration in pixels/sec² when thrusting const THRUST_ACCEL = 200; // Drag in pixels/sec² when coasting (slow deceleration) const DRAG = 35; const MAX_SPEED = 420; const FIRE_RATE = 280; // ms between shots // Nose is 24px ahead of sprite center in texture-local space const NOSE_OFFSET = 24; const INVINCIBLE_DURATION = 3000; // ms export default class Player { constructor(scene, x, y) { this.scene = scene; this.alive = true; // Texture is 64x32; ship nose points right (angle=0) this.sprite = scene.physics.add.sprite(x, y, 'player'); this.sprite.body.setAllowGravity(false); this.sprite.body.setDrag(DRAG, DRAG); // Hitbox: circle radius 13 centered at texture center (32,16) this.sprite.body.setCircle(13, 19, 3); // Start pointing up (-90°) this.angle = -90; this.sprite.angle = this.angle; this.spaceKey = scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.SPACE); this.aKey = scene.input.keyboard.addKey(Phaser.Input.Keyboard.KeyCodes.A); this.lastFired = 0; this.invincible = false; this.invincibleUntil = 0; } update(time, delta) { if (!this.alive) return; const dt = delta / 1000; // Invincibility flicker if (this.invincible) { if (time > this.invincibleUntil) { this.invincible = false; this.sprite.setAlpha(1); } else { this.sprite.setAlpha(Math.sin(time / 80) > 0 ? 1 : 0.15); } } // Rotate toward mouse cursor const pointer = this.scene.input.activePointer; const world = this.scene.cameras.main.getWorldPoint(pointer.x, pointer.y); const targetAngle = Phaser.Math.RadToDeg( Math.atan2(world.y - this.sprite.y, world.x - this.sprite.x) ); const maxRot = ROTATION_SPEED * dt; const diff = Phaser.Math.Angle.ShortestBetween(this.angle, targetAngle); this.angle += Phaser.Math.Clamp(diff, -maxRot, maxRot); this.sprite.angle = this.angle; // Thrust on left mouse button const thrusting = pointer.leftButtonDown(); if (thrusting) { const rad = Phaser.Math.DegToRad(this.angle); this.sprite.body.velocity.x += Math.cos(rad) * THRUST_ACCEL * dt; this.sprite.body.velocity.y += Math.sin(rad) * THRUST_ACCEL * dt; this.sprite.setTexture('player_thrust'); } else { this.sprite.setTexture('player'); } // Cap total speed const speed = this.sprite.body.speed; if (speed > MAX_SPEED) { this.sprite.body.velocity.scale(MAX_SPEED / speed); } // Wrap around screen this.scene.physics.world.wrap(this.sprite, 30); // Fire: hold key fires at fixed rate if (this.spaceKey.isDown || this.aKey.isDown) { if (time - this.lastFired > FIRE_RATE) { this.lastFired = time; const rad = Phaser.Math.DegToRad(this.angle); this.scene.spawnPlayerBullet( this.sprite.x + Math.cos(rad) * NOSE_OFFSET, this.sprite.y + Math.sin(rad) * NOSE_OFFSET, this.angle ); } } } // Returns true if the hit should count (not invincible) hit() { return !this.invincible; } // Hide the ship and go invincible immediately – call before delaying respawn die() { this.alive = false; this.invincible = true; this.invincibleUntil = this.scene.time.now + 99999; this.sprite.body.setVelocity(0, 0); const x = this.sprite.x; const y = this.sprite.y; const angle = this.sprite.angle; // White flash for 100ms, then hide this.sprite.setTint(0xffffff); this.scene.time.delayedCall(100, () => { if (this.sprite && this.sprite.active) { this.sprite.clearTint(); this.sprite.setActive(false).setVisible(false); } }); this._spawnShatterShards(x, y, angle); } _spawnShatterShards(x, y, angle) { const scene = this.scene; const defs = [ { angleOffset: -115, dist: 65, size: 7, spin: 270 }, { angleOffset: 115, dist: 65, size: 9, spin: -270 }, { angleOffset: 175, dist: 80, size: 11, spin: 240 }, ]; defs.forEach((def, i) => { const gfx = scene.add.graphics(); gfx.setDepth(5); gfx.fillStyle(0x00ff44, 1); const s = def.size; if (i === 0) gfx.fillTriangle(0, -s, -s * 0.6, s * 0.5, s * 0.4, s * 0.5); else if (i === 1) gfx.fillTriangle(s * 0.5, -s * 0.8, -s * 0.5, -s * 0.3, 0, s); else gfx.fillTriangle(-s * 0.7, 0, s * 0.7, 0, 0, s); gfx.x = x; gfx.y = y; const rad = Phaser.Math.DegToRad(angle + def.angleOffset); scene.tweens.add({ targets: gfx, x: x + Math.cos(rad) * def.dist, y: y + Math.sin(rad) * def.dist, angle: gfx.angle + def.spin, alpha: 0, duration: 800, ease: 'Quad.Out', onComplete: () => gfx.destroy() }); }); } makeInvincible() { this.invincible = true; this.invincibleUntil = this.scene.time.now + INVINCIBLE_DURATION; this.sprite.setAlpha(1); } respawn(x, y) { this.alive = true; this.sprite.setPosition(x, y); this.sprite.body.setVelocity(0, 0); this.sprite.setActive(true).setVisible(true); this.angle = -90; this.sprite.angle = this.angle; this.makeInvincible(); } destroy() { this.alive = false; if (this.sprite && this.sprite.active) { this.sprite.destroy(); } } }