90 lines
3.2 KiB
TypeScript
90 lines
3.2 KiB
TypeScript
import { EnemyType, ItemType } from '../data/Interfaces';
|
|
|
|
/**
|
|
* Drop system (req 7.1-7.6).
|
|
*
|
|
* Rules encoded here:
|
|
*
|
|
* 1. **水晶玉 (crystal jade)** — deterministic: every 12th kill on forest
|
|
* stages spawns one above the player (req 7.1). Lifetime 13-20s (req 7.2,
|
|
* handled by caller).
|
|
* 2. **点丸 / 术丸** — after 3 consecutive 赤忍 kills, 50% chance of one or
|
|
* the other (req 7.3).
|
|
* 3. **魔笛** — dropped by 黑忍 on death (implemented in `HeiRenAI`). Picking
|
|
* it up triggers a screen-wipe (req 7.4), applied by the level manager.
|
|
* 4. **增丸** — fixed spawn point per level config, no probability (req 7.5).
|
|
*
|
|
* All probabilistic decisions funnel through an injectable `random()` so
|
|
* deterministic tests stay stable.
|
|
*/
|
|
|
|
export interface IDropSystemCfg {
|
|
/** Kills required before a crystal jade is guaranteed. Default 12 (req 7.1). */
|
|
crystalJadeEveryN?: number;
|
|
/** Kills of Chi Ren required before point/spell ball eligible. Default 3. */
|
|
dianShuWanThresholdKills?: number;
|
|
/** Probability for dian_wan or shu_wan drop once threshold reached. Default 0.5. */
|
|
dianShuWanProbability?: number;
|
|
/** Injectable RNG. Default `Math.random`. */
|
|
random?: () => number;
|
|
}
|
|
|
|
export interface IDropEvent {
|
|
item: ItemType;
|
|
x: number;
|
|
y: number;
|
|
}
|
|
|
|
export class DropSystem {
|
|
private globalKills = 0;
|
|
private chiRenConsecutiveKills = 0;
|
|
private readonly crystalJadeEveryN: number;
|
|
private readonly dianShuWanThresholdKills: number;
|
|
private readonly dianShuWanProbability: number;
|
|
private readonly random: () => number;
|
|
|
|
constructor(cfg: IDropSystemCfg = {}) {
|
|
this.crystalJadeEveryN = cfg.crystalJadeEveryN ?? 12;
|
|
this.dianShuWanThresholdKills = cfg.dianShuWanThresholdKills ?? 3;
|
|
this.dianShuWanProbability = cfg.dianShuWanProbability ?? 0.5;
|
|
this.random = cfg.random ?? Math.random;
|
|
}
|
|
|
|
/** Register an enemy kill and return any drops produced. */
|
|
public onEnemyKilled(enemy: EnemyType, at: { x: number; y: number }): IDropEvent[] {
|
|
this.globalKills++;
|
|
const drops: IDropEvent[] = [];
|
|
// Crystal-jade rule (deterministic, req 7.1).
|
|
if (this.globalKills % this.crystalJadeEveryN === 0) {
|
|
drops.push({ item: ItemType.CrystalJade, x: at.x, y: at.y + 180 });
|
|
}
|
|
// Chi Ren consecutive rule (req 7.3).
|
|
if (enemy === EnemyType.ChiRen) {
|
|
this.chiRenConsecutiveKills++;
|
|
if (this.chiRenConsecutiveKills >= this.dianShuWanThresholdKills) {
|
|
this.chiRenConsecutiveKills = 0;
|
|
if (this.random() < this.dianShuWanProbability) {
|
|
const pickDian = this.random() < 0.5;
|
|
drops.push({
|
|
item: pickDian ? ItemType.DianWan : ItemType.ShuWan,
|
|
x: at.x,
|
|
y: at.y,
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
this.chiRenConsecutiveKills = 0;
|
|
}
|
|
return drops;
|
|
}
|
|
|
|
public reset(): void {
|
|
this.globalKills = 0;
|
|
this.chiRenConsecutiveKills = 0;
|
|
}
|
|
|
|
public get kills(): number {
|
|
return this.globalKills;
|
|
}
|
|
}
|