152 lines
6.3 KiB
TypeScript
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)
|
|
}
|
|
})
|
|
})
|