import { describe, expect, test } from "vitest" import { UNIMPLEMENTED_PASSES } from "../src/game/wilderness.ts" import { rollEliteModifiers, applyEliteModifiers, CANONICAL_ELITE_MODIFIERS, planLevelMonsters, } from "../src/game/monsters.ts" import { spawnMonsterPacks, createWorld, type MonsterPack, type SafeZone } from "../src/game/combat.ts" import { parseTable } from "../src/game/acts.ts" import { Rng } from "../src/game/rng.ts" // Act I outdoor population (the Act I waypoints, shrines, Flavie and the SuperUniques of Cold Plains, // Stony Field, Dark Wood, Burial Grounds and Moo Moo Farm) comes from the DRLG port and is checked on // its output in tests/drlg-act1-population.test.ts. describe("Wilderness Population Phase (Issue #36)", () => { test("DRLGOUTDOORS_SpawnAct12Waypoint and DRLGOUTDOORS_SpawnAct12Shrines are removed from UNIMPLEMENTED_PASSES", () => { expect(UNIMPLEMENTED_PASSES).not.toContain("DRLGOUTDOORS_SpawnAct12Waypoint") expect(UNIMPLEMENTED_PASSES).not.toContain("DRLGOUTDOORS_SpawnAct12Shrines") }) test("rollEliteModifiers and applyEliteModifiers roll and apply MonUMod bonuses", () => { const rng = new Rng(0xabcdef) const mods = rollEliteModifiers("unique", rng) expect(mods.length).toBeGreaterThanOrEqual(1) const allowedNames = CANONICAL_ELITE_MODIFIERS.map(m => m.name) for (const m of mods) { expect(allowedNames).toContain(m) } const baseStats = { id: "fallen1", name: "Fallen", hp: 20, damage: 4, speed: 2, reach: 32, aggroRadius: 220, cooldownTicks: 25, xp: 10, } const strongFast = applyEliteModifiers(baseStats, "unique", ["strong", "fast"], "Rakanishu") expect(strongFast.rank).toBe("unique") expect(strongFast.superUniqueId).toBe("Rakanishu") expect(strongFast.modifiers).toEqual(["strong", "fast"]) expect(strongFast.damage).toBe(6) // 4 * 1.5 expect(strongFast.speed).toBeCloseTo(2.8) // 2 * 1.4 }) test("spawnMonsterPacks enforces safeZones exclusion radius and anchors fixedPosition packs", () => { const world = createWorld(500, 500) const safeZones: SafeZone[] = [ { x: 500, y: 500, radius: 200 }, ] const packs: MonsterPack[] = [ { members: [{ id: "fallen1", name: "Fallen", hp: 10, damage: 2, speed: 2, reach: 30, aggroRadius: 200, cooldownTicks: 20, xp: 5 }], }, { members: [{ id: "fallen1", name: "Fallen", hp: 10, damage: 2, speed: 2, reach: 30, aggroRadius: 200, cooldownTicks: 20, xp: 5 }], }, { superUniqueId: "Rakanishu", fixedPosition: { x: 1200, y: 1200 }, members: [{ id: "fallen1", name: "Rakanishu", hp: 80, damage: 10, speed: 3, reach: 30, aggroRadius: 250, cooldownTicks: 20, xp: 50, rank: "unique", superUniqueId: "Rakanishu" }], }, ] const placed = spawnMonsterPacks(world, packs, { x: 500, y: 500 }, 800, { overlap: () => 0 }, safeZones) expect(placed).toBe(3) expect(world.monsters.length).toBe(3) for (const monster of world.monsters) { if (monster.stats.superUniqueId === "Rakanishu") { // Anchored to fixedPosition (1200, 1200) expect(Math.hypot(monster.x - 1200, monster.y - 1200)).toBeLessThan(80) } else { // Must respect safeZone around (500, 500) with radius 200 const dist = Math.hypot(monster.x - 500, monster.y - 500) expect(dist).toBeGreaterThanOrEqual(200) } } }) test("planLevelMonsters plans fixed SuperUniques for Cold Plains (3), Stony Field (4), Dark Wood (5), and Burial Grounds (17)", () => { const enc = new TextEncoder() const levelsTxt = parseTable( enc.encode( [ "Id\tName\tMonDen\tMonUMin\tMonUMax\tMonLvl1Ex\tm1\tm2\tm3", "3\tCold Plains\t600\t2\t4\t2\tfallen1\tfallen2\tzombie1", "4\tStony Field\t600\t2\t4\t4\tfallen1\tfallen2\tskeleton1", "5\tDark Wood\t600\t2\t4\t5\tbrute1\tfallen1\tskeleton1", "17\tBurial Grounds\t500\t1\t2\t3\tzombie1\tskeleton1\tcorruptrogue1", ].join("\n"), ), ) const monstatsTxt = parseTable( enc.encode( [ "Id\tBaseId\tNameStr\tLevel\tMinGrp\tMaxGrp\tVelocity\trun\tAC\tExp\tminHP\tmaxHP\tA1MinD\tA1MaxD\tA1TH\tenabled\tissend\tisSpawn\tisMelee", "fallen1\tfallen1\tFallen\t1\t3\t6\t8\t12\t5\t15\t8\t14\t1\t3\t20\t1\t1\t1\t1", "fallen2\tfallen1\tCarver\t2\t3\t6\t8\t12\t8\t25\t12\t20\t2\t4\t28\t1\t1\t1\t1", "fallenshaman1\tfallenshaman1\tFallen Shaman\t2\t1\t2\t7\t10\t10\t35\t16\t26\t3\t6\t30\t1\t1\t1\t1", "zombie1\tzombie1\tZombie\t1\t2\t4\t5\t8\t6\t18\t14\t22\t2\t5\t22\t1\t1\t1\t1", "skeleton1\tskeleton1\tSkeleton\t2\t3\t5\t7\t10\t8\t22\t12\t18\t2\t5\t26\t1\t1\t1\t1", "brute1\tbrute1\tGargantuan Beast\t5\t2\t4\t7\t10\t18\t65\t30\t45\t5\t10\t45\t1\t1\t1\t1", "corruptrogue1\tcorruptrogue1\tCorrupt Rogue\t3\t3\t5\t8\t12\t12\t30\t15\t24\t3\t6\t35\t1\t1\t1\t1", ].join("\n"), ), ) const monlvlTxt = parseTable( enc.encode( [ "Level\tAC\tTH\tHP\tDM\tXP", "1\t100\t100\t100\t100\t100", "2\t100\t100\t100\t100\t100", "3\t100\t100\t100\t100\t100", "4\t100\t100\t100\t100\t100", "5\t100\t100\t100\t100\t100", "6\t100\t100\t100\t100\t100", "7\t100\t100\t100\t100\t100", "8\t100\t100\t100\t100\t100", ].join("\n"), ), ) const tables = { levels: levelsTxt, monstats: monstatsTxt, monlvl: monlvlTxt } for (const [lvlId, suName] of [ [3, "Bishibosh"], [4, "Rakanishu"], [5, "Treehead WoodFist"], [17, "Blood Raven"], ] as const) { const planned = planLevelMonsters(tables, lvlId, 6400, 0x12345 + lvlId, 3.5, "normal", { landmarks: [{ id: suName, tileX: 40, tileY: 40, fixedPosition: { x: 1000, y: 1000 } }], }) expect(planned.superUniques).toContain(suName) const suPack = planned.packs.find(p => p.superUniqueId === suName) expect(suPack).toBeDefined() expect(suPack?.fixedPosition).toEqual({ x: 1000, y: 1000 }) expect(suPack?.landmarkTile).toEqual({ tileX: 40, tileY: 40 }) expect(suPack?.members[0]?.rank).toBe("unique") expect(suPack?.members[0]?.superUniqueId).toBe(suName) expect(suPack?.members[0]?.modifiers?.length).toBeGreaterThan(0) } }) })