Files
2026-05-06 08:17:32 +08:00

106 lines
3.2 KiB
TypeScript
Raw Permalink 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.
import { WeaponType } from '../data/Interfaces';
/**
* Score system (task 9.3, req 12.1-12.8).
*
* Scoring table:
* - Ninja sword kill ×2.0 base
* - Shuriken kill ×1.0 base
* - Perfect parry counterkill ×3.0 base
* - 5-combo "刃接触" bonus +1500
* - Flawless level (no damage) ×3.0 total
* - Remaining time bonus +10 pts / remaining sec
*
* Everything is deterministic and `Math.random`-free so QA can reproduce
* every score calculation in unit tests.
*/
export const BASE_ENEMY_SCORE = 100;
export const COMBO_BONUS = 1500;
export const COMBO_THRESHOLD = 5;
export const TIME_BONUS_PER_SEC = 10;
export interface IScoreSnapshot {
baseScore: number;
comboBonus: number;
timeBonus: number;
flawlessMultiplier: number;
finalScore: number;
killCount: number;
comboCount: number;
consecutiveBladeHits: number;
}
export class ScoreSystem {
private baseScore = 0;
private comboBonus = 0;
private killCount = 0;
private consecutiveBladeHits = 0;
private comboCount = 0;
private flawless = true;
private timeBonus = 0;
public reset(): void {
this.baseScore = 0;
this.comboBonus = 0;
this.killCount = 0;
this.consecutiveBladeHits = 0;
this.comboCount = 0;
this.flawless = true;
this.timeBonus = 0;
}
/** Record a kill weighted by weapon type (req 12.1-12.2). */
public recordEnemyKill(weapon: WeaponType): void {
this.killCount++;
const multiplier = weapon === WeaponType.NinjaSword ? 2 : 1;
this.baseScore += BASE_ENEMY_SCORE * multiplier;
}
/** Perfect parry followed by a counter-kill (req 12.3). */
public recordParryKill(): void {
this.killCount++;
this.baseScore += BASE_ENEMY_SCORE * 3;
}
/** Record a single blade contact; every 5 contacts award a combo bonus (req 12.4). */
public recordBladeContact(): void {
this.consecutiveBladeHits++;
if (this.consecutiveBladeHits >= COMBO_THRESHOLD) {
this.comboCount++;
this.comboBonus += COMBO_BONUS;
this.consecutiveBladeHits = 0;
}
}
/** Resets the consecutive blade counter if the combo is broken. */
public breakBladeChain(): void {
this.consecutiveBladeHits = 0;
}
/** Player took damage → flawless multiplier lost (req 12.5). */
public markTaken(): void {
this.flawless = false;
}
/** Stage-end timing bonus (req 12.6). */
public setRemainingTimeBonus(remainingSec: number): void {
this.timeBonus = Math.max(0, Math.floor(remainingSec)) * TIME_BONUS_PER_SEC;
}
public snapshot(): IScoreSnapshot {
const flawlessMultiplier = this.flawless ? 3 : 1;
const finalScore = (this.baseScore + this.comboBonus + this.timeBonus) * flawlessMultiplier;
return {
baseScore: this.baseScore,
comboBonus: this.comboBonus,
timeBonus: this.timeBonus,
flawlessMultiplier,
finalScore,
killCount: this.killCount,
comboCount: this.comboCount,
consecutiveBladeHits: this.consecutiveBladeHits,
};
}
}