Files
slop-game-opus5.5/app/static/js/entities/Player.js
T
2026-09-24 17:57:54 +02:00

181 lines
5.2 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.
/**
* 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);
}
}