Files
KateLegend2_proj/assets/scripts/logic/DropSystem.ts
T
2026-05-06 08:17:32 +08:00

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;
}
}