247 lines
7.1 KiB
TypeScript
247 lines
7.1 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest'
|
|
import { getEmbeddedDropTables } from '../src/game/embedded-drop-tables.ts'
|
|
import * as dropPipeline from '../src/game/drop-pipeline.ts'
|
|
import { GameEngine } from '../src/game/engine.ts'
|
|
import type { MonsterKind } from '../src/game/monsters.ts'
|
|
|
|
describe('Fail-Fast Drop Resolution & Elimination of Permissive Fallbacks (Issue #413)', () => {
|
|
const dropTables = getEmbeddedDropTables()
|
|
|
|
const dummyTerrain = {
|
|
widthPx: 1000,
|
|
heightPx: 1000,
|
|
overlap: () => 0,
|
|
}
|
|
|
|
function createTestEngine(
|
|
difficulty: 'normal' | 'nightmare' | 'hell' = 'normal',
|
|
customMonsterKinds?: Map<string, MonsterKind>,
|
|
) {
|
|
return new GameEngine(dummyTerrain, {
|
|
spawn: { x: 0, y: 0 },
|
|
stats: [],
|
|
xpTable: [0, 100, 200],
|
|
dropTables,
|
|
difficulty,
|
|
monsterKinds: customMonsterKinds,
|
|
skills: [],
|
|
npcDefs: [],
|
|
questDefs: [],
|
|
combatOptions: {} as any,
|
|
talkRadius: 50,
|
|
pickupRadius: 50,
|
|
inventoryCols: 10,
|
|
inventoryRows: 4,
|
|
})
|
|
}
|
|
|
|
const idleInput = {
|
|
movement: { x: 0, y: 0 },
|
|
attacking: false,
|
|
pickingUp: false,
|
|
talking: false,
|
|
saving: false,
|
|
loading: false,
|
|
digits: [],
|
|
}
|
|
|
|
it('1. throws descriptive error when death event has missing monsterLevel', () => {
|
|
const engine = createTestEngine('normal')
|
|
engine.world.pendingKills = [
|
|
{
|
|
kind: 'kill',
|
|
x: 10,
|
|
y: 10,
|
|
subjectId: 'fallen1',
|
|
monsterRank: 'normal',
|
|
} as any,
|
|
]
|
|
expect(() => engine.tick(idleInput)).toThrow(
|
|
/GameEngine drop failure: missing or invalid monsterLevel for death event \(subjectId: "fallen1"/,
|
|
)
|
|
})
|
|
|
|
it('2. throws descriptive error when death event has non-positive or NaN monsterLevel', () => {
|
|
const engine = createTestEngine('normal')
|
|
engine.world.pendingKills = [
|
|
{
|
|
kind: 'kill',
|
|
x: 10,
|
|
y: 10,
|
|
subjectId: 'fallen1',
|
|
monsterRank: 'normal',
|
|
monsterLevel: 0,
|
|
} as any,
|
|
]
|
|
expect(() => engine.tick(idleInput)).toThrow(
|
|
/GameEngine drop failure: missing or invalid monsterLevel/,
|
|
)
|
|
|
|
engine.world.pendingKills = [
|
|
{
|
|
kind: 'kill',
|
|
x: 10,
|
|
y: 10,
|
|
subjectId: 'fallen1',
|
|
monsterRank: 'normal',
|
|
monsterLevel: -5,
|
|
} as any,
|
|
]
|
|
expect(() => engine.tick(idleInput)).toThrow(
|
|
/GameEngine drop failure: missing or invalid monsterLevel/,
|
|
)
|
|
|
|
engine.world.pendingKills = [
|
|
{
|
|
kind: 'kill',
|
|
x: 10,
|
|
y: 10,
|
|
subjectId: 'fallen1',
|
|
monsterRank: 'normal',
|
|
monsterLevel: Number.NaN,
|
|
} as any,
|
|
]
|
|
expect(() => engine.tick(idleInput)).toThrow(
|
|
/GameEngine drop failure: missing or invalid monsterLevel/,
|
|
)
|
|
})
|
|
|
|
it('3. throws descriptive error on unknown monster with no valid subjectId or superUniqueId', () => {
|
|
const engine = createTestEngine('normal')
|
|
engine.world.pendingKills = [
|
|
{
|
|
kind: 'kill',
|
|
x: 10,
|
|
y: 10,
|
|
subjectId: 'non_existent_monster_kind_999',
|
|
monsterRank: 'normal',
|
|
monsterLevel: 10,
|
|
} as any,
|
|
]
|
|
expect(() => engine.tick(idleInput)).toThrow(
|
|
/GameEngine drop failure: unknown monster \(subjectId: "non_existent_monster_kind_999", superUniqueId: "undefined"\) not found in canonical drop tables/,
|
|
)
|
|
})
|
|
|
|
it('4. throws descriptive error when monster TC is corrupt or unresolvable', () => {
|
|
const customKinds = new Map<string, MonsterKind>()
|
|
customKinds.set('corrupt_boss', {
|
|
id: 'corrupt_boss',
|
|
baseId: 'corrupt_boss',
|
|
name: 'Corrupt Boss',
|
|
boss: true,
|
|
levels: { normal: 10, nightmare: 40, hell: 70 },
|
|
treasureClasses: ['Act 1 H2H A'],
|
|
getTreasureClass: () => '',
|
|
} as any)
|
|
|
|
const engine = createTestEngine('normal', customKinds)
|
|
engine.world.pendingKills = [
|
|
{
|
|
kind: 'kill',
|
|
x: 10,
|
|
y: 10,
|
|
subjectId: 'corrupt_boss',
|
|
monsterRank: 'unique',
|
|
monsterLevel: 10,
|
|
} as any,
|
|
]
|
|
|
|
expect(() => engine.tick(idleInput)).toThrow(
|
|
/GameEngine drop failure: corrupt or unresolvable treasure class for monster "corrupt_boss" on difficulty "normal"/,
|
|
)
|
|
})
|
|
|
|
it('5. throws descriptive error when resolved TC is unregistered in drop tables', () => {
|
|
const customKinds = new Map<string, MonsterKind>()
|
|
customKinds.set('unregistered_tc_mob', {
|
|
id: 'unregistered_tc_mob',
|
|
baseId: 'unregistered_tc_mob',
|
|
name: 'Unregistered TC Mob',
|
|
levels: { normal: 10, nightmare: 40, hell: 70 },
|
|
treasureClasses: ['Bogus NonExistent Treasure Class XYZ'],
|
|
} as any)
|
|
|
|
const engine = createTestEngine('normal', customKinds)
|
|
engine.world.pendingKills = [
|
|
{
|
|
kind: 'kill',
|
|
x: 10,
|
|
y: 10,
|
|
subjectId: 'unregistered_tc_mob',
|
|
monsterRank: 'normal',
|
|
monsterLevel: 10,
|
|
} as any,
|
|
]
|
|
|
|
expect(() => engine.tick(idleInput)).toThrow(
|
|
/GameEngine drop failure: treasure class "Bogus NonExistent Treasure Class XYZ" resolved for monster "unregistered_tc_mob" not found in drop tables/,
|
|
)
|
|
})
|
|
|
|
it('6. authentic non-dropping ambient critters (chicken, rat, bird) succeed with 0 drops without throwing', () => {
|
|
const critters = ['chicken', 'rat', 'bird1', 'bird2']
|
|
|
|
for (const critterId of critters) {
|
|
const engine = createTestEngine('normal')
|
|
engine.world.pendingKills = [
|
|
{
|
|
kind: 'kill',
|
|
x: 10,
|
|
y: 10,
|
|
subjectId: critterId,
|
|
monsterRank: 'normal',
|
|
monsterLevel: 1,
|
|
} as any,
|
|
]
|
|
|
|
expect(() => engine.tick(idleInput)).not.toThrow()
|
|
expect(engine.metrics.dropsRolled).toBe(0)
|
|
expect(engine.ground.length).toBe(0)
|
|
}
|
|
})
|
|
|
|
it('7. authentic non-dropping SuperUniques (Ancient Barbarians) succeed with 0 drops without throwing', () => {
|
|
const ancients = ['Ancient Barbarian 1', 'Ancient Barbarian 2', 'Ancient Barbarian 3']
|
|
|
|
for (const ancientId of ancients) {
|
|
const engine = createTestEngine('normal')
|
|
engine.world.pendingKills = [
|
|
{
|
|
kind: 'kill',
|
|
x: 10,
|
|
y: 10,
|
|
subjectId: ancientId.toLowerCase().replace(/\s+/g, ''),
|
|
superUniqueId: ancientId,
|
|
monsterRank: 'unique',
|
|
monsterLevel: 68,
|
|
} as any,
|
|
]
|
|
|
|
expect(() => engine.tick(idleInput)).not.toThrow()
|
|
expect(engine.metrics.dropsRolled).toBe(0)
|
|
expect(engine.ground.length).toBe(0)
|
|
}
|
|
})
|
|
|
|
it('8. verifies no silent fallback to Act 1 H2H A occurs when TC resolution fails', () => {
|
|
const dropSpy = vi.spyOn(dropPipeline, 'executeDropPipeline')
|
|
const engine = createTestEngine('normal')
|
|
|
|
engine.world.pendingKills = [
|
|
{
|
|
kind: 'kill',
|
|
x: 10,
|
|
y: 10,
|
|
subjectId: 'invalid_entity_id',
|
|
monsterRank: 'normal',
|
|
monsterLevel: 10,
|
|
} as any,
|
|
]
|
|
|
|
expect(() => engine.tick(idleInput)).toThrow(/GameEngine drop failure: unknown monster/)
|
|
expect(dropSpy).not.toHaveBeenCalled()
|
|
dropSpy.mockRestore()
|
|
})
|
|
})
|