/** * 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); } }