import { DropSystem } from '@logic/DropSystem'; import { EnemyType, ItemType } from '@data/Interfaces'; describe('DropSystem — crystal jade deterministic rule (req 7.1)', () => { it('spawns a crystal jade on exactly the 12th kill', () => { const ds = new DropSystem({ random: () => 1 }); let crystalEvents = 0; for (let i = 1; i <= 24; i++) { const drops = ds.onEnemyKilled(EnemyType.YaoFang, { x: 100, y: 0 }); if (drops.some((d) => d.item === ItemType.CrystalJade)) crystalEvents++; } expect(crystalEvents).toBe(2); }); it('spawns the crystal above the kill point', () => { const ds = new DropSystem({ random: () => 1 }); for (let i = 0; i < 11; i++) ds.onEnemyKilled(EnemyType.QingRen, { x: 0, y: 0 }); const drops = ds.onEnemyKilled(EnemyType.QingRen, { x: 300, y: 20 }); const crystal = drops.find((d) => d.item === ItemType.CrystalJade); expect(crystal!.y).toBeGreaterThan(20); }); }); describe('DropSystem — Chi Ren consecutive rule (req 7.3)', () => { it('drops dian_wan or shu_wan on the 3rd consecutive Chi Ren kill if RNG <0.5', () => { const ds = new DropSystem({ dianShuWanProbability: 0.5, random: (() => { // probability check then which-item check both pass const vals = [0.1, 0.2]; let i = 0; return () => vals[i++ % vals.length]; })(), }); ds.onEnemyKilled(EnemyType.ChiRen, { x: 0, y: 0 }); ds.onEnemyKilled(EnemyType.ChiRen, { x: 0, y: 0 }); const drops = ds.onEnemyKilled(EnemyType.ChiRen, { x: 50, y: 10 }); expect(drops.find((d) => d.item === ItemType.DianWan)).toBeDefined(); }); it('does not drop when RNG fails probability', () => { const ds = new DropSystem({ dianShuWanProbability: 0.5, random: () => 0.95 }); ds.onEnemyKilled(EnemyType.ChiRen, { x: 0, y: 0 }); ds.onEnemyKilled(EnemyType.ChiRen, { x: 0, y: 0 }); const drops = ds.onEnemyKilled(EnemyType.ChiRen, { x: 0, y: 0 }); expect(drops.filter((d) => d.item !== ItemType.CrystalJade).length).toBe(0); }); it('non-Chi-Ren kill resets the consecutive counter', () => { const ds = new DropSystem({ dianShuWanProbability: 1.0, random: () => 0 }); ds.onEnemyKilled(EnemyType.ChiRen, { x: 0, y: 0 }); ds.onEnemyKilled(EnemyType.ChiRen, { x: 0, y: 0 }); ds.onEnemyKilled(EnemyType.QingRen, { x: 0, y: 0 }); const drops = ds.onEnemyKilled(EnemyType.ChiRen, { x: 0, y: 0 }); // Only 1 Chi Ren kill since reset — below threshold. expect(drops.filter((d) => d.item === ItemType.DianWan || d.item === ItemType.ShuWan).length).toBe(0); }); }); describe('DropSystem — reset()', () => { it('zeroes kill counters', () => { const ds = new DropSystem(); ds.onEnemyKilled(EnemyType.QingRen, { x: 0, y: 0 }); ds.reset(); expect(ds.kills).toBe(0); }); });