overrun/js/entities/Player.js

172 lines
4.6 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.

const TURN_RATE_DEG = 4; // degrees per frame at 60fps
const BULLET_SPEED = 700;
const BULLET_DAMAGE = 20;
const BULLET_SIZE = 5;
const INVINCIBILITY_MS = 1500;
const PLAYER_RADIUS = 16;
const PLAYER_COLOR = 0x00ccff;
const BARREL_LENGTH = 22;
export class Player {
constructor(scene, x, y) {
this.scene = scene;
this.facing = 0; // radians
// Mutable stat block — modified by skill tree
this.stats = {
speed: 200,
damage: BULLET_DAMAGE,
fireRate: 1, // multiplier applied to cooldown
damageReduction: 0, // 01 additive
maxHp: 100,
};
this.hp = this.stats.maxHp;
this.lives = 3;
this.invincible = false;
this._fireCooldown = 0;
this._baseFireInterval = 250; // ms between shots
this._buildGraphics(x, y);
this._setupKeys();
this.bullets = scene.add.group();
}
_buildGraphics(x, y) {
this.body = this.scene.add.circle(x, y, PLAYER_RADIUS, PLAYER_COLOR);
this.barrel = this.scene.add.rectangle(x + BARREL_LENGTH / 2, y, BARREL_LENGTH, 5, 0x0088cc);
this.scene.physics.add.existing(this.body);
this.body.body.setCollideWorldBounds(true);
this.body.body.setCircle(PLAYER_RADIUS);
}
_setupKeys() {
this.keys = this.scene.input.keyboard.addKeys({
up: Phaser.Input.Keyboard.KeyCodes.W,
down: Phaser.Input.Keyboard.KeyCodes.S,
left: Phaser.Input.Keyboard.KeyCodes.A,
right: Phaser.Input.Keyboard.KeyCodes.D,
});
}
get x() { return this.body.x; }
get y() { return this.body.y; }
get active() { return this.body.active; }
update(delta) {
this._move();
this._rotateFacing(delta);
this._updateBarrel();
this._handleFire(delta);
this._updateBullets();
}
_move() {
let dx = 0, dy = 0;
if (this.keys.left.isDown) dx -= 1;
if (this.keys.right.isDown) dx += 1;
if (this.keys.up.isDown) dy -= 1;
if (this.keys.down.isDown) dy += 1;
if (dx !== 0 && dy !== 0) { dx *= 0.707; dy *= 0.707; }
this.body.body.setVelocity(dx * this.stats.speed, dy * this.stats.speed);
}
_rotateFacing(delta) {
const ptr = this.scene.input.activePointer;
const targetAngle = Phaser.Math.Angle.Between(this.x, this.y, ptr.worldX, ptr.worldY);
const maxTurn = Phaser.Math.DegToRad(TURN_RATE_DEG) * (delta / (1000 / 60));
this.facing = Phaser.Math.Angle.RotateTo(this.facing, targetAngle, maxTurn);
}
_updateBarrel() {
const bx = this.x + Math.cos(this.facing) * (PLAYER_RADIUS + BARREL_LENGTH / 2);
const by = this.y + Math.sin(this.facing) * (PLAYER_RADIUS + BARREL_LENGTH / 2);
this.barrel.setPosition(bx, by);
this.barrel.setRotation(this.facing);
}
_handleFire(delta) {
this._fireCooldown -= delta;
const ptr = this.scene.input.activePointer;
if (ptr.isDown && this._fireCooldown <= 0) {
this._fireCooldown = this._baseFireInterval / this.stats.fireRate;
this._spawnBullet();
}
}
_spawnBullet() {
const bx = this.x + Math.cos(this.facing) * (PLAYER_RADIUS + BARREL_LENGTH);
const by = this.y + Math.sin(this.facing) * (PLAYER_RADIUS + BARREL_LENGTH);
const bullet = this.scene.add.circle(bx, by, BULLET_SIZE, 0xffff00);
this.scene.physics.add.existing(bullet);
bullet.body.setVelocity(
Math.cos(this.facing) * BULLET_SPEED,
Math.sin(this.facing) * BULLET_SPEED
);
bullet.damage = this.stats.damage;
this.bullets.add(bullet);
}
_updateBullets() {
const W = this.scene.scale.width;
const H = this.scene.scale.height;
this.bullets.getChildren().forEach(b => {
if (b.x < -20 || b.x > W + 20 || b.y < -20 || b.y > H + 20) {
b.destroy();
}
});
}
takeDamage(amount) {
if (this.invincible) return;
const reduced = amount * (1 - Math.min(this.stats.damageReduction, 0.9));
this.hp -= reduced;
if (this.hp <= 0) {
this.hp = 0;
this._loseLife();
}
}
_loseLife() {
this.lives--;
if (this.lives <= 0) {
this.scene.events.emit('game-over');
return;
}
this.hp = this.stats.maxHp;
this._startInvincibility();
}
_startInvincibility() {
this.invincible = true;
// Flash effect
this.scene.tweens.add({
targets: [this.body, this.barrel],
alpha: 0,
duration: 150,
yoyo: true,
repeat: 4,
onComplete: () => {
this.body.setAlpha(1);
this.barrel.setAlpha(1);
this.invincible = false;
}
});
}
respawn(x, y) {
this.body.setPosition(x, y);
this.barrel.setPosition(x, y);
this.hp = this.stats.maxHp;
this._startInvincibility();
}
destroy() {
this.body.destroy();
this.barrel.destroy();
this.bullets.clear(true, true);
}
}