Files
slop-game-opus5.5/app/static/js/logic/weapon.js
T
2026-09-24 17:57:54 +02:00

147 lines
4.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);
}