import { existsSync, readFileSync } from "node:fs"; import path from "node:path"; import { describe, expect, it } from "vitest"; import { FORSAKEN_ABBEY_DEFINITION } from "./dungeons/forsakenAbbey"; import { FORSAKEN_ABBEY_BOSSES, FORSAKEN_ABBEY_ENTITIES, FORSAKEN_ABBEY_POPULATION_PROVENANCE, FORSAKEN_ABBEY_STATIC_SPAWNS, } from "./generated/forsakenAbbeyPopulation.generated"; const projectFile = (...parts: string[]) => path.join(process.cwd(), ...parts); const snapshot = JSON.parse(readFileSync(projectFile( "scripts", "runewaker-pipeline", "snapshots", "forsaken-abbey-zone-102.json", ), "utf8")); const report = JSON.parse(readFileSync(projectFile( "src", "assets", "game", "dungeons", "forsaken-abbey", "population-import-report.json", ), "utf8")); describe("Forsaken Abbey portable RuneWaker population", () => { it("accounts for every active Zone 102 source row reproducibly", () => { expect(snapshot.rowCount).toBe(251); expect(snapshot.rows).toHaveLength(251); expect(new Set(snapshot.rows.map((row: { spawnId: number }) => row.spawnId)).size).toBe(251); expect(snapshot.contentSha256) .toBe("13ef6edd3df6e71389aa92d01201889148c0794cf9ecaafdbaf90d3145d2bf7b"); const classifications = snapshot.rows.reduce( (counts: Record, row: { classification: string }) => { counts[row.classification] = (counts[row.classification] ?? 0) + 1; return counts; }, {}, ); expect(classifications).toEqual({ combat: 142, boss: 8, "deferred-object": 101, }); }); it("generates unique static combat spawns without AzerothCore id collisions", () => { expect(FORSAKEN_ABBEY_STATIC_SPAWNS).toHaveLength(150); expect(new Set(FORSAKEN_ABBEY_STATIC_SPAWNS.map((spawn) => spawn.id)).size).toBe(150); expect(Object.keys(FORSAKEN_ABBEY_ENTITIES)).toHaveLength(15); for (const entity of Object.values(FORSAKEN_ABBEY_ENTITIES)) { expect(entity.id).toMatch(/^rw-fa-[0-9]+$/); expect(entity.combat).not.toHaveProperty("serverEntry"); expect(entity.visual.model.url.startsWith("/assets/creatures/forsaken-abbey/")).toBe(true); expect(entity.visual.model.url.endsWith(".glb")).toBe(true); expect(entity.visual.model.animationMode).toBe("native"); expect(existsSync(projectFile("public", entity.visual.model.url.slice(1)))).toBe(true); } for (const spawn of FORSAKEN_ABBEY_STATIC_SPAWNS) { expect(spawn.id).toMatch(/^rw-fa-spawn-[0-9]+$/); expect(spawn.position.every(Number.isFinite)).toBe(true); expect(Number.isFinite(spawn.yaw)).toBe(true); } }); it("classifies the authoritative five boss templates into eight objectives", () => { const bossTemplateIds = [...new Set(snapshot.rows .filter((row: { classification: string }) => row.classification === "boss") .map((row: { templateId: number }) => row.templateId))] .sort((a, b) => a - b); expect(bossTemplateIds).toEqual([100070, 100074, 100561, 100562, 100563]); expect(FORSAKEN_ABBEY_BOSSES).toHaveLength(8); expect(FORSAKEN_ABBEY_BOSSES.filter((boss) => boss.name.startsWith("Necromage Keeper"))) .toHaveLength(4); for (const boss of FORSAKEN_ABBEY_BOSSES) { const spawn = FORSAKEN_ABBEY_STATIC_SPAWNS.find((candidate) => candidate.id === boss.id); expect(spawn).toBeDefined(); expect(FORSAKEN_ABBEY_ENTITIES[spawn!.entityId].kind).toBe("boss"); } }); it("projects every source row and keeps all combat spawns within two meters", () => { expect(report.records).toHaveLength(251); expect(report.excluded).toHaveLength(101); expect(FORSAKEN_ABBEY_POPULATION_PROVENANCE.navmeshProjection.maximumCombatDistance) .toBeLessThanOrEqual(2); expect(report.records.every((row: { projectedPosition: number[] }) => ( row.projectedPosition.length === 3 && row.projectedPosition.every(Number.isFinite) ))).toBe(true); }); it("is the packaged population used by the playable dungeon definition", () => { expect(FORSAKEN_ABBEY_DEFINITION.availability).toBe("available"); expect(FORSAKEN_ABBEY_DEFINITION.entities).toBe(FORSAKEN_ABBEY_ENTITIES); expect(FORSAKEN_ABBEY_DEFINITION.staticSpawns).toBe(FORSAKEN_ABBEY_STATIC_SPAWNS); expect(FORSAKEN_ABBEY_DEFINITION.bosses).toBe(FORSAKEN_ABBEY_BOSSES); }); });