/** * Waffenzustand: Magazin, Reserve, Feuerrate, Nachladen (ohne Phaser). * @module logic/weapon */ import { degToRad } from './math.js'; /** * @typedef {Object} WeaponDef * @property {string} id * @property {string} name * @property {number} magazineSize * @property {number|null} reserveStart null = unbegrenzt * @property {number|null} reserveMax null = unbegrenzt * @property {number} fireIntervalMs * @property {number} reloadMs * @property {number} damage * @property {number} pellets * @property {number} spreadDeg * @property {number} bulletSpeed * @property {number} rangePx * @property {boolean} automatic */ export class WeaponState { /** @param {WeaponDef} def */ constructor(def) { this.def = def; this.magazine = def.magazineSize; /** @type {number} Bei unbegrenzter Reserve Infinity. */ this.reserve = def.reserveStart === null ? Infinity : def.reserveStart; /** Spielzeit, zu der das Nachladen fertig ist; 0 = lädt nicht nach. */ this.reloadingUntil = 0; this.reloadStartedAt = 0; this.lastShotAt = -Infinity; } /** @returns {boolean} */ get hasInfiniteReserve() { return this.def.reserveMax === null; } /** @returns {boolean} Weder Magazin noch Reserve – Waffe ist leer. */ get isEmpty() { return this.magazine === 0 && this.reserve === 0; } /** @returns {boolean} */ get isReloading() { return this.reloadingUntil > 0; } /** * Schließt ein fälliges Nachladen ab. Muss jeden Frame aufgerufen werden. * @param {number} now * @returns {boolean} true, wenn in diesem Aufruf fertig nachgeladen wurde. */ update(now) { if (this.isReloading && now >= this.reloadingUntil) { const needed = this.def.magazineSize - this.magazine; const taken = Math.min(needed, this.reserve); this.magazine += taken; this.reserve -= taken; this.reloadingUntil = 0; return true; } return false; } /** * @param {number} now * @returns {boolean} */ canFire(now) { return !this.isReloading && this.magazine > 0 && now - this.lastShotAt >= this.def.fireIntervalMs; } /** * Feuert einen Schuss, falls möglich. * @param {number} now * @param {() => number} rng Zufall für die Streuung. * @returns {number[]|null} Winkelabweichungen der Projektile (Bogenmaß) oder null. */ fire(now, rng) { if (!this.canFire(now)) return null; this.magazine -= 1; this.lastShotAt = now; const offsets = pelletOffsets(this.def.pellets, this.def.spreadDeg, rng); // Leeres Magazin lädt automatisch nach – niemand will im Gefecht R suchen. if (this.magazine === 0) this.startReload(now); return offsets; } /** * Beginnt das Nachladen. * @param {number} now * @returns {boolean} false, wenn es nichts nachzuladen gibt. */ startReload(now) { if (this.isReloading || this.magazine >= this.def.magazineSize || this.reserve <= 0) { return false; } this.reloadStartedAt = now; this.reloadingUntil = now + this.def.reloadMs; return true; } /** Bricht das Nachladen ab (z. B. beim Waffenwechsel); nichts wird verbraucht. */ cancelReload() { this.reloadingUntil = 0; } /** * @param {number} now * @returns {number} 0..1, 1 wenn nicht nachgeladen wird. */ reloadProgress(now) { if (!this.isReloading) return 1; return Math.min(1, (now - this.reloadStartedAt) / this.def.reloadMs); } /** Füllt die Reserve bis zum Maximum auf (Nachschubkapsel). */ refill() { if (!this.hasInfiniteReserve) { this.reserve = /** @type {number} */ (this.def.reserveMax); } } } /** * Streuung der Projektile eines Schusses. Mehrere Projektile (Schrot) werden * gleichmäßig über den Fächer verteilt und leicht verwackelt, damit sich das Muster * nicht wiederholt; ein Einzelprojektil streut zufällig innerhalb des Fächers. * @param {number} pellets * @param {number} spreadDeg * @param {() => number} rng * @returns {number[]} */ export function pelletOffsets(pellets, spreadDeg, rng) { const spread = degToRad(spreadDeg); if (pellets <= 1) { return [(rng() - 0.5) * spread]; } const step = spread / (pellets - 1); return Array.from({ length: pellets }, (_, i) => -spread / 2 + i * step + (rng() - 0.5) * step * 0.5); }