128 lines
3.5 KiB
JavaScript
128 lines
3.5 KiB
JavaScript
/**
|
||
* 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,
|
||
};
|
||
}
|
||
}
|