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