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

128 lines
3.5 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.
/**
* Punkte, Serien und Multiplikator (ohne Phaser).
*
* Regeln: Jeder Abschuss innerhalb von `streakWindowMs` nach dem letzten setzt die
* Serie fort. Alle `stepKills` Abschüsse steigt der Multiplikator um `stepValue`
* bis `maxMultiplier`. Ein Treffer am Präsidenten beendet die Serie – das belohnt
* sauberes Spielen statt blindem Draufhalten.
* @module logic/scoring
*/
import { SCORING } from '../config.js';
/**
* Multiplikator für eine Serienlänge.
* @param {number} streak Anzahl Abschüsse in Folge (inkl. des aktuellen).
* @param {typeof SCORING} [cfg]
* @returns {number}
*/
export function multiplierForStreak(streak, cfg = SCORING) {
if (streak <= 0) return 1;
return Math.min(cfg.maxMultiplier, 1 + Math.floor(streak / cfg.stepKills) * cfg.stepValue);
}
/**
* Bonus für eine überstandene Welle (ohne Multiplikator).
* @param {number} waveNumber
* @param {typeof SCORING} [cfg]
* @returns {number}
*/
export function waveBonus(waveNumber, cfg = SCORING) {
return waveNumber * cfg.waveBonusPerWave;
}
/**
* Führt Punktestand, Serie und Statistik einer Spielrunde.
*/
export class ScoreKeeper {
/** @param {typeof SCORING} [cfg] */
constructor(cfg = SCORING) {
this.cfg = cfg;
this.score = 0;
this.kills = 0;
this.streak = 0;
this.bestStreak = 0;
this.lastKillAt = -Infinity;
}
/**
* Läuft die Serie zum Zeitpunkt `now` noch?
* @param {number} now Spielzeit in ms.
* @returns {boolean}
*/
isStreakActive(now) {
return this.streak > 0 && now - this.lastKillAt <= this.cfg.streakWindowMs;
}
/**
* Aktueller Multiplikator (1, wenn die Serie abgelaufen ist).
* @param {number} now
* @returns {number}
*/
multiplierAt(now) {
return this.isStreakActive(now) ? multiplierForStreak(this.streak, this.cfg) : 1;
}
/**
* Verbleibender Anteil des Serienfensters (1 = gerade getroffen, 0 = abgelaufen).
* @param {number} now
* @returns {number}
*/
streakTimeLeft(now) {
if (!this.isStreakActive(now)) return 0;
return 1 - (now - this.lastKillAt) / this.cfg.streakWindowMs;
}
/**
* Verbucht einen Abschuss.
* @param {number} basePoints Punkte des Gegnertyps.
* @param {number} now Spielzeit in ms.
* @returns {{awarded: number, multiplier: number, streak: number}}
*/
registerKill(basePoints, now) {
if (!this.isStreakActive(now)) {
this.streak = 0;
}
this.streak += 1;
this.kills += 1;
this.bestStreak = Math.max(this.bestStreak, this.streak);
this.lastKillAt = now;
const multiplier = multiplierForStreak(this.streak, this.cfg);
const awarded = Math.round(basePoints * multiplier);
this.score += awarded;
return { awarded, multiplier, streak: this.streak };
}
/** Beendet die laufende Serie (z. B. wenn der Präsident getroffen wird). */
breakStreak() {
this.streak = 0;
}
/**
* Fügt Bonuspunkte ohne Multiplikator hinzu.
* @param {number} points
* @returns {number} Tatsächlich gutgeschriebene Punkte.
*/
addBonus(points) {
const awarded = Math.max(0, Math.round(points));
this.score += awarded;
return awarded;
}
/**
* Momentaufnahme für HUD und Game-Over-Bildschirm.
* @param {number} now
*/
snapshot(now) {
const active = this.isStreakActive(now);
return {
score: this.score,
kills: this.kills,
streak: active ? this.streak : 0,
multiplier: this.multiplierAt(now),
streakTimeLeft: this.streakTimeLeft(now),
bestStreak: this.bestStreak,
};
}
}