first commmit
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user