import { describe, expect, test } from "vitest" import { type Canvas, DRLG_GRID_FLAG_WAYPOINT, DRLG_GRID_FLAG_WAYPOINT_COLDPLAINS, DRLG_GRID_SPAWN_INVALID_MASK, DRLGOUTDOORS_SpawnAct12Waypoint, type PlacedPresetRect, rollLimited, type SeamConstraint, TestGridCellSpawnValid, type WildernessStats, } from "../src/game/wilderness.ts" import { Rng } from "../src/game/rng.ts" import type { Ds1Cell } from "../src/formats/ds1.ts" class TrackingRng extends Rng { rollCount = 0 override next(): number { this.rollCount += 1 return super.next() } } function makeOutdoorCanvas(width: number, height: number): Canvas { const cells: Ds1Cell[][] = [] for (let y = 0; y < height; y += 1) { const row: Ds1Cell[] = [] for (let x = 0; x < width; x += 1) { row.push({ walls: [], floors: [{ prop1: 2, sequence: 0, style: 0, unknown1: 0, unknown2: 0, hidden: false, }], shadows: [], substitutions: [], }) } cells.push(row) } return { width, height, cells, wallLayers: 1, floorLayers: 1, substitutionLayers: 1, objects: [], } } function makeEmptyStats(): WildernessStats { return { substitutions: [], borderPieces: {}, unresolved: [], notes: [], borderStamped: 0, groundCells: 0, groundTile: null, sizeSource: "default", roadCells: 0, roadSegments: 0, anchors: 0, specialPresets: [], entrances: [], waypointsSpawned: 0, shrinesSpawned: 0, interactivesSpawned: 0, } } describe("DRLGOUTDOORS_SpawnAct12Waypoint 1.13c Parity (Issue #80)", () => { describe("TestGridCellSpawnValid and DRLG_GRID_SPAWN_INVALID_MASK", () => { test("DRLG_GRID_SPAWN_INVALID_MASK matches 1.13c disassembly 0x1B81", () => { expect(DRLG_GRID_SPAWN_INVALID_MASK).toBe(0x1b81) }) test("cell flags with no 0x1B81 bits pass validation", () => { const validFlags = [ 0, 0x0002, 0x0004, 0x0008, 0x0010, 0x0020, 0x0040, 0x0400, 0x2000, 0x4000, 0x8000, DRLG_GRID_FLAG_WAYPOINT, // 0x10000 DRLG_GRID_FLAG_WAYPOINT_COLDPLAINS, // 0x20000 0x0002 | 0x0010 | 0x20000, ] for (const flag of validFlags) { const grid2D = [[flag]] expect(TestGridCellSpawnValid(grid2D, 0, 0)).toBe(true) const grid1D = [flag] expect(TestGridCellSpawnValid(grid1D, 0, 0, 1)).toBe(true) const gridU32 = new Uint32Array([flag]) expect(TestGridCellSpawnValid(gridU32, 0, 0, 1)).toBe(true) const gridObj = { width: 1, height: 1, flags: [flag] } expect(TestGridCellSpawnValid(gridObj, 0, 0)).toBe(true) } }) test("cell flags with any 0x1B81 bits fail validation", () => { const invalidFlags = [ 0x0001, // preset occupied 0x0080, // border cell 0x0100, // collision bit 0x0200, // collision bit 0x0800, // collision bit 0x1000, // walls / impassable 0x1b81, // full mask 0x1000 | 0x0002, // mixed 0x0001 | DRLG_GRID_FLAG_WAYPOINT, ] for (const flag of invalidFlags) { const grid2D = [[flag]] expect(TestGridCellSpawnValid(grid2D, 0, 0)).toBe(false) const grid1D = [flag] expect(TestGridCellSpawnValid(grid1D, 0, 0, 1)).toBe(false) const gridU32 = new Uint32Array([flag]) expect(TestGridCellSpawnValid(gridU32, 0, 0, 1)).toBe(false) } }) test("out of bounds coordinates safely return false", () => { const grid = [[0, 0], [0, 0]] expect(TestGridCellSpawnValid(grid, -1, 0)).toBe(false) expect(TestGridCellSpawnValid(grid, 0, -1)).toBe(false) expect(TestGridCellSpawnValid(grid, 2, 0)).toBe(false) expect(TestGridCellSpawnValid(grid, 0, 2)).toBe(false) }) }) describe("rollLimited helper", () => { test("rolls within [0, max - 1] and consumes exactly 1 roll per call", () => { const rng = new TrackingRng(0x12345678) const max = 36 for (let i = 0; i < 100; i += 1) { const prevRolls = rng.rollCount const val = rollLimited(rng, max) expect(val).toBeGreaterThanOrEqual(0) expect(val).toBeLessThan(max) expect(rng.rollCount).toBe(prevRolls + 1) } }) test("handles edge case max <= 0 without consuming or erroring", () => { const rng = new TrackingRng(0x12345678) expect(rollLimited(rng, 0)).toBe(0) expect(rollLimited(rng, -5)).toBe(0) expect(rng.rollCount).toBe(0) }) }) describe("Branch A: Cold Plains Special (levelId === 3)", () => { test("consumes exactly 0 RNG rolls", () => { const canvas = makeOutdoorCanvas(64, 64) const stats = makeEmptyStats() const rng = new TrackingRng(0xabcdef) const seamConstraints: SeamConstraint[] = [ { fromLevelId: 3, toLevelId: 2, side: "west", blockCoord: 0 }, ] DRLGOUTDOORS_SpawnAct12Waypoint( canvas, 3, 1, undefined, [], rng, stats, "Act 1 - Wilderness", undefined, seamConstraints, ) expect(stats.waypointsSpawned).toBe(1) expect(stats.waypointTile).toBeDefined() // Exactly 0 RNG rolls consumed! expect(rng.rollCount).toBe(0) }) test("clamps connection to Blood Moor on all 4 boundary sides and sets 0x20000 flag", () => { const testCases: { side: "west" | "east" | "north" | "south"; expectedBlockX: number; expectedBlockY: number }[] = [ { side: "west", expectedBlockX: 1, expectedBlockY: 4 }, // j=0 clamped to 1, i=4 { side: "east", expectedBlockX: 6, expectedBlockY: 4 }, // j=7 clamped to 6, i=4 { side: "north", expectedBlockX: 4, expectedBlockY: 1 }, // i=0 clamped to 1, j=4 { side: "south", expectedBlockX: 4, expectedBlockY: 6 }, // i=7 clamped to 6, j=4 ] for (const tc of testCases) { const canvas = makeOutdoorCanvas(64, 64) // 8x8 blocks (0..7) const stats = makeEmptyStats() const rng = new TrackingRng(0x9999) const seamConstraints: SeamConstraint[] = [ { fromLevelId: 3, toLevelId: 2, side: tc.side, blockCoord: 4 }, ] DRLGOUTDOORS_SpawnAct12Waypoint( canvas, 3, 1, undefined, [], rng, stats, "Act 1 - Wilderness", undefined, seamConstraints, ) expect(stats.waypointsSpawned).toBe(1) expect(stats.waypointTile).toBeDefined() const wpTile = stats.waypointTile! const blockX = Math.floor(wpTile.x / 8) const blockY = Math.floor(wpTile.y / 8) expect(blockX).toBe(tc.expectedBlockX) expect(blockY).toBe(tc.expectedBlockY) const note = stats.notes.find(n => n.includes("Cold Plains special grid cell")) expect(note).toBeDefined() expect(note).toContain(`(${tc.expectedBlockX}, ${tc.expectedBlockY})`) expect(rng.rollCount).toBe(0) } }) test("clamps extreme boundary corners (e.g. j=0 clamped to 1, i=0 clamped to 1)", () => { const canvas = makeOutdoorCanvas(64, 64) const stats = makeEmptyStats() const rng = new TrackingRng(0x4242) // West gate at extreme top (blockCoord 0) const seamConstraints: SeamConstraint[] = [ { fromLevelId: 3, toLevelId: 2, side: "west", blockCoord: 0 }, ] DRLGOUTDOORS_SpawnAct12Waypoint( canvas, 3, 1, undefined, [], rng, stats, "Act 1 - Wilderness", undefined, seamConstraints, ) expect(stats.waypointTile).toBeDefined() const blockX = Math.floor(stats.waypointTile!.x / 8) const blockY = Math.floor(stats.waypointTile!.y / 8) expect(blockX).toBe(1) expect(blockY).toBe(1) expect(rng.rollCount).toBe(0) }) }) describe("Branch B: General Path (e.g. Stony Field, levelId === 4)", () => { test("consumes exactly 2 * nArea RNG rolls for coordinate shuffle", () => { // 64x64 tiles -> 8x8 blocks of 8x8 tiles // nGridWidth = 8, nGridHeight = 8 // nWidth = 8 - 2 = 6, nHeight = 8 - 2 = 6 // nArea = 6 * 6 = 36 // Exactly 2 * nArea = 72 rolls for shuffle, and 0 rolls during placement search const canvas = makeOutdoorCanvas(64, 64) const stats = makeEmptyStats() const rng = new TrackingRng(0x778899) DRLGOUTDOORS_SpawnAct12Waypoint(canvas, 4, 1, undefined, [], rng, stats) expect(stats.waypointsSpawned).toBe(1) expect(stats.waypointTile).toBeDefined() const expectedRolls = 2 * 36 expect(rng.rollCount).toBe(expectedRolls) }) test("consumes exactly 2 * nArea rolls for larger map (80x80 tiles -> 10x10 blocks -> 8x8 area = 64 cells -> 128 rolls)", () => { const canvas = makeOutdoorCanvas(80, 80) const stats = makeEmptyStats() const rng = new TrackingRng(0x112233) DRLGOUTDOORS_SpawnAct12Waypoint(canvas, 4, 1, undefined, [], rng, stats) expect(stats.waypointsSpawned).toBe(1) expect(stats.waypointTile).toBeDefined() // nWidth = 10 - 2 = 8, nHeight = 10 - 2 = 8, nArea = 64 // 2 * 64 = 128 rolls expect(rng.rollCount).toBe(128) }) test("skips cells occupied by preset rectangles (obstacle-free placement via TestGridCellSpawnValid)", () => { const canvas = makeOutdoorCanvas(64, 64) const stats = makeEmptyStats() const rng = new TrackingRng(0x5555) // Mark preset covering block (1, 1) -> tiles [8..15, 8..15] const placedPresets: PlacedPresetRect[] = [ { name: "PresetObstacle", x: 8, y: 8, w: 8, h: 8 }, ] DRLGOUTDOORS_SpawnAct12Waypoint(canvas, 4, 1, undefined, placedPresets, rng, stats) expect(stats.waypointsSpawned).toBe(1) expect(stats.waypointTile).toBeDefined() const wp = stats.waypointTile! const blockX = Math.floor(wp.x / 8) const blockY = Math.floor(wp.y / 8) // Block (1, 1) had an obstacle so waypoint must not be in (1, 1) expect(blockX === 1 && blockY === 1).toBe(false) // Must be within inner bounds [1..6, 1..6] expect(blockX).toBeGreaterThanOrEqual(1) expect(blockX).toBeLessThanOrEqual(6) expect(blockY).toBeGreaterThanOrEqual(1) expect(blockY).toBeLessThanOrEqual(6) }) test("is deterministic: identical seeds produce identical waypoint coordinates", () => { const canvas1 = makeOutdoorCanvas(64, 64) const stats1 = makeEmptyStats() const rng1 = new TrackingRng(0xfeedbeef) DRLGOUTDOORS_SpawnAct12Waypoint(canvas1, 4, 1, undefined, [], rng1, stats1) const canvas2 = makeOutdoorCanvas(64, 64) const stats2 = makeEmptyStats() const rng2 = new TrackingRng(0xfeedbeef) DRLGOUTDOORS_SpawnAct12Waypoint(canvas2, 4, 1, undefined, [], rng2, stats2) expect(stats1.waypointTile).toEqual(stats2.waypointTile) expect(rng1.rollCount).toBe(rng2.rollCount) }) }) })