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