181 lines
6.0 KiB
JavaScript
181 lines
6.0 KiB
JavaScript
// 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();
|
||
}
|
||
}
|
||
}
|