import { describe, expect, it } from "vitest"; import { activeDungeonCompletedObjectiveIds, advanceDungeonEncounters, createDungeonEncounterSnapshot, reduceDungeonEncounter, resetDungeonEncounters, validateDungeonEncounterDefinition, } from "./dungeonEncounterRuntime"; import { BITE_SIZED_STOCKADES_DUNGEON_ID, STORMWIND_STOCKADE_DUNGEON_ID } from "./dungeonContentSource"; import { DUNGEON_REGISTRY, dungeonDefinitionById } from "./dungeonRegistry"; import { activateDungeonSession, restartActiveDungeonSession } from "./dungeonSession"; import { completedDungeonRun, useDungeonRunStore } from "./dungeonRunStore"; import { BITE_SIZED_STOCKADES_BOSS_CANDIDATES, createBiteSizedStockadesDefinition, } from "./dungeons/biteSizedStockades"; import { validateDungeonWorldObject } from "./dungeonWorldObjects"; import { dungeonEnemyScaling } from "./dungeonBossScaling"; const source = DUNGEON_REGISTRY[STORMWIND_STOCKADE_DUNGEON_ID]; describe("bite-sized Stormwind Stockades", () => { it("creates a separate central-corridor dungeon without changing Stockades", () => { const originalBossIds = source.bosses.map((boss) => boss.id); const originalSpawnCount = source.staticSpawns.length; const biteSized = createBiteSizedStockadesDefinition(source, "run-a"); expect(biteSized.id).toBe(BITE_SIZED_STOCKADES_DUNGEON_ID); expect(biteSized).not.toBe(source); expect(source.bosses.map((boss) => boss.id)).toEqual(originalBossIds); expect(source.staticSpawns).toHaveLength(originalSpawnCount); expect(source.bosses).toHaveLength(5); expect(source.serverGameObjects.some((object) => object.id.startsWith("bite-sized-"))).toBe(false); const bossId = biteSized.bosses[0].id; expect(BITE_SIZED_STOCKADES_BOSS_CANDIDATES.map((boss) => boss.sourceSpawnId)).toContain(bossId); expect(biteSized.objectiveOrder).toEqual(biteSized.bosses); expect(biteSized.finalBossDurability).toBe("standard"); expect(biteSized.staticSpawns.filter((spawn) => spawn.id === bossId)).toHaveLength(1); expect(biteSized.staticSpawns.every((spawn) => Math.abs(spawn.position[2]) <= 15)).toBe(true); expect(biteSized.navigationLinks).toEqual(source.navigationLinks); expect(biteSized.traversalRamps).toEqual(source.traversalRamps); expect(biteSized.roamingPacks).toEqual([]); }); it("seals both side wings with visible movement and sight blockers", () => { const biteSized = createBiteSizedStockadesDefinition(source, "run-b"); const walls = biteSized.serverGameObjects.filter((object) => object.id.startsWith("bite-sized-stockades-")); expect(walls.map((wall) => wall.id).sort()).toEqual([ "bite-sized-stockades-north-wall", "bite-sized-stockades-south-wall", ]); for (const wall of walls) { expect(() => validateDungeonWorldObject(wall)).not.toThrow(); expect(wall.states).toEqual([expect.objectContaining({ id: "sealed", visible: true, blocksMovement: true, blocksSight: true, })]); } }); it("chooses all five source bosses deterministically across fresh run ids", () => { const selected = new Set(); for (let index = 0; index < 1_000; index += 1) { selected.add(createBiteSizedStockadesDefinition(source, `fresh-run-${index}`).bosses[0].name); } expect([...selected].sort()).toEqual(BITE_SIZED_STOCKADES_BOSS_CANDIDATES.map((boss) => boss.name).sort()); const first = createBiteSizedStockadesDefinition(source, "same-run"); const second = createBiteSizedStockadesDefinition(source, "same-run"); expect(second.bosses[0]).toEqual(first.bosses[0]); expect(second.staticSpawns.find((spawn) => spawn.id === second.bosses[0].id)?.entityId) .toBe(first.staticSpawns.find((spawn) => spawn.id === first.bosses[0].id)?.entityId); }); it("retains the shorter Bite-Sized durability curve for every level-17 roll", () => { const variants = new Map>(); for (let index = 0; index < 1_000 && variants.size < BITE_SIZED_STOCKADES_BOSS_CANDIDATES.length; index += 1) { const definition = createBiteSizedStockadesDefinition(source, `level-17-run-${index}`); variants.set(definition.bosses[0].id, definition); } expect(variants.size).toBe(BITE_SIZED_STOCKADES_BOSS_CANDIDATES.length); const ordinaryBoss = source.bosses[0]; const ordinarySpawn = source.staticSpawns.find((spawn) => spawn.id === ordinaryBoss.id)!; const ordinaryEntity = source.entities[ordinarySpawn.entityId]; for (const characterSource of ["wow", "rom", "coa"] as const) { const ordinaryHealth = dungeonEnemyScaling( source, ordinaryEntity, ordinaryBoss.id, 17, "normal", 1, characterSource, ).maxHealth; for (const definition of variants.values()) { const boss = definition.bosses[0]; const spawn = definition.staticSpawns.find((entry) => entry.id === boss.id)!; const entity = definition.entities[spawn.entityId]; const health = dungeonEnemyScaling( definition, entity, boss.id, 17, "normal", 1, characterSource, ).maxHealth; expect(health, `${characterSource}:${boss.name}`).toBeGreaterThan(0); expect(health, `${characterSource}:${boss.name}`).toBeLessThan(ordinaryHealth); // The established leveling curve interpolates a 0.685–0.835 WoW // floor/ceiling at level 17; editor work must not restore campaign HP. if (characterSource === "wow") { expect(health).toBeGreaterThanOrEqual(ordinaryHealth * 0.685 - 0.001); expect(health).toBeLessThanOrEqual(ordinaryHealth * 0.835 + 0.001); } } } }); it("uses the run identity for fresh, restarted, and shared sessions", () => { expect(activateDungeonSession(BITE_SIZED_STOCKADES_DUNGEON_ID, undefined, { runId: "run-a" })).toBe(true); const firstBoss = dungeonDefinitionById(BITE_SIZED_STOCKADES_DUNGEON_ID)!.bosses[0].name; expect(useDungeonRunStore.getState().progress?.id).toBe("run-a"); expect(restartActiveDungeonSession()).toBe(true); expect(dungeonDefinitionById(BITE_SIZED_STOCKADES_DUNGEON_ID)!.bosses[0].name).toBe(firstBoss); expect(activateDungeonSession(BITE_SIZED_STOCKADES_DUNGEON_ID, undefined, { runId: "run-b" })).toBe(true); expect(useDungeonRunStore.getState().progress?.id).toBe("run-b"); expect(dungeonDefinitionById(BITE_SIZED_STOCKADES_DUNGEON_ID)!.bosses[0].name).not.toBe(firstBoss); }); it("completes the one-boss run only through its stable objective", () => { const biteSized = createBiteSizedStockadesDefinition(source, "completion-run"); const bossId = biteSized.bosses[0].id; const encounter = biteSized.encounters![0]; expect(() => validateDungeonEncounterDefinition(encounter)).not.toThrow(); const snapshot = createDungeonEncounterSnapshot(encounter, 1_000); const reduced = reduceDungeonEncounter(encounter, snapshot, { kind: "tick", now: 2_000, facts: { registeredEntityIds: [bossId], deadEntityIds: [bossId], }, }).snapshot; expect(reduced.completedObjectiveIds).toEqual([bossId]); expect(reduced.flags["dungeon-complete"]).toBe(true); expect(reduced.phase).toBe("complete"); }); it("rebuilds seeded boss rules and records the original timer after a wipe and retry", () => { expect(activateDungeonSession(BITE_SIZED_STOCKADES_DUNGEON_ID, undefined, { runId: "stale-provider-a" })).toBe(true); const firstBossId = dungeonDefinitionById(BITE_SIZED_STOCKADES_DUNGEON_ID)!.bosses[0].id; let nextRunId = "stale-provider-b"; for (let index = 0; index < 1_000; index += 1) { const candidate = `stale-provider-${index}`; if (createBiteSizedStockadesDefinition(source, candidate).bosses[0].id !== firstBossId) { nextRunId = candidate; break; } } expect(activateDungeonSession(BITE_SIZED_STOCKADES_DUNGEON_ID, undefined, { runId: nextRunId })).toBe(true); const rerolledBossId = dungeonDefinitionById(BITE_SIZED_STOCKADES_DUNGEON_ID)!.bosses[0].id; expect(rerolledBossId).not.toBe(firstBossId); const party = [{ name: "Tester", classId: "priest", level: 17, role: "healer", npc: false }] as const; const run = useDungeonRunStore.getState(); run.start(1_000, party); advanceDungeonEncounters(BITE_SIZED_STOCKADES_DUNGEON_ID, { kind: "tick", now: 2_000, facts: { registeredEntityIds: [rerolledBossId], deadEntityIds: [] }, }); resetDungeonEncounters("boss-wipe", BITE_SIZED_STOCKADES_DUNGEON_ID, 3_000); expect(useDungeonRunStore.getState().progress?.startedAt).toBe(1_000); advanceDungeonEncounters(BITE_SIZED_STOCKADES_DUNGEON_ID, { kind: "tick", now: 9_000, facts: { registeredEntityIds: [rerolledBossId], deadEntityIds: [rerolledBossId] }, }); run.advance(activeDungeonCompletedObjectiveIds(BITE_SIZED_STOCKADES_DUNGEON_ID), [rerolledBossId], 9_000, party); expect(completedDungeonRun(useDungeonRunStore.getState().progress!)).toMatchObject({ id: nextRunId, durationMs: 8_000, killedBossIds: [rerolledBossId], fullClear: true, }); }); });