147 lines
4.2 KiB
JavaScript
147 lines
4.2 KiB
JavaScript
/**
|
||
* 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);
|
||
}
|