Files
2026-09-24 17:57:54 +02:00

107 lines
3.2 KiB
JavaScript

/**
* Abwurfobjekte der Orbital-Befehle: Verteidigungsgeschütz und Nachschubkapsel.
* @module entities/Deployables
*/
import { GAME } from '../config.js';
import { angleTo, distance } from '../logic/math.js';
import { TEXTURE_KEYS } from '../systems/Placeholders.js';
/** Letzte Sekunden, in denen ein auslaufendes Objekt blinkt. */
const EXPIRY_WARNING_MS = 3000;
/**
* Geschütz »Wachsamer Nachbar«: zielt automatisch auf den nächsten Gegner in Reichweite.
*/
export class Turret extends Phaser.GameObjects.Sprite {
/**
* @param {Phaser.Scene} scene
* @param {number} x
* @param {number} y
* @param {{durationMs: number, fireIntervalMs: number, range: number, bulletDamage: number, bulletSpeed: number}} cfg
* @param {number} now
*/
constructor(scene, x, y, cfg, now) {
super(scene, x, y, TEXTURE_KEYS.turret);
scene.add.existing(this);
this.setDepth(GAME.depth.pickups);
this.cfg = cfg;
this.expiresAt = now + cfg.durationMs;
this.nextShotAt = now;
}
/**
* @param {number} now
* @param {Iterable<import('./Enemy.js').Enemy>} enemies
* @param {(x: number, y: number, angle: number) => void} fire
* @returns {boolean} false, wenn die Einsatzzeit abgelaufen ist.
*/
step(now, enemies, fire) {
if (now >= this.expiresAt) return false;
let target = null;
let best = this.cfg.range;
for (const enemy of enemies) {
if (!enemy.alive) continue;
const d = distance(this, enemy);
if (d < best) {
best = d;
target = enemy;
}
}
if (target) {
const angle = angleTo(this, target);
this.setRotation(angle);
if (now >= this.nextShotAt) {
this.nextShotAt = now + this.cfg.fireIntervalMs;
fire(this.x + Math.cos(angle) * 24, this.y + Math.sin(angle) * 24, angle);
}
}
// Kurz vor Ablauf blinken, damit der Spieler nicht überrascht wird.
const left = this.expiresAt - now;
this.setAlpha(left < EXPIRY_WARNING_MS && Math.floor(now / 150) % 2 === 0 ? 0.4 : 1);
return true;
}
}
/**
* Nachschubkapsel »Fürsorgepaket«: Beim Berühren gibt es Leben und Munition.
*/
export class SupplyPod extends Phaser.Physics.Arcade.Sprite {
/**
* @param {Phaser.Scene} scene
* @param {number} x
* @param {number} y
* @param {{lifetimeMs: number, pickupRadius: number, heal: number}} cfg
* @param {number} now
*/
constructor(scene, x, y, cfg, now) {
super(scene, x, y, TEXTURE_KEYS.supplyPod);
scene.add.existing(this);
scene.physics.add.existing(this);
this.setDepth(GAME.depth.pickups);
this.cfg = cfg;
this.expiresAt = now + cfg.lifetimeMs;
this.collected = false;
}
/** Nach dem Einfügen in die Gruppe: runden Aufnahmebereich setzen. */
initBody() {
const r = this.cfg.pickupRadius;
this.body.setCircle(r, this.width / 2 - r, this.height / 2 - r);
// Liegt fest am Boden: kein Wegschieben durch Physik.
this.body.setImmovable(true);
this.body.moves = false;
}
/**
* @param {number} now
* @returns {boolean} false, wenn die Kapsel abgelaufen ist.
*/
tick(now) {
const left = this.expiresAt - now;
if (left <= 0) return false;
this.setAlpha(left < EXPIRY_WARNING_MS && Math.floor(now / 150) % 2 === 0 ? 0.4 : 1);
return true;
}
}