diff --git a/scripts/pack-act-assets.ts b/scripts/pack-act-assets.ts index ebb47fc..b0ba3db 100644 --- a/scripts/pack-act-assets.ts +++ b/scripts/pack-act-assets.ts @@ -30,7 +30,7 @@ import { join } from 'node:path' import { MpqArchive } from '../src/mpq/archive.ts' import { fileSource } from '../src/mpq/file-source.ts' import { MountedArchives } from '../src/mpq/mount.ts' -import { cell, loadActTables, parseTable, resolveLevel, resolveLevelLibraries, tileMemberPath } from '../src/game/acts.ts' +import { cell, loadActTables, parseTable, resolveLevel, resolveLevelLibraries, tileMemberPath, ACT_TOWNS } from '../src/game/acts.ts' import type { ActTables, LevelInfo, D2Table } from '../src/game/acts.ts' import { decodeDs1 } from '../src/formats/ds1.ts' import type { Ds1 } from '../src/formats/ds1.ts' @@ -366,9 +366,6 @@ const monstersTable = { */ const ACT_LAYOUT_SEED = 0x5eed_2000 -/** Where each act's town is, for working out which way is "up" in a dungeon. */ -const ACT_TOWNS: readonly number[] = [1, 40, 75, 103, 109] - const worldGraph: WorldGraph = assignGateSides( buildWorldGraph(parseLevelRows(tables.levels)), ACT_LAYOUT_SEED, diff --git a/scripts/verify-world-graph.ts b/scripts/verify-world-graph.ts index 9d83983..f6bccb6 100644 --- a/scripts/verify-world-graph.ts +++ b/scripts/verify-world-graph.ts @@ -33,7 +33,7 @@ import { readFileSync } from 'node:fs' import { MountedArchives } from '../src/mpq/mount.ts' import { MpqArchive } from '../src/mpq/archive.ts' import { fileSource } from '../src/mpq/file-source.ts' -import { loadActTables } from '../src/game/acts.ts' +import { loadActTables, ACT_TOWNS } from '../src/game/acts.ts' import { parseLevelRows, buildWorldGraph, @@ -56,8 +56,6 @@ const dir = process.argv[2] ?? 'samples/d2' const MOUNTS = ['d2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq'] /** The baked asset packs. */ const PACK_INDEX = 'samples/d2-packs/index.json' -/** The town of each act, keyed by the 0-based `Act` column. */ -const ACT_TOWNS: readonly number[] = [1, 40, 75, 103, 109] /** Waypoint ids run 0..38 with no gaps. */ const WAYPOINT_COUNT = 39 /** How many seeds to exercise the side solver with. */ diff --git a/src/game/acts.ts b/src/game/acts.ts index 6d10208..6d3dc41 100644 --- a/src/game/acts.ts +++ b/src/game/acts.ts @@ -183,6 +183,28 @@ export interface LevelInfo { */ export type ActTown = LevelInfo +/** + * Town level IDs for Acts 1..5 in order: + * Act 1: Rogue Encampment (1) + * Act 2: Lut Gholein (40) + * Act 3: Kurast Docks (75) + * Act 4: The Pandemonium Fortress (103) + * Act 5: Harrogath (109) + */ +export const ACT_TOWNS: readonly number[] = [1, 40, 75, 103, 109] + +/** + * Returns the town level ID for an act (1..5). + * Defaults to Act 5 Harrogath (109) for acts >= 5, or Act 1 Rogue Encampment (1) for acts <= 1. + */ +export function townLevelForAct(act: number): number { + if (act >= 1 && act <= 5) { + return ACT_TOWNS[act - 1]! + } + if (act <= 1) return ACT_TOWNS[0]! + return ACT_TOWNS[4]! +} + /** Turn a table-relative tile path into a member name. */ export function tileMemberPath(relative: string): string { return `${TILES}${relative.replaceAll('/', '\\')}` diff --git a/src/game/portal.ts b/src/game/portal.ts index da4ded1..f59db5c 100644 --- a/src/game/portal.ts +++ b/src/game/portal.ts @@ -213,27 +213,4 @@ export class TownPortalSlot { } } -/** - * The town for an act. - * - * Hard-coded because `Levels.txt` does not mark towns: the Rogue Encampment's - * row looks like any other preset. The ids are stable across every version of - * the game. - * - * @param act - the act, 1..5. - * @returns the town's level id. - */ -export function townLevelForAct(act: number): number { - switch (act) { - case 1: - return 1 - case 2: - return 40 - case 3: - return 75 - case 4: - return 103 - default: - return 109 - } -} +export { ACT_TOWNS, townLevelForAct } from './acts.ts' diff --git a/src/game/wilderness.ts b/src/game/wilderness.ts index 05e7df1..dc79fb1 100644 --- a/src/game/wilderness.ts +++ b/src/game/wilderness.ts @@ -4941,16 +4941,7 @@ export function generateWilderness(request: WildernessRequest): WildernessResult let floorLayers = 1 let substitutionType = 0 let version = 0 - let inferredAct = 1 - if (request.levelId >= 40 && request.levelId < 75) inferredAct = 2 - else if (request.levelId >= 75 && request.levelId < 103) inferredAct = 3 - else if (request.levelId >= 103 && request.levelId < 109) inferredAct = 4 - else if (request.levelId >= 109) inferredAct = 5 - else if (/Act 2/i.test(request.levelTypeName)) inferredAct = 2 - else if (/Act 3/i.test(request.levelTypeName)) inferredAct = 3 - else if (/Act 4/i.test(request.levelTypeName)) inferredAct = 4 - else if (/Act 5/i.test(request.levelTypeName)) inferredAct = 5 - + const inferredAct = actOfLevel(request.levelId, request.levelTypeName) let act = inferredAct const allLevels: Ds1[] = [] for (const piece of request.pieces) for (const level of piece.levels) allLevels.push(level) diff --git a/tests/world-graph.test.ts b/tests/world-graph.test.ts index efe14ac..458445f 100644 --- a/tests/world-graph.test.ts +++ b/tests/world-graph.test.ts @@ -1,5 +1,7 @@ import { describe, it, expect } from 'vitest' import type { D2Table } from '../src/game/acts.ts' +import { ACT_TOWNS, townLevelForAct } from '../src/game/acts.ts' +import { actOfLevel } from '../src/game/wilderness.ts' import { parseLevelRows, buildWorldGraph, @@ -444,3 +446,37 @@ describe('seamArrivalSpot', () => { expect(Math.abs(spot.y - 0)).toBeGreaterThan(SEAM_TRIGGER_SUBTILES) }) }) + +describe('ACT_TOWNS and townLevelForAct single source of truth', () => { + it('defines the correct canonical town level IDs for Acts 1..5', () => { + expect(ACT_TOWNS).toEqual([1, 40, 75, 103, 109]) + }) + + it('resolves townLevelForAct for all 5 acts correctly', () => { + expect(townLevelForAct(1)).toBe(1) + expect(townLevelForAct(2)).toBe(40) + expect(townLevelForAct(3)).toBe(75) + expect(townLevelForAct(4)).toBe(103) + expect(townLevelForAct(5)).toBe(109) + }) + + it('handles boundary / out-of-range acts gracefully', () => { + expect(townLevelForAct(0)).toBe(1) + expect(townLevelForAct(-5)).toBe(1) + expect(townLevelForAct(6)).toBe(109) + expect(townLevelForAct(99)).toBe(109) + }) + + it('actOfLevel correctly partitions level IDs matching ACT_TOWNS', () => { + expect(actOfLevel(1)).toBe(1) + expect(actOfLevel(39)).toBe(1) + expect(actOfLevel(40)).toBe(2) + expect(actOfLevel(74)).toBe(2) + expect(actOfLevel(75)).toBe(3) + expect(actOfLevel(102)).toBe(3) + expect(actOfLevel(103)).toBe(4) + expect(actOfLevel(108)).toBe(4) + expect(actOfLevel(109)).toBe(5) + expect(actOfLevel(136)).toBe(5) + }) +})