106 lines
3.2 KiB
TypeScript
106 lines
3.2 KiB
TypeScript
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,
|
||
};
|
||
}
|
||
}
|