slop game oder so

This commit is contained in:
2026-09-24 17:57:54 +02:00
commit 4c997eee21
166 changed files with 14976 additions and 0 deletions
+146
View File
@@ -0,0 +1,146 @@
/**
* 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);
}