diablo2-web/tests/wilderness-population.test.ts

152 lines
6.3 KiB
TypeScript

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)
}
})
})