Asteroids-2026/js/entities/Player.js

125 lines
4.1 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;
}
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();
}
}
}