181 lines
5.2 KiB
JavaScript
181 lines
5.2 KiB
JavaScript
/**
|
||
* Präsident Maximilian Valor – die Spielfigur.
|
||
*
|
||
* Die Figur ist frei erfunden: Raumrüstung mit geschlossenem Helm (kein Gesicht,
|
||
* keine Frisur) und Umhang in den Farben der fiktiven Vereinigten Erde.
|
||
* @module entities/Player
|
||
*/
|
||
|
||
import { GAME, PLAYER, WEAPONS } from '../config.js';
|
||
import { fromAngle, scale } from '../logic/math.js';
|
||
import { isNearlyZero } from '../logic/steering.js';
|
||
import { WeaponState } from '../logic/weapon.js';
|
||
import { TEXTURE_KEYS } from '../systems/Placeholders.js';
|
||
|
||
export class Player extends Phaser.Physics.Arcade.Sprite {
|
||
/**
|
||
* @param {Phaser.Scene} scene
|
||
* @param {number} x
|
||
* @param {number} y
|
||
*/
|
||
constructor(scene, x, y) {
|
||
super(scene, x, y, TEXTURE_KEYS.player);
|
||
scene.add.existing(this);
|
||
scene.physics.add.existing(this);
|
||
this.setDepth(GAME.depth.player);
|
||
this.setCollideWorldBounds(true);
|
||
const r = PLAYER.radius;
|
||
this.body.setCircle(r, this.width / 2 - r, this.height / 2 - r);
|
||
|
||
this.maxHp = PLAYER.maxHp;
|
||
this.hp = PLAYER.maxHp;
|
||
this.alive = true;
|
||
this.weapons = PLAYER.weaponSlots.map((id) => new WeaponState(WEAPONS[id]));
|
||
this.weaponIndex = 0;
|
||
this.aimAngle = 0;
|
||
this.dodgeUntil = 0;
|
||
this.dodgeReadyAt = 0;
|
||
this.dodgeDirection = { x: 1, y: 0 };
|
||
this.hitInvulnerableUntil = 0;
|
||
}
|
||
|
||
/** @returns {WeaponState} */
|
||
get weapon() {
|
||
return this.weapons[this.weaponIndex];
|
||
}
|
||
|
||
/** @param {number} now @returns {boolean} */
|
||
isDodging(now) {
|
||
return now < this.dodgeUntil;
|
||
}
|
||
|
||
/**
|
||
* Während der Rolle und kurz nach einem Treffer nimmt der Präsident keinen Schaden.
|
||
* @param {number} now
|
||
* @returns {boolean}
|
||
*/
|
||
isInvulnerable(now) {
|
||
return !this.alive || this.isDodging(now) || now < this.hitInvulnerableUntil;
|
||
}
|
||
|
||
/**
|
||
* @param {number} now
|
||
* @returns {number} Bereitschaft der Rolle, 0..1 (1 = bereit).
|
||
*/
|
||
dodgeReadiness(now) {
|
||
if (now >= this.dodgeReadyAt) return 1;
|
||
return 1 - (this.dodgeReadyAt - now) / PLAYER.dodge.cooldownMs;
|
||
}
|
||
|
||
/**
|
||
* Startet eine Ausweichrolle in Bewegungsrichtung (ohne Bewegung: in Blickrichtung).
|
||
* @param {number} now
|
||
* @param {{x: number, y: number}} moveDirection Normalisiert.
|
||
* @returns {boolean} true, wenn die Rolle ausgelöst wurde.
|
||
*/
|
||
tryDodge(now, moveDirection) {
|
||
if (!this.alive || now < this.dodgeReadyAt) return false;
|
||
this.dodgeDirection = isNearlyZero(moveDirection) ? fromAngle(this.aimAngle) : moveDirection;
|
||
this.dodgeUntil = now + PLAYER.dodge.durationMs;
|
||
this.dodgeReadyAt = now + PLAYER.dodge.cooldownMs;
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* Bewegung für diesen Frame.
|
||
* @param {number} now
|
||
* @param {{x: number, y: number}} moveDirection Normalisiert.
|
||
* @param {boolean} blocked true, solange ein Orbital-Befehl eingegeben wird.
|
||
*/
|
||
move(now, moveDirection, blocked) {
|
||
if (!this.alive) {
|
||
this.setVelocity(0, 0);
|
||
return;
|
||
}
|
||
if (this.isDodging(now)) {
|
||
const v = scale(this.dodgeDirection, PLAYER.dodge.speed);
|
||
this.setVelocity(v.x, v.y);
|
||
this.setAlpha(0.55);
|
||
return;
|
||
}
|
||
this.setAlpha(1);
|
||
const v = blocked ? { x: 0, y: 0 } : scale(moveDirection, PLAYER.speed);
|
||
this.setVelocity(v.x, v.y);
|
||
}
|
||
|
||
/**
|
||
* Richtet die Figur auf einen Punkt in Weltkoordinaten aus.
|
||
* @param {number} x
|
||
* @param {number} y
|
||
*/
|
||
aimAt(x, y) {
|
||
this.aimAngle = Math.atan2(y - this.y, x - this.x);
|
||
this.setRotation(this.aimAngle);
|
||
}
|
||
|
||
/** @returns {{x: number, y: number}} Position der Waffenmündung. */
|
||
muzzlePosition() {
|
||
const reach = PLAYER.radius + 8;
|
||
return {
|
||
x: this.x + Math.cos(this.aimAngle) * reach,
|
||
y: this.y + Math.sin(this.aimAngle) * reach,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* @param {number} index
|
||
* @returns {boolean} true, wenn gewechselt wurde.
|
||
*/
|
||
switchWeapon(index) {
|
||
if (index === this.weaponIndex || !this.weapons[index]) return false;
|
||
// Nachladen bricht beim Wechsel ab – sonst könnte man durch Wechseln „heimlich“ laden.
|
||
this.weapon.cancelReload();
|
||
this.weaponIndex = index;
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* @param {1|-1} step
|
||
* @returns {boolean}
|
||
*/
|
||
cycleWeapon(step) {
|
||
const count = this.weapons.length;
|
||
return this.switchWeapon((this.weaponIndex + step + count) % count);
|
||
}
|
||
|
||
/**
|
||
* Fügt Schaden zu, sofern der Präsident nicht gerade unverwundbar ist.
|
||
* @param {number} amount
|
||
* @param {number} now
|
||
* @returns {number} Tatsächlich erlittener Schaden.
|
||
*/
|
||
takeDamage(amount, now) {
|
||
if (this.isInvulnerable(now) || amount <= 0) return 0;
|
||
const dealt = Math.min(this.hp, amount);
|
||
this.hp -= dealt;
|
||
this.hitInvulnerableUntil = now + PLAYER.hitInvulnerabilityMs;
|
||
if (this.hp <= 0) {
|
||
this.alive = false;
|
||
this.setVelocity(0, 0);
|
||
}
|
||
return dealt;
|
||
}
|
||
|
||
/** @param {number} amount @returns {number} Tatsächlich geheilte Punkte. */
|
||
heal(amount) {
|
||
const before = this.hp;
|
||
this.hp = Math.min(this.maxHp, this.hp + amount);
|
||
return this.hp - before;
|
||
}
|
||
|
||
/** Füllt die Reserve aller Waffen auf (Nachschubkapsel). */
|
||
refillAmmo() {
|
||
for (const weapon of this.weapons) weapon.refill();
|
||
}
|
||
|
||
/** @param {number} now @returns {boolean} true, wenn die aktive Waffe fertig nachgeladen hat. */
|
||
updateWeapons(now) {
|
||
return this.weapon.update(now);
|
||
}
|
||
}
|