Asteroids-2026/js/entities/Player.js

135 lines
4.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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.sprite.setActive(false).setVisible(false);
this.sprite.body.setVelocity(0, 0);
// Go invincible right away so no further collision callbacks count
this.invincible = true;
this.invincibleUntil = this.scene.time.now + 99999;
}
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();
}
}
}