diff --git a/scripts/drlg-diff.ts b/scripts/drlg-diff.ts index f82556f..49cec2e 100644 --- a/scripts/drlg-diff.ts +++ b/scripts/drlg-diff.ts @@ -26,6 +26,7 @@ import { DrlgTraceWriter, dumpAct1, type DrlgDump } from '../src/game/drlg/drlg- import { firstDiff, firstTraceDivergence, formatDiff, fsDrlgSource, ORACLE_DATA, runOracle } from './lib/drlg-oracle.ts' interface Args { + act: number | undefined levels: number[] seeds: number[] difficulty: number @@ -39,6 +40,14 @@ interface Args { traceCheck: boolean } +const ACT_DEFAULT_OUTDOOR_LEVELS: Record = { + 0: [2, 3, 4, 5, 6, 7, 17, 26, 27, 28, 32, 33, 39], + 1: [41, 42, 43, 44, 45, 46], + 2: [76, 77, 78, 79, 80, 81, 82, 83], + 3: [104, 105, 106, 108], + 4: [110, 111, 112, 117, 134], +} + function parseLevels(spec: string): number[] { const out: number[] = [] for (const part of spec.split(',')) { @@ -54,8 +63,9 @@ function parseLevels(spec: string): number[] { } function parseArgs(argv: string[]): Args { - const args: Args = { levels: [], seeds: [], difficulty: 0, isolated: false, dataDir: ORACLE_DATA, stopOnFirst: false, report: null, noActivation: false, traceCheck: false } + const args: Args = { act: undefined, levels: [], seeds: [], difficulty: 0, isolated: false, dataDir: ORACLE_DATA, stopOnFirst: false, report: null, noActivation: false, traceCheck: false } let nRandom = 0 + const positionals: string[] = [] for (let i = 0; i < argv.length; i += 1) { const a = argv[i]! const next = (): string => { @@ -63,7 +73,8 @@ function parseArgs(argv: string[]): Args { if (v === undefined) throw new Error(`missing value for ${a}`) return v } - if (a === '--levels') args.levels = parseLevels(next()) + if (a === '--act') args.act = Number(next()) + else if (a === '--levels') args.levels = parseLevels(next()) else if (a === '--seeds') nRandom = Number(next()) else if (a === '--seed') args.seeds.push(...next().split(',').filter(Boolean).map(s => Number(s) >>> 0)) else if (a === '--difficulty') args.difficulty = Number(next()) @@ -73,8 +84,22 @@ function parseArgs(argv: string[]): Args { else if (a === '--report') args.report = next() else if (a === '--no-activation') args.noActivation = true else if (a === '--trace-check') args.traceCheck = true + else if (!a.startsWith('-')) positionals.push(a) else throw new Error(`unknown argument ${a}`) } + if (positionals.length > 0) { + const posAct = Number(positionals[0]) + const actIdx = posAct >= 1 && posAct <= 5 ? posAct - 1 : posAct + if (!Number.isInteger(actIdx) || actIdx < 0 || actIdx > 4) throw new Error(`bad positional act "${positionals[0]}"`) + args.act = actIdx + if (args.levels.length === 0) args.levels = [...ACT_DEFAULT_OUTDOOR_LEVELS[actIdx]!] + if (positionals.length >= 2 && nRandom === 0 && args.seeds.length === 0) { + nRandom = Number(positionals[1]) + } + } + if (args.act !== undefined && (!Number.isInteger(args.act) || args.act < 0 || args.act > 4)) { + throw new Error('--act must be 0..4') + } if (!Number.isInteger(nRandom) || nRandom < 0) throw new Error('--seeds must be a non-negative integer') if (nRandom > 0) args.seeds.push(...webcrypto.getRandomValues(new Uint32Array(nRandom))) if (args.seeds.length === 0) throw new Error('give --seeds N or --seed X') @@ -125,6 +150,7 @@ function main(): void { const runPort = (seed: number, trace: DrlgTraceWriter | undefined): DrlgDump => dumpAct1(createDrlgEnv(source, tables), seed, args.levels, { + ...(args.act !== undefined ? { act: args.act } : {}), difficulty: args.difficulty, isolated: args.isolated, actOnly: args.levels.length === 0, @@ -134,7 +160,14 @@ function main(): void { for (const seed of args.seeds) { const seedHex = `0x${seed.toString(16).padStart(8, '0')}` - const runOpts = { seed, levels: args.levels, difficulty: args.difficulty, isolated: args.isolated, dataDir: args.dataDir } + const runOpts = { + seed, + levels: args.levels, + ...(args.act !== undefined ? { act: args.act } : {}), + difficulty: args.difficulty, + isolated: args.isolated, + dataDir: args.dataDir, + } let result: SeedResult try { const oracleRun = runOracle({ ...runOpts, trace: args.traceCheck }) diff --git a/scripts/lib/drlg-mpq-source.ts b/scripts/lib/drlg-mpq-source.ts index 940a116..ed0197e 100644 --- a/scripts/lib/drlg-mpq-source.ts +++ b/scripts/lib/drlg-mpq-source.ts @@ -64,9 +64,17 @@ export function drlgActDt1Files(t: DrlgTables, act1Based: number): string[] { const key = normalizeDrlgPath(path) if (!out.has(key)) out.set(key, path) } + const extraLevelTypes = new Set() + if (act1Based === 5) { + for (let id = 109; id < t.levelDefs.length; id += 1) { + const def = t.levelDefs[id] + if (def && def.dwLevelType > 0) extraLevelTypes.add(def.dwLevelType) + } + } let rows = 0 - for (const rec of t.lvlTypes) { - if (rec.nAct !== act1Based) continue + for (let typeId = 0; typeId < t.lvlTypes.length; typeId += 1) { + const rec = t.lvlTypes[typeId]! + if (rec.nAct !== act1Based && !extraLevelTypes.has(typeId)) continue rows += 1 // DATATBLS_LoadLevelTypesTxt leaves names of one character or less ("0", "") unprefixed: they are // unused slots that DRLGROOMTILE_LoadDT1FilesForRoom never opens. diff --git a/scripts/lib/drlg-oracle.ts b/scripts/lib/drlg-oracle.ts index 52af170..4c9aa7a 100644 --- a/scripts/lib/drlg-oracle.ts +++ b/scripts/lib/drlg-oracle.ts @@ -39,6 +39,7 @@ export interface OracleRunOptions { readonly seed: number /** Empty: act stage only. */ readonly levels: readonly number[] + readonly act?: number readonly difficulty?: number readonly isolated?: boolean readonly trace?: boolean @@ -58,6 +59,7 @@ export function runOracle(opts: OracleRunOptions): OracleRunResult { const tracePath = join(dir, 'trace.txt') const args = ['--data', opts.dataDir ?? ORACLE_DATA, '--seed', `0x${(opts.seed >>> 0).toString(16)}`, '--out', out] if (opts.levels.length > 0) args.push('--levels', opts.levels.join(',')) + if (opts.act !== undefined) args.push('--act', String(opts.act)) if (opts.difficulty !== undefined) args.push('--difficulty', String(opts.difficulty)) if (opts.isolated) args.push('--isolated') if (opts.trace) args.push('--trace', tracePath) diff --git a/src/game/drlg/drlg-act-links.ts b/src/game/drlg/drlg-act-links.ts index 3865046..a979da0 100644 --- a/src/game/drlg/drlg-act-links.ts +++ b/src/game/drlg/drlg-act-links.ts @@ -1,44 +1,121 @@ /** - * D2Common act layout (level placement and level-to-level links): 1:1 port of the Act I parts of - * D2MOO source/D2Common/src/Drlg/DrlgOutPlace.cpp (commit 5596f5c, MIT License, Copyright (c) - * 2020-2025 The Phrozen Keep community): DRLGOUTPLACE_CreateLevelConnections and helpers. - * - * The solver works on a COPY of pDrlg->pSeed (sub_6FD823C0), so its rolls do not advance the act - * seed. Only the Black Marsh branch advances pDrlg->pSeed, with an inlined (untraced) LCG step. + * D2Common act layout (level placement and level-to-level links): 1:1 port of + * D2MOO source/D2Common/src/Drlg/DrlgOutPlace.cpp and DrlgOutRoom.cpp (commit 5596f5c, MIT License, + * Copyright (c) 2020-2025 The Phrozen Keep community): DRLGOUTPLACE_CreateLevelConnections, + * DRLG_GenerateJungles, and link helpers. */ -import { D2SEED_AdvanceInline, SEED_RollRandomNumber, type D2SeedStrc } from '../d2-rng.ts' -import { DATATBLS_GetLevelDefRecord } from './drlg-tables.ts' -import { - ACT_I, - ACT_V, - DRLGTYPE_OUTDOOR, - DRLGTYPE_PRESET, - LEVEL_BLACKMARSH, - LEVEL_BLOODMOOR, - LEVEL_BURIALGROUNDS, - LEVEL_COLDPLAINS, - LEVEL_DARKWOOD, - LEVEL_LUTGHOLEIN, - LEVEL_MONASTERYGATE, - LEVEL_MOOMOOFARM, - LEVEL_OUTERCLOISTER, - LEVEL_ROGUEENCAMPMENT, - LEVEL_STONYFIELD, - LEVEL_TAMOEHIGHLAND, - newCoord, - type D2DrlgCoordStrc, - type D2DrlgLevelStrc, - type D2DrlgStrc, - type DrlgEnv, -} from './drlg-types.ts' +import { D2SEED_AdvanceInline, SEED_RollLimitedRandomNumber, SEED_RollRandomNumber, type D2SeedStrc } from '../d2-rng.ts' import { DRLG_GetDirectionFromCoordinates, DRLG_GetDrlgWarpFromLevelId, DRLG_GetLevel, DRLG_SetWarpId, } from './drlg-drlg.ts' -import { DRLG_CheckNotOverlappingUsingManhattanDistance, DRLGROOM_AddOrth, DRLGROOM_GetVisArrayFromLevelId } from './drlg-room.ts' +import { + LVLPREST_ACT3_CLEARING_WEBBY_E, + LVLPREST_ACT3_CLEARING_WEBBY_EW, + LVLPREST_ACT3_CLEARING_WEBBY_N, + LVLPREST_ACT3_CLEARING_WEBBY_NE, + LVLPREST_ACT3_CLEARING_WEBBY_NS, + LVLPREST_ACT3_CLEARING_WEBBY_NW, + LVLPREST_ACT3_CLEARING_WEBBY_S, + LVLPREST_ACT3_CLEARING_WEBBY_SE, + LVLPREST_ACT3_CLEARING_WEBBY_SW, + LVLPREST_ACT3_CLEARING_WEBBY_W, + LVLPREST_ACT3_JUNGLE_E_N, + LVLPREST_ACT3_JUNGLE_E_S, + LVLPREST_ACT3_JUNGLE_E_W, + LVLPREST_ACT3_JUNGLE_EW_N, + LVLPREST_ACT3_JUNGLE_EW_S, + LVLPREST_ACT3_JUNGLE_N_E, + LVLPREST_ACT3_JUNGLE_N_S, + LVLPREST_ACT3_JUNGLE_N_W, + LVLPREST_ACT3_JUNGLE_NE_S, + LVLPREST_ACT3_JUNGLE_NE_W, + LVLPREST_ACT3_JUNGLE_NEW_S, + LVLPREST_ACT3_JUNGLE_NS_E, + LVLPREST_ACT3_JUNGLE_NS_W, + LVLPREST_ACT3_JUNGLE_NSE_W, + LVLPREST_ACT3_JUNGLE_NSW_E, + LVLPREST_ACT3_JUNGLE_NW_E, + LVLPREST_ACT3_JUNGLE_NW_S, + LVLPREST_ACT3_JUNGLE_S_E, + LVLPREST_ACT3_JUNGLE_S_N, + LVLPREST_ACT3_JUNGLE_S_W, + LVLPREST_ACT3_JUNGLE_SE_N, + LVLPREST_ACT3_JUNGLE_SE_W, + LVLPREST_ACT3_JUNGLE_SEW_N, + LVLPREST_ACT3_JUNGLE_SW_E, + LVLPREST_ACT3_JUNGLE_SW_N, + LVLPREST_ACT3_JUNGLE_TAIL, + LVLPREST_ACT3_JUNGLE_W_E, + LVLPREST_ACT3_JUNGLE_W_N, + LVLPREST_ACT3_JUNGLE_W_S, + LVLPREST_ACT3_TOWN, + LVLPREST_NONE, +} from './drlg-ids.ts' +import { + DRLG_CheckNotOverlappingUsingManhattanDistance, + DRLG_CheckOverlappingWithOrthogonalMargin, + DRLGROOM_AddOrth, + DRLGROOM_GetVisArrayFromLevelId, +} from './drlg-room.ts' +import { DATATBLS_GetLevelDefRecord } from './drlg-tables.ts' +import { + ACT_I, + ACT_II, + ACT_III, + ACT_IV, + ACT_V, + type D2DrlgCoordStrc, + type D2DrlgLevelStrc, + type D2DrlgStrc, + DIRECTION_COUNT, + DIRECTION_NORTHEAST, + DIRECTION_NORTHWEST, + DIRECTION_SOUTHEAST, + DIRECTION_SOUTHWEST, + type DrlgEnv, + DRLGTYPE_OUTDOOR, + DRLGTYPE_PRESET, + idiv, + LEVEL_ARREATPLATEAU, + LEVEL_BLACKMARSH, + LEVEL_BLOODMOOR, + LEVEL_BLOODYFOOTHILLS, + LEVEL_BURIALGROUNDS, + LEVEL_CANYONOFTHEMAGI, + LEVEL_CHAOSSANCTUM, + LEVEL_CITYOFTHEDAMNED, + LEVEL_COLDPLAINS, + LEVEL_DARKWOOD, + LEVEL_DRYHILLS, + LEVEL_FAROASIS, + LEVEL_HARROGATH, + LEVEL_ID_ACT5_BARRICADE_1, + LEVEL_KURASTDOCKTOWN, + LEVEL_LOSTCITY, + LEVEL_LOWERKURAST, + LEVEL_LUTGHOLEIN, + LEVEL_MONASTERYGATE, + LEVEL_MOOMOOFARM, + LEVEL_OUTERCLOISTER, + LEVEL_OUTERSTEPPES, + LEVEL_PANDEMONIUMFINALE, + LEVEL_PANDEMONIUMRUN2, + LEVEL_PLAINSOFDESPAIR, + LEVEL_ROCKYWASTE, + LEVEL_ROGUEENCAMPMENT, + LEVEL_SPIDERFOREST, + LEVEL_STONYFIELD, + LEVEL_TAMOEHIGHLAND, + LEVEL_THEPANDEMONIUMFORTRESS, + LEVEL_TRAVINCAL, + LEVEL_TUNDRAWASTELANDS, + LEVEL_VALLEYOFSNAKES, + newCoord, +} from './drlg-types.ts' import { DRLGWARP_GetWarpIdArrayFromLevelId } from './drlg-warp.ts' /** D2DrlgLevelLinkDataStrc. `nRand` is the C union nRand[4][15] / nRand2[60]: nRand[k][i] = nRand[k * 15 + i]. */ @@ -64,6 +141,8 @@ interface D2DrlgLinkStrc { const R = (k: number, i: number): number => k * 15 + i +let dword_6FDEA6FC = 0 + /** Pads a link table to its 15 static entries; C zero-initialises the unlisted ones. */ function linkTable(entries: D2DrlgLinkStrc[]): readonly D2DrlgLinkStrc[] { const table = entries.slice() @@ -137,6 +216,68 @@ function sub_6FD81430(c1: D2DrlgCoordStrc, c2: D2DrlgCoordStrc, a3: number, a4: } } +//D2Common.0x6FD81530 +function sub_6FD81530(d: D2DrlgLevelLinkDataStrc): boolean { + const it = d.nIteration + if (d.nRand[R(1, it)] === -1) { + d.nRand[R(1, it)] = SEED_RollRandomNumber(d.pSeed) & 7 + d.nRand[R(0, it)] = d.nRand[R(1, it)]! + } else { + if ((d.nRand[R(0, it)]! + 1) % 8 === d.nRand[R(1, it)]) return false + d.nRand[R(0, it)] = (d.nRand[R(0, it)]! + 1) % 8 + } + sub_6FD815E0(d.pLevelCoord[d.pLink[it]!.nLevelLink]!, d.pLevelCoord[it]!, d.nRand[R(0, it)]!, 1) + return true +} + +//D2Common.0x6FD815E0 +function sub_6FD815E0(c1: D2DrlgCoordStrc, c2: D2DrlgCoordStrc, a3: number, a4: number): void { + switch (a3) { + case 0: + c2.nPosX = c1.nPosX + c2.nPosY = c1.nPosY + c1.nHeight + if (a4 === 1) c2.nPosX -= idiv(c2.nWidth, 2) + 8 + break + case 1: + c2.nPosX = c1.nPosX + c2.nPosY = c1.nPosY + c1.nHeight + if (a4 === 1) c2.nPosX += idiv(c2.nWidth, 2) + 8 + break + case 2: + c2.nPosX = c1.nPosX - c2.nWidth + c2.nPosY = c1.nPosY + if (a4 === 1) c2.nPosY -= idiv(c2.nHeight, 2) + 8 + break + case 3: + c2.nPosX = c1.nPosX - c2.nWidth + c2.nPosY = c1.nPosY + if (a4 === 1) c2.nPosY += idiv(c2.nHeight, 2) + 8 + break + case 4: + c2.nPosX = c1.nPosX + c2.nPosY = c1.nPosY - c2.nHeight + if (a4 === 1) c2.nPosX -= idiv(c2.nWidth, 2) + 8 + break + case 5: + c2.nPosX = c1.nPosX + c2.nPosY = c1.nPosY - c2.nHeight + if (a4 === 1) c2.nPosX += idiv(c2.nWidth, 2) + 8 + break + case 6: + c2.nPosX = c1.nPosX + c1.nWidth + c2.nPosY = c1.nPosY + if (a4 === 1) c2.nPosY -= idiv(c2.nHeight, 2) + 8 + break + case 7: + c2.nPosX = c1.nPosX + c1.nWidth + c2.nPosY = c1.nPosY + if (a4 === 1) c2.nPosY += idiv(c2.nHeight, 2) + 8 + break + default: + return + } +} + //D2Common.0x6FD81720 function sub_6FD81720(d: D2DrlgLevelLinkDataStrc): boolean { const it = d.nIteration @@ -240,6 +381,143 @@ function sub_6FD81AD0(d: D2DrlgLevelLinkDataStrc): boolean { return true } +//D2Common.0x6FD81B30 +function sub_6FD81B30(d: D2DrlgLevelLinkDataStrc): boolean { + const it = d.nIteration + if (d.nRand[R(1, it)] === -1) { + d.nRand[R(1, it)] = (SEED_RollRandomNumber(d.pSeed) & 1) + 1 + d.nRand[R(0, it)] = d.nRand[R(1, it)]! + } else { + const nRand = 2 - (d.nRand[R(0, it)] !== 1 ? 1 : 0) + if (nRand === d.nRand[R(1, it)]) return false + d.nRand[R(0, it)] = nRand + } + + if (d.nRand[R(0, it)] === 1) { + sub_6FD81430(d.pLevelCoord[d.pLink[it]!.nLevelLink]!, d.pLevelCoord[it]!, 1, 0) + } else { + sub_6FD81850(d.pLevelCoord[d.pLink[it]!.nLevelLink]!, d.pLevelCoord[it]!, d.nRand[R(0, it)]!, 0) + } + return true +} + +//D2Common.0x6FD81BF0 +function sub_6FD81BF0(d: D2DrlgLevelLinkDataStrc): boolean { + const it = d.nIteration + if (d.nRand[R(1, it)] === -1) { + d.nRand[R(1, it)] = SEED_RollRandomNumber(d.pSeed) & 7 + d.nRand[R(0, it)] = d.nRand[R(1, it)]! + } else { + const nRand = (d.nRand[R(0, it)]! + 1) % 8 + if (nRand === d.nRand[R(1, it)]) return false + d.nRand[R(0, it)] = nRand + } + + sub_6FD815E0(d.pLevelCoord[d.pLink[it]!.nLevelLink]!, d.pLevelCoord[it]!, d.nRand[R(0, it)]!, 0) + return true +} + +//D2Common.0x6FD81CA0 +function sub_6FD81CA0(d: D2DrlgLevelLinkDataStrc): boolean { + const it = d.nIteration + d.nRand[R(1, it)] = 3 + d.nRand[R(0, it)] = d.nRand[R(1, it)]! + + if (!(SEED_RollRandomNumber(d.pSeed) & 1)) { + sub_6FD81850(d.pLevelCoord[d.pLink[it]!.nLevelLink]!, d.pLevelCoord[it]!, d.nRand[R(0, it)]!, 3) + dword_6FDEA6FC = 0x400000 + } else { + sub_6FD81430(d.pLevelCoord[d.pLink[it]!.nLevelLink]!, d.pLevelCoord[it]!, d.nRand[R(0, it)]!, 3) + dword_6FDEA6FC = 0x800000 + } + return true +} + +//D2Common.0x6FD83EC0 (DrlgOutRoom.cpp) +function DRLGOUTROOM_LinkLevelsByLevelCoords(d: D2DrlgLevelLinkDataStrc): boolean { + const it = d.nIteration + const nRand = SEED_RollRandomNumber(d.pSeed) & 1 + + d.nRand[R(0, it)] = nRand + d.nRand[R(1, it)] = nRand + + if (nRand === 0) { + d.pLevelCoord[it]!.nWidth = 64 + d.pLevelCoord[it]!.nHeight = 160 + } else if (nRand === 1) { + d.pLevelCoord[it]!.nWidth = 160 + d.pLevelCoord[it]!.nHeight = 64 + } + + const pLinkCoord = d.pLevelCoord[d.pLink[it]!.nLevelLink]! + d.pLevelCoord[it]!.nPosX = pLinkCoord.nPosX - d.pLevelCoord[it]!.nWidth + d.pLevelCoord[it]!.nPosY = pLinkCoord.nHeight - d.pLevelCoord[it]!.nHeight + pLinkCoord.nPosY - 16 + + return true +} + +//D2Common.0x6FD83F70 (DrlgOutRoom.cpp) +function DRLGOUTROOM_LinkLevelsByLevelDef(d: D2DrlgLevelLinkDataStrc): boolean { + const it = d.nIteration + const nRand = SEED_RollRandomNumber(d.pSeed) & 1 + + d.nRand[R(0, it)] = nRand + d.nRand[R(1, it)] = nRand + + if (nRand === 0) { + d.pLevelCoord[it]!.nWidth = 64 + d.pLevelCoord[it]!.nHeight = 160 + } else if (nRand === 1) { + d.pLevelCoord[it]!.nWidth = 160 + d.pLevelCoord[it]!.nHeight = 64 + } + + const pLevelDefBin = DATATBLS_GetLevelDefRecord(d.env.tables, d.nCurrentLevel) + d.pLevelCoord[it]!.nPosX = pLevelDefBin.dwOffsetX + d.pLevelCoord[it]!.nPosY = pLevelDefBin.dwOffsetY + + return true +} + +const pOffsetCoords_6FD84010: readonly (readonly [number, number])[] = [ + [0, -160], + [-96, -64], + [-64, -96], + [-160, 0], +] + +//D2Common.0x6FD84010 (DrlgOutRoom.cpp) +function DRLGOUTROOM_LinkLevelsByOffsetCoords(d: D2DrlgLevelLinkDataStrc): boolean { + const it = d.nIteration + + if (d.nRand[R(1, it)] === -1) { + d.nRand[R(1, it)] = SEED_RollRandomNumber(d.pSeed) & 1 + d.nRand[R(0, it)] = d.nRand[R(1, it)]! + } else { + const bRand0IsNull = d.nRand[R(0, it)] === 0 ? 1 : 0 + if (bRand0IsNull === d.nRand[R(1, it)]) { + return false + } + d.nRand[R(0, it)] = bRand0IsNull + } + + if (d.nRand[R(0, it)] === 0) { + d.pLevelCoord[it]!.nWidth = 64 + d.pLevelCoord[it]!.nHeight = 160 + } else if (d.nRand[R(0, it)] === 1) { + d.pLevelCoord[it]!.nWidth = 160 + d.pLevelCoord[it]!.nHeight = 64 + } + + const nIndex = d.nRand[R(0, it)]! + 2 * d.nRand[R(0, d.pLink[it]!.nLevelLink)]! + const pLinkCoord = d.pLevelCoord[d.pLink[it]!.nLevelLink]! + + d.pLevelCoord[it]!.nPosX = pLinkCoord.nPosX + pOffsetCoords_6FD84010[nIndex]![0] + d.pLevelCoord[it]!.nPosY = pLinkCoord.nPosY + pOffsetCoords_6FD84010[nIndex]![1] + + return true +} + //D2Common.0x6FDCFE40 const gAct1WildernessDrlgLink = linkTable([ { pfLinker: sub_6FD81330, nLevel: LEVEL_STONYFIELD, nLevelLink: -1, nLevelLinkEx: -1 }, @@ -260,6 +538,60 @@ const gAct1MonasteryDrlgLink = linkTable([ { pfLinker: null, nLevel: 0, nLevelLink: -1, nLevelLinkEx: -1 }, ]) +//D2Common.0x6FDD0020 +const gAct2OutdoorDrlgLink = linkTable([ + { pfLinker: sub_6FD81330, nLevel: LEVEL_LUTGHOLEIN, nLevelLink: -1, nLevelLinkEx: -1 }, + { pfLinker: sub_6FD81B30, nLevel: LEVEL_ROCKYWASTE, nLevelLink: 0, nLevelLinkEx: -1 }, + { pfLinker: sub_6FD81530, nLevel: LEVEL_DRYHILLS, nLevelLink: 1, nLevelLinkEx: -1 }, + { pfLinker: sub_6FD81530, nLevel: LEVEL_FAROASIS, nLevelLink: 2, nLevelLinkEx: -1 }, + { pfLinker: sub_6FD81530, nLevel: LEVEL_LOSTCITY, nLevelLink: 3, nLevelLinkEx: -1 }, + { pfLinker: sub_6FD81BF0, nLevel: LEVEL_VALLEYOFSNAKES, nLevelLink: 4, nLevelLinkEx: -1 }, + { pfLinker: null, nLevel: 0, nLevelLink: -1, nLevelLinkEx: -1 }, +]) + +//D2Common.0x6FDD0110 +const gAct2CanyonDrlgLink = linkTable([ + { pfLinker: sub_6FD81330, nLevel: LEVEL_CANYONOFTHEMAGI, nLevelLink: -1, nLevelLinkEx: -1 }, + { pfLinker: null, nLevel: 0, nLevelLink: -1, nLevelLinkEx: -1 }, +]) + +//1.11 : D2Common.0x6FDDC0B8 +const gAct5UbersDrlgLink = linkTable([ + { pfLinker: sub_6FD81330, nLevel: LEVEL_PANDEMONIUMRUN2, nLevelLink: -1, nLevelLinkEx: -1 }, + { pfLinker: sub_6FD81330, nLevel: LEVEL_PANDEMONIUMFINALE, nLevelLink: -1, nLevelLinkEx: -1 }, + { pfLinker: null, nLevel: 0, nLevelLink: -1, nLevelLinkEx: -1 }, +]) + +//D2Common.0x6FDD0200 +const gAct4OutdoorDrlgLink = linkTable([ + { pfLinker: sub_6FD81330, nLevel: LEVEL_THEPANDEMONIUMFORTRESS, nLevelLink: -1, nLevelLinkEx: -1 }, + { pfLinker: sub_6FD81CA0, nLevel: LEVEL_OUTERSTEPPES, nLevelLink: 0, nLevelLinkEx: -1 }, + { pfLinker: sub_6FD81380, nLevel: LEVEL_PLAINSOFDESPAIR, nLevelLink: 1, nLevelLinkEx: -1 }, + { pfLinker: sub_6FD81380, nLevel: LEVEL_CITYOFTHEDAMNED, nLevelLink: 2, nLevelLinkEx: -1 }, + { pfLinker: null, nLevel: 0, nLevelLink: -1, nLevelLinkEx: -1 }, +]) + +//D2Common.0x6FDD02F0 +const gAct4ChaosSanctumDrlgLink = linkTable([ + { pfLinker: sub_6FD81330, nLevel: LEVEL_CHAOSSANCTUM, nLevelLink: -1, nLevelLinkEx: -1 }, + { pfLinker: null, nLevel: 0, nLevelLink: -1, nLevelLinkEx: -1 }, +]) + +//1.10f: D2Common.0x6FDD03E0 +const gAct5OutdoorDrlgLink = linkTable([ + { pfLinker: sub_6FD81330, nLevel: LEVEL_HARROGATH, nLevelLink: -1, nLevelLinkEx: -1 }, + { pfLinker: sub_6FD81330, nLevel: LEVEL_BLOODYFOOTHILLS, nLevelLink: 0, nLevelLinkEx: -1 }, + { pfLinker: DRLGOUTROOM_LinkLevelsByLevelCoords, nLevel: LEVEL_ID_ACT5_BARRICADE_1, nLevelLink: 1, nLevelLinkEx: -1 }, + { pfLinker: DRLGOUTROOM_LinkLevelsByOffsetCoords, nLevel: LEVEL_ARREATPLATEAU, nLevelLink: 2, nLevelLinkEx: -1 }, + { pfLinker: null, nLevel: 0, nLevelLink: -1, nLevelLinkEx: -1 }, +]) + +//1.10f: D2Common.0x6FDD04D0 +const gAct5TundraDrlgLink = linkTable([ + { pfLinker: DRLGOUTROOM_LinkLevelsByLevelDef, nLevel: LEVEL_TUNDRAWASTELANDS, nLevelLink: -1, nLevelLinkEx: -1 }, + { pfLinker: null, nLevel: 0, nLevelLink: -1, nLevelLinkEx: -1 }, +]) + //D2Common.0x6FD81D60 (1.10) export function DRLGOUTPLACE_CreateLevelConnections(pDrlg: D2DrlgStrc, nAct: number): void { switch (nAct) { @@ -268,8 +600,70 @@ export function DRLGOUTPLACE_CreateLevelConnections(pDrlg: D2DrlgStrc, nAct: num sub_6FD823C0(pDrlg, gAct1MonasteryDrlgLink, sub_6FD82130, sub_6FD82360) sub_6FD82750(pDrlg, LEVEL_ROGUEENCAMPMENT, LEVEL_BURIALGROUNDS) break + + case ACT_II: + sub_6FD823C0(pDrlg, gAct2OutdoorDrlgLink, DRLGOUTPLACE_LinkAct2Outdoors, null) + sub_6FD823C0(pDrlg, gAct2CanyonDrlgLink, DRLGOUTPLACE_LinkAct2Canyon, null) + sub_6FD82750(pDrlg, LEVEL_LUTGHOLEIN, LEVEL_CANYONOFTHEMAGI) + break + + case ACT_III: { + let pLevel = DRLG_GetLevel(pDrlg, LEVEL_KURASTDOCKTOWN) + const pTownLevelDef = DATATBLS_GetLevelDefRecord(pDrlg.env.tables, LEVEL_KURASTDOCKTOWN) + + pLevel.pLevelCoords.nPosX = pTownLevelDef.dwOffsetX + pLevel.pLevelCoords.nPosY = pTownLevelDef.dwOffsetY + pLevel.pLevelCoords.nWidth = pTownLevelDef.dwSizeX[pDrlg.nDifficulty]! + pLevel.pLevelCoords.nHeight = pTownLevelDef.dwSizeY[pDrlg.nDifficulty]! + + pLevel = DRLG_GenerateJungles(pLevel) + + let nPosY = 0 + for (let i = LEVEL_LOWERKURAST; i <= LEVEL_TRAVINCAL; i += 1) { + const pLevelDefBinRecord = DATATBLS_GetLevelDefRecord(pDrlg.env.tables, i) + const nSizeX = pLevelDefBinRecord.dwSizeX[pLevel.pDrlg.nDifficulty]! + const nSizeY = pLevelDefBinRecord.dwSizeY[pLevel.pDrlg.nDifficulty]! + + nPosY -= nSizeY + + const pAdjacentLevel = DRLG_GetLevel(pLevel.pDrlg, i) + pAdjacentLevel.pLevelCoords.nPosX = + idiv(pLevel.pLevelCoords.nWidth, 2) + pLevel.pLevelCoords.nPosX - idiv(nSizeX, 2) + pAdjacentLevel.pLevelCoords.nWidth = nSizeX + pAdjacentLevel.pLevelCoords.nPosY = nPosY + pLevel.pLevelCoords.nPosY + pAdjacentLevel.pLevelCoords.nHeight = nSizeY + } + + sub_6FD826D0(pDrlg, LEVEL_KURASTDOCKTOWN, LEVEL_TRAVINCAL) + sub_6FD82750(pDrlg, LEVEL_KURASTDOCKTOWN, LEVEL_TRAVINCAL) + break + } + + case ACT_IV: { + sub_6FD823C0(pDrlg, gAct4OutdoorDrlgLink, DRLGOUTPLACE_LinkAct4Outdoors, null) + sub_6FD823C0(pDrlg, gAct4ChaosSanctumDrlgLink, DRLGOUTPLACE_LinkAct4ChaosSanctum, null) + + const pLevel = DRLG_GetLevel(pDrlg, LEVEL_OUTERSTEPPES) + pLevel.pOutdoors!.dwFlags = (pLevel.pOutdoors!.dwFlags | dword_6FDEA6FC) >>> 0 + + sub_6FD82750(pDrlg, LEVEL_THEPANDEMONIUMFORTRESS, LEVEL_CITYOFTHEDAMNED) + break + } + + case ACT_V: + sub_6FD823C0(pDrlg, gAct5OutdoorDrlgLink, null, null) + sub_6FD823C0(pDrlg, gAct5TundraDrlgLink, null, null) + + sub_6FD826D0(pDrlg, LEVEL_ID_ACT5_BARRICADE_1, LEVEL_ARREATPLATEAU) + sub_6FD82750(pDrlg, LEVEL_ID_ACT5_BARRICADE_1, LEVEL_ARREATPLATEAU) + + sub_6FD826D0(pDrlg, LEVEL_BLOODYFOOTHILLS, LEVEL_ID_ACT5_BARRICADE_1) + sub_6FD826D0(pDrlg, LEVEL_HARROGATH, LEVEL_BLOODYFOOTHILLS) + sub_6FD823C0(pDrlg, gAct5UbersDrlgLink, DRLGOUTPLACE_LinkAct5Ubers, null) + break + default: - throw new Error(`DRLGOUTPLACE_CreateLevelConnections: act ${nAct} is not ported`) + return } } @@ -327,6 +721,61 @@ function sub_6FD82130(d: D2DrlgLevelLinkDataStrc, nIteration: number): boolean { return bResult } +//D2Common.0x6FD821E0 +function DRLGOUTPLACE_LinkAct2Outdoors(d: D2DrlgLevelLinkDataStrc, nIteration: number): boolean { + const nLevelLink = gAct2OutdoorDrlgLink[nIteration]!.nLevelLink + for (let i = 0; i < nIteration; i += 1) { + if (i !== nLevelLink && !DRLG_CheckNotOverlappingUsingManhattanDistance(d.pLevelCoord[nIteration]!, d.pLevelCoord[i]!, 0)) { + return false + } + } + return true +} + +//D2Common.0x6FD82240 +function DRLGOUTPLACE_LinkAct2Canyon(d: D2DrlgLevelLinkDataStrc, nIteration: number): boolean { + const nLevelLink = gAct2CanyonDrlgLink[nIteration]!.nLevelLink + for (let i = 0; i < nIteration; i += 1) { + if (i !== nLevelLink && !DRLG_CheckNotOverlappingUsingManhattanDistance(d.pLevelCoord[nIteration]!, d.pLevelCoord[i]!, 0)) { + return false + } + } + return true +} + +//1.11 : D2Common.0x6FD6C560 +function DRLGOUTPLACE_LinkAct5Ubers(d: D2DrlgLevelLinkDataStrc, nIteration: number): boolean { + const nLevelLink = gAct5UbersDrlgLink[nIteration]!.nLevelLink + for (let i = 0; i < nIteration; i += 1) { + if (i !== nLevelLink && !DRLG_CheckNotOverlappingUsingManhattanDistance(d.pLevelCoord[nIteration]!, d.pLevelCoord[i]!, 0)) { + return false + } + } + return true +} + +//D2Common.0x6FD822A0 +function DRLGOUTPLACE_LinkAct4Outdoors(d: D2DrlgLevelLinkDataStrc, nIteration: number): boolean { + const nLevelLink = gAct4OutdoorDrlgLink[nIteration]!.nLevelLink + for (let i = 0; i < nIteration; i += 1) { + if (i !== nLevelLink && !DRLG_CheckNotOverlappingUsingManhattanDistance(d.pLevelCoord[nIteration]!, d.pLevelCoord[i]!, 0)) { + return false + } + } + return true +} + +//D2Common.0x6FD82300 +function DRLGOUTPLACE_LinkAct4ChaosSanctum(d: D2DrlgLevelLinkDataStrc, nIteration: number): boolean { + const nLevelLink = gAct4ChaosSanctumDrlgLink[nIteration]!.nLevelLink + for (let i = 0; i < nIteration; i += 1) { + if (i !== nLevelLink && !DRLG_CheckNotOverlappingUsingManhattanDistance(d.pLevelCoord[nIteration]!, d.pLevelCoord[i]!, 0)) { + return false + } + } + return true +} + //D2Common.0x6FD82360 const stru_6FDD06C0: readonly (readonly [number, number, number, number, number, number])[] = [ // nLevelId, nExcludedLevel1, nExcludedLevel2, nRand, nNextRand, nFlags @@ -454,6 +903,23 @@ function sub_6FD823C0( } } +//D2Common.0x6FD826D0 +function sub_6FD826D0(pDrlg: D2DrlgStrc, nStartId: number, nEndId: number): void { + for (let i = nStartId; i <= nEndId; i += 1) { + const pLevel = DRLG_GetLevel(pDrlg, i) + const pDrlgWarp = DRLG_GetDrlgWarpFromLevelId(pDrlg, i) + + for (let j = nStartId; j <= nEndId; j += 1) { + if ( + i !== j && + DRLG_CheckOverlappingWithOrthogonalMargin(pLevel.pLevelCoords, DRLG_GetLevel(pDrlg, j).pLevelCoords, -1) + ) { + DRLG_SetWarpId(pDrlgWarp, j, -1, -1) + } + } + } +} + //D2Common.0x6FD82750 function sub_6FD82750(pDrlg: D2DrlgStrc, nStartId: number, nEndId: number): void { for (let i = nStartId; i <= nEndId; i += 1) { @@ -475,3 +941,632 @@ function sub_6FD82750(pDrlg: D2DrlgStrc, nStartId: number, nEndId: number): void } } } + +// --------------------------------------------------------------------------------------------- +// Act III Jungle layout (DrlgOutPlace.cpp:1926-2616) +// --------------------------------------------------------------------------------------------- + +const JUNGLE_MAX_ATTACH = 3 +const JUNGLE_PRESET2_ATTACH_POINT = 2 + +const JUNGLE_FLAG_LEFT_ = 0b0001 +const JUNGLE_FLAG_RIGHT_ = 0b0010 +const JUNGLE_FLAG_BOTTOM_ = 0b0100 +const JUNGLE_FLAG_TOP_ = 0b1000 + +interface D2JungleStrc { + pDrlgCoord: D2DrlgCoordStrc + field_10: number + nBranch: number + pBasedOnJungle: D2JungleStrc | null + pJungleBranches: (D2JungleStrc | null)[] + nPresetsBlocksX: number + nPresetsBlocksY: number + pJungleDefs: Int32Array | null + nJungleDefs: number +} + +function newJungleStrc(): D2JungleStrc { + return { + pDrlgCoord: newCoord(), + field_10: 0, + nBranch: 0, + pBasedOnJungle: null, + pJungleBranches: [null, null, null], + nPresetsBlocksX: 0, + nPresetsBlocksY: 0, + pJungleDefs: null, + nJungleDefs: 0, + } +} + +//D2Common.0x6FDD0828 +const gJunglePresets: readonly number[] = [ + LVLPREST_NONE, LVLPREST_ACT3_JUNGLE_W_E, LVLPREST_ACT3_JUNGLE_W_S, LVLPREST_ACT3_JUNGLE_W_N, + LVLPREST_ACT3_JUNGLE_E_W, LVLPREST_NONE, LVLPREST_ACT3_JUNGLE_E_S, LVLPREST_ACT3_JUNGLE_E_N, + LVLPREST_NONE, LVLPREST_NONE, LVLPREST_ACT3_JUNGLE_EW_S, LVLPREST_ACT3_JUNGLE_EW_N, + LVLPREST_ACT3_JUNGLE_S_W, LVLPREST_ACT3_JUNGLE_S_E, LVLPREST_NONE, LVLPREST_ACT3_JUNGLE_S_N, + LVLPREST_NONE, LVLPREST_ACT3_JUNGLE_SW_E, LVLPREST_NONE, LVLPREST_ACT3_JUNGLE_SW_N, + LVLPREST_ACT3_JUNGLE_SE_W, LVLPREST_NONE, LVLPREST_NONE, LVLPREST_ACT3_JUNGLE_SE_N, + LVLPREST_NONE, LVLPREST_NONE, LVLPREST_NONE, LVLPREST_ACT3_JUNGLE_SEW_N, + LVLPREST_ACT3_JUNGLE_N_W, LVLPREST_ACT3_JUNGLE_N_E, LVLPREST_ACT3_JUNGLE_N_S, LVLPREST_NONE, + LVLPREST_NONE, LVLPREST_ACT3_JUNGLE_NW_E, LVLPREST_ACT3_JUNGLE_NW_S, LVLPREST_NONE, + LVLPREST_ACT3_JUNGLE_NE_W, LVLPREST_NONE, LVLPREST_ACT3_JUNGLE_NE_S, LVLPREST_NONE, + LVLPREST_NONE, LVLPREST_NONE, LVLPREST_ACT3_JUNGLE_NEW_S, LVLPREST_NONE, + LVLPREST_ACT3_JUNGLE_NS_W, LVLPREST_ACT3_JUNGLE_NS_E, LVLPREST_NONE, LVLPREST_NONE, + LVLPREST_NONE, LVLPREST_ACT3_JUNGLE_NSW_E, LVLPREST_NONE, LVLPREST_NONE, + LVLPREST_ACT3_JUNGLE_NSE_W, LVLPREST_NONE, LVLPREST_NONE, LVLPREST_NONE, + LVLPREST_NONE, LVLPREST_NONE, LVLPREST_NONE, LVLPREST_NONE, +] + +//D2Common.0x6FDD0924 +const gSpiderForestPresets: readonly number[] = [ + LVLPREST_NONE, LVLPREST_ACT3_CLEARING_WEBBY_W, LVLPREST_ACT3_CLEARING_WEBBY_E, LVLPREST_ACT3_CLEARING_WEBBY_EW, + LVLPREST_ACT3_CLEARING_WEBBY_S, LVLPREST_ACT3_CLEARING_WEBBY_SW, LVLPREST_ACT3_CLEARING_WEBBY_SE, LVLPREST_NONE, + LVLPREST_ACT3_CLEARING_WEBBY_N, LVLPREST_ACT3_CLEARING_WEBBY_NW, LVLPREST_ACT3_CLEARING_WEBBY_NE, LVLPREST_NONE, + LVLPREST_ACT3_CLEARING_WEBBY_NS, LVLPREST_NONE, LVLPREST_NONE, LVLPREST_NONE, + LVLPREST_NONE, +] + +const nPresetBlocksDimensions = 32 + +function DRLG_GenerateJunglesAttachPoints( + pDrlg: D2DrlgStrc, + tJungles: D2JungleStrc[], + nMinX: number, + nMinY: number, + nSpiderForestLevelSizeX: number, + nSpiderForestLevelSizeY: number, + nPresetsWidth: number, + nPresetsHeight: number, + pPreset0: Int32Array, + pPreset1: Int32Array, + pPreset2: Int32Array, + pLevelPresetId: Int32Array, +): void { + pPreset0.fill(0, 0, nPresetsWidth * nPresetsHeight) + pPreset1.fill(0, 0, nPresetsWidth * nPresetsHeight) + pPreset2.fill(0, 0, nPresetsWidth * nPresetsHeight) + pLevelPresetId.fill(0, 0, nPresetsWidth * nPresetsHeight) + + const nSpiderForestLevelPresetsBlocksSizeX = idiv(nSpiderForestLevelSizeX, nPresetBlocksDimensions) + const nSpiderForestLevelPresetsBlocksSizeY = idiv(nSpiderForestLevelSizeY, nPresetBlocksDimensions) + + for (let nJungleIdx = 0; nJungleIdx < JUNGLE_MAX_ATTACH; nJungleIdx += 1) { + const tCurrentJungle = tJungles[nJungleIdx]! + const nJungleOffsetX = tCurrentJungle.pDrlgCoord.nPosX - nMinX + const nJungleOffsetY = tCurrentJungle.pDrlgCoord.nPosY - nMinY + + tCurrentJungle.nPresetsBlocksX = (((nJungleOffsetX & 0x1f) + nJungleOffsetX) >> 5) + 1 + tCurrentJungle.nPresetsBlocksY = (((nJungleOffsetY & 0x1f) + nJungleOffsetY) >> 5) + 1 + + tCurrentJungle.pJungleDefs = new Int32Array( + nSpiderForestLevelPresetsBlocksSizeX * nSpiderForestLevelPresetsBlocksSizeY, + ) + + let bLineStartWithOffset: boolean + if (nJungleIdx !== 0) { + bLineStartWithOffset = tCurrentJungle.field_10 % 2 !== 0 + } else { + bLineStartWithOffset = SEED_RollLimitedRandomNumber(pDrlg.pSeed, 2) !== 0 + } + + let nJungleAttachPoints = 0 + while (nJungleAttachPoints < 2) { + let nPresetBlockRowOffset = tCurrentJungle.nPresetsBlocksX + nPresetsWidth * tCurrentJungle.nPresetsBlocksY + for (let nPresetBlockY = 0; nPresetBlockY < nSpiderForestLevelPresetsBlocksSizeY; nPresetBlockY += 1) { + for (let nPresetBlockX = 0; nPresetBlockX < nSpiderForestLevelPresetsBlocksSizeX; nPresetBlockX += 1) { + const nPresetBlockIndex = nPresetBlockRowOffset + nPresetBlockX + pPreset0[nPresetBlockIndex] = nJungleIdx + 1 + pPreset1[nPresetBlockIndex] = 0 + pPreset2[nPresetBlockIndex] = 0 + pLevelPresetId[nPresetBlockIndex] = 0 + } + nPresetBlockRowOffset += nPresetsWidth + } + + const nFirstPresetsBlockY = tCurrentJungle.nPresetsBlocksY + const nLastPresetsBlockY = nFirstPresetsBlockY + nSpiderForestLevelPresetsBlocksSizeY - 1 + + if (nJungleIdx !== 0) { + const nFirstPresetsBlockIndex = nLastPresetsBlockY * nPresetsWidth + tCurrentJungle.nPresetsBlocksX + pPreset2[nFirstPresetsBlockIndex + (bLineStartWithOffset ? 1 : 0)] = 1 + pPreset2[nFirstPresetsBlockIndex + (!bLineStartWithOffset ? 1 : 0)] = JUNGLE_PRESET2_ATTACH_POINT + } + + for (let nBranchIdx = 0; nBranchIdx < tCurrentJungle.nBranch; nBranchIdx += 1) { + const pJungleBranch = tCurrentJungle.pJungleBranches[nBranchIdx]! + let nPresetOffsetX = 0 + let nPresetOffsetY = 0 + switch (pJungleBranch.field_10) { + case 0: + break + case 1: + nPresetOffsetY = 3 + break + case 2: + nPresetOffsetX = 1 + nPresetOffsetY = 3 + break + case 3: + nPresetOffsetY = 1 + break + case 4: + nPresetOffsetX = 1 + nPresetOffsetY = 1 + break + default: + throw new Error(`DRLG_GenerateJunglesAttachPoints: unreachable field_10=${pJungleBranch.field_10}`) + } + pPreset2[ + tCurrentJungle.nPresetsBlocksX + + nPresetOffsetX + + nPresetsWidth * (tCurrentJungle.nPresetsBlocksY + nPresetOffsetY) + ] = 1 + } + + nJungleAttachPoints = nJungleIdx !== 0 ? 1 : 0 + + const nCurrentPresetsBlockX = tCurrentJungle.nPresetsBlocksX + let i = 20 * (5 * nJungleIdx + 5) + let nColumnSize = 0 + for (let nCurrentPresetsBlockY = nLastPresetsBlockY; nCurrentPresetsBlockY >= nFirstPresetsBlockY; ) { + const nCurrentPresetsBlockRowOffset = nCurrentPresetsBlockY * nPresetsWidth + + pPreset1[nCurrentPresetsBlockRowOffset + nCurrentPresetsBlockX + (bLineStartWithOffset ? 1 : 0)] = i + i += 1 + + if ( + nColumnSize === 0 || + nCurrentPresetsBlockY === nFirstPresetsBlockY || + SEED_RollLimitedRandomNumber(pDrlg.pSeed, 3) !== 0 + ) { + nColumnSize += 1 + + if (nColumnSize >= 2 && nCurrentPresetsBlockY > 1) { + const nNextPresetIdx = + nCurrentPresetsBlockRowOffset + nCurrentPresetsBlockX + (!bLineStartWithOffset ? 1 : 0) + + if (pPreset2[nNextPresetIdx] === 0) { + pPreset2[nNextPresetIdx] = JUNGLE_PRESET2_ATTACH_POINT + nJungleAttachPoints += 1 + } + } + nCurrentPresetsBlockY -= 1 + } else { + nColumnSize = 0 + bLineStartWithOffset = !bLineStartWithOffset + } + } + } + + while (nJungleAttachPoints > 3) { + let bFinished = false + let nAttachPointsFound = 0 + const nAttachPointToRemove = SEED_RollLimitedRandomNumber(pDrlg.pSeed, nJungleAttachPoints) >>> 0 + + for ( + let nCurrentPresetsBlockY = 0; + nCurrentPresetsBlockY < nSpiderForestLevelPresetsBlocksSizeY && !bFinished; + nCurrentPresetsBlockY += 1 + ) { + const nCurrentPresetsBlockOffset = + tCurrentJungle.nPresetsBlocksX + + (tCurrentJungle.nPresetsBlocksY + nCurrentPresetsBlockY) * nPresetsWidth + + for ( + let nCurrentPresetsBlockX = 0; + nCurrentPresetsBlockX < nSpiderForestLevelPresetsBlocksSizeX && !bFinished; + nCurrentPresetsBlockX += 1 + ) { + const nCurrentPresetBlockIdx = nCurrentPresetsBlockOffset + nCurrentPresetsBlockX + if (pPreset2[nCurrentPresetBlockIdx] === JUNGLE_PRESET2_ATTACH_POINT) { + if (nAttachPointsFound === nAttachPointToRemove) { + pPreset2[nCurrentPresetBlockIdx] = 0 + bFinished = true + nJungleAttachPoints -= 1 + } + nAttachPointsFound += 1 + } + } + } + } + } +} + +function DRLG_JungleComputeConnexity( + _pDrlg: D2DrlgStrc, + nPresetsWidth: number, + nPresetsHeight: number, + pPreset0: Int32Array, + pPreset1: Int32Array, + pPreset2: Int32Array, + pLevelPresetId: Int32Array, +): void { + const nMaxValue = 0x7fffffff + + for (let nCurrentPresetY = 0; nCurrentPresetY < nPresetsHeight; nCurrentPresetY += 1) { + for (let nCurrentPresetX = 0; nCurrentPresetX < nPresetsWidth; nCurrentPresetX += 1) { + let nFlags = 0 + let nLastDirPreset1Value = nMaxValue + + const nCurrentPresetIndex = nCurrentPresetY * nPresetsWidth + nCurrentPresetX + const nRightPresetIndex = nCurrentPresetY * nPresetsWidth + nCurrentPresetX + 1 + const nLeftPresetIndex = nCurrentPresetY * nPresetsWidth + nCurrentPresetX - 1 + const nTopPresetIndex = (nCurrentPresetY - 1) * nPresetsWidth + nCurrentPresetX + const nBottomPresetIndex = (nCurrentPresetY + 1) * nPresetsWidth + nCurrentPresetX + + if (pPreset2[nCurrentPresetIndex] === 1) { + const nPreset0CurrentValue = pPreset0[nCurrentPresetIndex]! + + const UpdateAccordingToPreset1 = (nPresetIndex: number, nDirectionFlag: number): void => { + const nPreset1Value = pPreset1[nPresetIndex]! + if ( + nPreset1Value !== 0 && + nPreset1Value < nLastDirPreset1Value && + pPreset0[nPresetIndex] === nPreset0CurrentValue + ) { + nFlags = nDirectionFlag + nLastDirPreset1Value = nPreset1Value + } + } + + UpdateAccordingToPreset1(nTopPresetIndex, JUNGLE_FLAG_TOP_) + UpdateAccordingToPreset1(nBottomPresetIndex, JUNGLE_FLAG_BOTTOM_) + UpdateAccordingToPreset1(nRightPresetIndex, JUNGLE_FLAG_RIGHT_) + UpdateAccordingToPreset1(nLeftPresetIndex, JUNGLE_FLAG_LEFT_) + + if (nFlags & JUNGLE_FLAG_TOP_) pLevelPresetId[nTopPresetIndex]! |= JUNGLE_FLAG_BOTTOM_ + if (nFlags & JUNGLE_FLAG_BOTTOM_) pLevelPresetId[nBottomPresetIndex]! |= JUNGLE_FLAG_TOP_ + if (nFlags & JUNGLE_FLAG_RIGHT_) pLevelPresetId[nRightPresetIndex]! |= JUNGLE_FLAG_LEFT_ + if (nFlags & JUNGLE_FLAG_LEFT_) pLevelPresetId[nLeftPresetIndex]! |= JUNGLE_FLAG_RIGHT_ + + if (pPreset2[nTopPresetIndex] === 1 && pPreset0[nTopPresetIndex] !== nPreset0CurrentValue) { + nFlags |= JUNGLE_FLAG_TOP_ + } + if (pPreset2[nBottomPresetIndex] === 1 && pPreset0[nBottomPresetIndex] !== nPreset0CurrentValue) { + nFlags |= JUNGLE_FLAG_BOTTOM_ + } + if (pPreset2[nRightPresetIndex] === 1 && pPreset0[nRightPresetIndex] !== nPreset0CurrentValue) { + nFlags |= JUNGLE_FLAG_RIGHT_ + } + if (pPreset2[nLeftPresetIndex] === 1 && pPreset0[nLeftPresetIndex] !== nPreset0CurrentValue) { + nFlags |= JUNGLE_FLAG_LEFT_ + } + } + + const nPreset1CurrentValue = pPreset1[nCurrentPresetIndex]! + if (nPreset1CurrentValue) { + if (Math.abs(pPreset1[nTopPresetIndex]! - nPreset1CurrentValue) === 1) { + nFlags |= JUNGLE_FLAG_TOP_ + } + if (Math.abs(pPreset1[nBottomPresetIndex]! - nPreset1CurrentValue) === 1) { + nFlags |= JUNGLE_FLAG_BOTTOM_ + } + if (Math.abs(pPreset1[nRightPresetIndex]! - nPreset1CurrentValue) === 1) { + nFlags |= JUNGLE_FLAG_RIGHT_ + } + if (Math.abs(pPreset1[nLeftPresetIndex]! - nPreset1CurrentValue) === 1) { + nFlags |= JUNGLE_FLAG_LEFT_ + } + } + + pLevelPresetId[nCurrentPresetIndex]! |= nFlags + } + } +} + +function DRLG_JungleUpdateAttachPointsDirections( + pDrlg: D2DrlgStrc, + nPresetsWidth: number, + nPresetsHeight: number, + pPreset0: Int32Array, + pPreset2: Int32Array, + pLevelPresetId: Int32Array, +): boolean { + for (let nCurrentPresetY = 0; nCurrentPresetY < nPresetsHeight; nCurrentPresetY += 1) { + for (let nCurrentPresetX = 0; nCurrentPresetX < nPresetsWidth; nCurrentPresetX += 1) { + const nCurrentPresetIndex = nCurrentPresetY * nPresetsWidth + nCurrentPresetX + + let nDirectionBase = SEED_RollLimitedRandomNumber(pDrlg.pSeed, DIRECTION_COUNT) | 0 + + if (pPreset2[nCurrentPresetIndex] === JUNGLE_PRESET2_ATTACH_POINT) { + const nRightPresetIndex = nCurrentPresetY * nPresetsWidth + nCurrentPresetX + 1 + const nLeftPresetIndex = nCurrentPresetY * nPresetsWidth + nCurrentPresetX - 1 + const nTopPresetIndex = (nCurrentPresetY - 1) * nPresetsWidth + nCurrentPresetX + const nBottomPresetIndex = (nCurrentPresetY + 1) * nPresetsWidth + nCurrentPresetX + + const nPreset0CurrentValue = pPreset0[nCurrentPresetIndex]! + + let nCurrentPresetLevelId = pLevelPresetId[nCurrentPresetIndex]! + const bHadNoPresetLeveld = nCurrentPresetLevelId === 0 + + let bLookAtNextDirection = true + const LookForDirection = (CheckDirectionFunction: (nPresetIndex: number, nFlagToAdd: number) => void): void => { + for (let i = 0; i < DIRECTION_COUNT && bLookAtNextDirection; i += 1) { + switch ((nDirectionBase + i) % DIRECTION_COUNT) { + case DIRECTION_SOUTHWEST: + CheckDirectionFunction(nTopPresetIndex, JUNGLE_FLAG_TOP_) + break + case DIRECTION_NORTHWEST: + CheckDirectionFunction(nBottomPresetIndex, JUNGLE_FLAG_BOTTOM_) + break + case DIRECTION_SOUTHEAST: + CheckDirectionFunction(nRightPresetIndex, JUNGLE_FLAG_RIGHT_) + break + case DIRECTION_NORTHEAST: + CheckDirectionFunction(nLeftPresetIndex, JUNGLE_FLAG_LEFT_) + break + default: + break + } + } + } + + bLookAtNextDirection = true + LookForDirection((nPresetIndex, nFlagToAdd) => { + if ( + pPreset0[nPresetIndex] === nPreset0CurrentValue && + pLevelPresetId[nPresetIndex]! !== 0 && + pLevelPresetId[nPresetIndex]! < 15 + ) { + nCurrentPresetLevelId |= nFlagToAdd << 4 + bLookAtNextDirection = false + } + }) + + if (!nCurrentPresetLevelId) { + return false + } + + if (bHadNoPresetLeveld) { + bLookAtNextDirection = true + LookForDirection((nPresetIndex, nFlagToAdd) => { + if ( + pPreset0[nPresetIndex] !== nPreset0CurrentValue && + pPreset2[nPresetIndex] === JUNGLE_PRESET2_ATTACH_POINT + ) { + nCurrentPresetLevelId |= nFlagToAdd << 4 + bLookAtNextDirection = false + } + }) + + bLookAtNextDirection = SEED_RollLimitedRandomNumber(pDrlg.pSeed, 2) !== 0 && bLookAtNextDirection + + nDirectionBase = SEED_RollLimitedRandomNumber(pDrlg.pSeed, DIRECTION_COUNT) | 0 + + LookForDirection((nPresetIndex, nFlagToAdd) => { + if ( + pLevelPresetId[nPresetIndex]! !== 0 && + pLevelPresetId[nPresetIndex]! < 15 && + (nCurrentPresetLevelId & (nFlagToAdd << 4)) === 0 + ) { + nCurrentPresetLevelId |= nFlagToAdd << 4 + bLookAtNextDirection = false + } + }) + } + + if ((nCurrentPresetLevelId & (JUNGLE_FLAG_TOP_ << 4)) !== 0) { + pLevelPresetId[nTopPresetIndex]! |= JUNGLE_FLAG_BOTTOM_ << 4 + } + if ((nCurrentPresetLevelId & (JUNGLE_FLAG_BOTTOM_ << 4)) !== 0) { + pLevelPresetId[nBottomPresetIndex]! |= JUNGLE_FLAG_TOP_ << 4 + } + if ((nCurrentPresetLevelId & (JUNGLE_FLAG_RIGHT_ << 4)) !== 0) { + pLevelPresetId[nRightPresetIndex]! |= JUNGLE_FLAG_LEFT_ << 4 + } + if ((nCurrentPresetLevelId & (JUNGLE_FLAG_LEFT_ << 4)) !== 0) { + pLevelPresetId[nLeftPresetIndex]! |= JUNGLE_FLAG_RIGHT_ << 4 + } + + pPreset2[nCurrentPresetIndex] = 0 + pLevelPresetId[nCurrentPresetIndex]! |= nCurrentPresetLevelId + } + } + } + + return true +} + +function DRLG_JungleNormalizeLevelPresetId(nLevelPresetId: number): number { + const nbDirections = 16 + let nPDef = idiv(nLevelPresetId, 1) % nbDirections + if (nPDef !== 0) { + if (nLevelPresetId < nbDirections) { + nPDef += LVLPREST_ACT3_TOWN + } else { + const nPresetDefinitionOffset = 4 * (nPDef - 1) + if ((nLevelPresetId & (JUNGLE_FLAG_LEFT_ << 4)) !== 0) nPDef = gJunglePresets[nPresetDefinitionOffset]! + if ((nLevelPresetId & (JUNGLE_FLAG_RIGHT_ << 4)) !== 0) nPDef = gJunglePresets[nPresetDefinitionOffset + 1]! + if ((nLevelPresetId & (JUNGLE_FLAG_BOTTOM_ << 4)) !== 0) nPDef = gJunglePresets[nPresetDefinitionOffset + 2]! + if ((nLevelPresetId & (JUNGLE_FLAG_TOP_ << 4)) !== 0) nPDef = gJunglePresets[nPresetDefinitionOffset + 3]! + } + } else if (nLevelPresetId >= nbDirections) { + nPDef = gSpiderForestPresets[nLevelPresetId >> 4]! + } + return nPDef +} + +//D2Common.0x6FD82820 +export function DRLG_GenerateJungles(pLevel: D2DrlgLevelStrc): D2DrlgLevelStrc { + const pDrlg = pLevel.pDrlg + const pSpiderForestLevelDef = DATATBLS_GetLevelDefRecord(pDrlg.env.tables, LEVEL_SPIDERFOREST) + const nDifficulty = pDrlg.nDifficulty + const nSpiderForestLevelSizeX = pSpiderForestLevelDef.dwSizeX[nDifficulty]! + const nSpiderForestLevelSizeY = pSpiderForestLevelDef.dwSizeY[nDifficulty]! + let nMinX = pLevel.pLevelCoords.nPosX + let nMaxX = nSpiderForestLevelSizeX + nMinX + const nMaxY = pLevel.pLevelCoords.nPosY + let nMinY = nMaxY - nSpiderForestLevelSizeY + + const tJungles: D2JungleStrc[] = [newJungleStrc(), newJungleStrc(), newJungleStrc()] + sub_6FD83970(pLevel.pLevelCoords, tJungles[0]!, 0, nSpiderForestLevelSizeX, nSpiderForestLevelSizeY) + + for (let nJungleAttachIdx = 1; nJungleAttachIdx < JUNGLE_MAX_ATTACH; nJungleAttachIdx += 1) { + const nBaseOn = SEED_RollLimitedRandomNumber(pDrlg.pSeed, nJungleAttachIdx) >>> 0 + + const pCurrentJungle = tJungles[nJungleAttachIdx]! + sub_6FD83970( + tJungles[nBaseOn]!.pDrlgCoord, + pCurrentJungle, + SEED_RollLimitedRandomNumber(pDrlg.pSeed, 5) | 0, + nSpiderForestLevelSizeX, + nSpiderForestLevelSizeY, + ) + + let nFirstOverlappingJungle = 0 + for (nFirstOverlappingJungle = 0; nFirstOverlappingJungle < nJungleAttachIdx; nFirstOverlappingJungle += 1) { + const levelsOverlaps = !DRLG_CheckNotOverlappingUsingManhattanDistance( + tJungles[nFirstOverlappingJungle]!.pDrlgCoord, + pCurrentJungle.pDrlgCoord, + 0, + ) + if (levelsOverlaps) { + break + } + } + + if (nFirstOverlappingJungle < nJungleAttachIdx) { + nJungleAttachIdx -= 1 + continue + } + + pCurrentJungle.pBasedOnJungle = tJungles[nBaseOn]! + tJungles[nBaseOn]!.pJungleBranches[tJungles[nBaseOn]!.nBranch] = pCurrentJungle + tJungles[nBaseOn]!.nBranch += 1 + + if (nMinX > pCurrentJungle.pDrlgCoord.nPosX) nMinX = pCurrentJungle.pDrlgCoord.nPosX + if (nMinY > pCurrentJungle.pDrlgCoord.nPosY) nMinY = pCurrentJungle.pDrlgCoord.nPosY + if (nMaxX < pCurrentJungle.pDrlgCoord.nWidth + pCurrentJungle.pDrlgCoord.nPosX) { + nMaxX = pCurrentJungle.pDrlgCoord.nWidth + pCurrentJungle.pDrlgCoord.nPosX + } + } + + const nPresetsWidth = idiv(nMaxX - nMinX, nPresetBlocksDimensions) + 2 + const nPresetsHeight = idiv(nMaxY - nMinY, nPresetBlocksDimensions) + 2 + const nPresets = nPresetsWidth * nPresetsHeight + + const pPreset0 = new Int32Array(nPresets) + const pPreset1 = new Int32Array(nPresets) + const pPreset2 = new Int32Array(nPresets) + const pLevelPresetId = new Int32Array(nPresets) + + do { + DRLG_GenerateJunglesAttachPoints( + pDrlg, + tJungles, + nMinX, + nMinY, + nSpiderForestLevelSizeX, + nSpiderForestLevelSizeY, + nPresetsWidth, + nPresetsHeight, + pPreset0, + pPreset1, + pPreset2, + pLevelPresetId, + ) + DRLG_JungleComputeConnexity(pDrlg, nPresetsWidth, nPresetsHeight, pPreset0, pPreset1, pPreset2, pLevelPresetId) + } while (!DRLG_JungleUpdateAttachPointsDirections(pDrlg, nPresetsWidth, nPresetsHeight, pPreset0, pPreset2, pLevelPresetId)) + + for (let nCurrentPresetY = 0; nCurrentPresetY < nPresetsHeight; nCurrentPresetY += 1) { + for (let nCurrentPresetX = 0; nCurrentPresetX < nPresetsWidth; nCurrentPresetX += 1) { + const nCurrentPresetIndex = nCurrentPresetY * nPresetsWidth + nCurrentPresetX + pLevelPresetId[nCurrentPresetIndex] = DRLG_JungleNormalizeLevelPresetId(pLevelPresetId[nCurrentPresetIndex]!) + } + } + + const nSpiderForestLevelPresetsBlocksSizeX = idiv(nSpiderForestLevelSizeX, nPresetBlocksDimensions) + const nSpiderForestLevelPresetsBlocksSizeY = idiv(nSpiderForestLevelSizeY, nPresetBlocksDimensions) + + for (let nJungleIdx = 0; nJungleIdx < JUNGLE_MAX_ATTACH; nJungleIdx += 1) { + const tCurrentJungle = tJungles[nJungleIdx]! + let nJungleDefsIndex = 0 + for (let nBlockY = 0; nBlockY < nSpiderForestLevelPresetsBlocksSizeY; nBlockY += 1) { + for (let nBlockX = 0; nBlockX < nSpiderForestLevelPresetsBlocksSizeX; nBlockX += 1) { + const nCurrentPresetIndex = + nBlockX + tCurrentJungle.nPresetsBlocksX + nPresetsWidth * (nBlockY + tCurrentJungle.nPresetsBlocksY) + const nPrestId = pLevelPresetId[nCurrentPresetIndex]! + tCurrentJungle.pJungleDefs![nJungleDefsIndex] = nPrestId + if (nPrestId > LVLPREST_ACT3_JUNGLE_TAIL) { + tCurrentJungle.nJungleDefs += 1 + } + nJungleDefsIndex += 1 + } + } + } + + tJungles.sort((lhs, rhs) => rhs.pDrlgCoord.nPosY - lhs.pDrlgCoord.nPosY) + + let pJungleLevel: D2DrlgLevelStrc | null = null + for (let i = 0; i < JUNGLE_MAX_ATTACH; i += 1) { + pJungleLevel = DRLG_GetLevel(pDrlg, i + LEVEL_SPIDERFOREST) + pJungleLevel.pJungleDefs = tJungles[i]!.pJungleDefs + pJungleLevel.nJungleDefs = tJungles[i]!.nJungleDefs + pJungleLevel.pLevelCoords.nPosX = tJungles[i]!.pDrlgCoord.nPosX + pJungleLevel.pLevelCoords.nPosY = tJungles[i]!.pDrlgCoord.nPosY + pJungleLevel.pLevelCoords.nWidth = tJungles[i]!.pDrlgCoord.nWidth + pJungleLevel.pLevelCoords.nHeight = tJungles[i]!.pDrlgCoord.nHeight + } + + return pJungleLevel! +} + +//D2Common.0x6FD83970 +function sub_6FD83970( + pDrlgCoord: D2DrlgCoordStrc, + pJungle: D2JungleStrc, + nRand: number, + nSizeX: number, + nSizeY: number, +): void { + let nX = 0 + let nY = 0 + + switch (nRand) { + case 0: + nX = 0 + nY = -nSizeY + break + + case 1: { + nX = -nSizeX + const mul = Number(BigInt.asUintN(64, 0x55555555n * BigInt.asUintN(64, BigInt(nSizeY))) >> 32n) | 0 + const nTemp = (mul - nSizeY) >> 1 + nY = ((nTemp >>> 31) + nTemp) | 0 + break + } + + case 2: { + nX = nSizeX + const mul = Number(BigInt.asUintN(64, 0x55555555n * BigInt.asUintN(64, BigInt(nSizeY))) >> 32n) | 0 + const nTemp = (mul - nSizeY) >> 1 + nY = ((nTemp >>> 31) + nTemp) | 0 + break + } + + case 3: { + nX = -nSizeX + const nTemp = Number(BigInt.asUintN(64, 0xffffffff55555554n * BigInt.asUintN(64, BigInt(nSizeY))) >> 32n) | 0 + nY = ((nTemp >>> 31) + nTemp) | 0 + break + } + + case 4: { + nX = nSizeX + const nTemp = Number(BigInt.asUintN(64, 0xffffffff55555554n * BigInt.asUintN(64, BigInt(nSizeY))) >> 32n) | 0 + nY = ((nTemp >>> 31) + nTemp) | 0 + break + } + + default: + break + } + + pJungle.pDrlgCoord.nPosX = nX + pDrlgCoord.nPosX + pJungle.pDrlgCoord.nHeight = nSizeY + pJungle.pDrlgCoord.nWidth = nSizeX + pJungle.pDrlgCoord.nPosY = nY + pDrlgCoord.nPosY + pJungle.field_10 = nRand +} diff --git a/src/game/drlg/drlg-anim.ts b/src/game/drlg/drlg-anim.ts new file mode 100644 index 0000000..066ffd2 --- /dev/null +++ b/src/game/drlg/drlg-anim.ts @@ -0,0 +1,211 @@ +/** + * D2Common DRLG animated tiles: 1:1 port of D2MOO source/D2Common/src/Drlg/DrlgDrlgAnim.cpp + * (commit 5596f5c, MIT License, Copyright (c) 2020-2025 The Phrozen Keep community). + */ + +import { + D2CMP_10079_GetTileFlags, + D2CMP_10081_GetTileRarity, + D2CMP_10088_GetTiles, + type D2TileLibraryEntryStrc, +} from './drlg-d2cmp.ts' +import { DRLGGRID_GetGridEntry } from './drlg-grid.ts' +import { + DRLGROOMTILE_InitFloorTileData, + DRLGROOMTILE_InitShadowTileData, + DRLGROOMTILE_InitWallTileData, + GetMapTileLayer, + MAPTILE_HIDDEN, + PackedTile, + TILETYPE_FLOOR, + TILETYPE_SHADOW, + tileGridOf, +} from './drlg-roomtile.ts' +import { + DRLGROOMFLAG_ANIMATED_FLOOR, + type D2DrlgAnimTileGridStrc, + type D2DrlgGridStrc, + type D2DrlgRoomStrc, + type D2DrlgTileDataStrc, +} from './drlg-types.ts' + +// D2TileMaterialFlags (D2CMP.h) +const TILE_FLAGS_LAVA = 0x100 + +//D2Common.0x6FD75560 +export function DRLGANIM_TestLoadAnimatedRoomTiles( + pDrlgRoom: D2DrlgRoomStrc, + pDrlgGrid: D2DrlgGridStrc, + pTileTypeGrid: D2DrlgGridStrc | null, + nTileType: number, + bKillEdgeX: boolean, + bKillEdgeY: boolean, +): void { + const ppTileLibraryEntry: D2TileLibraryEntryStrc[] = [] + const pTiles = tileGridOf(pDrlgRoom).pTiles + const nMaxY = (pDrlgRoom.pDrlgCoord.nHeight + (bKillEdgeY ? 0 : 1)) | 0 + const nMaxX = (pDrlgRoom.pDrlgCoord.nWidth + (bKillEdgeX ? 0 : 1)) | 0 + + for (let nY = 0; nY < nMaxY; nY += 1) { + for (let nX = 0; nX < nMaxX; nX += 1) { + const nGridEntry = DRLGGRID_GetGridEntry(pDrlgGrid, nX, nY) >>> 0 + if (PackedTile.bShadow(nGridEntry) || PackedTile.bIsWall(nGridEntry) || PackedTile.bIsFloor(nGridEntry)) { + if (pTileTypeGrid) { + nTileType = DRLGGRID_GetGridEntry(pTileTypeGrid, nX, nY) + } + + const nStyle = PackedTile.nTileStyle(nGridEntry) + const nSequence = PackedTile.nTileSequence(nGridEntry) + + const nTilesCount = D2CMP_10088_GetTiles(pDrlgRoom.pTiles, nTileType, nStyle, nSequence, ppTileLibraryEntry, 40) + if (nTilesCount && D2CMP_10079_GetTileFlags(ppTileLibraryEntry[0]!) & TILE_FLAGS_LAVA) { + if (PackedTile.bIsFloor(nGridEntry)) { + pTiles.nFloors = (pTiles.nFloors + nTilesCount - 1) | 0 + } else if (PackedTile.bIsWall(nGridEntry)) { + pTiles.nWalls = (pTiles.nWalls + nTilesCount - 1) | 0 + } else { + pTiles.nRoofs = (pTiles.nRoofs + nTilesCount - 1) | 0 + } + + pDrlgRoom.dwFlags = (pDrlgRoom.dwFlags | DRLGROOMFLAG_ANIMATED_FLOOR) >>> 0 + } + } + } + } +} + +// Helper function +function DRLGANIM_FindAnimatedTileFrame( + pTileLibraryEntries: D2TileLibraryEntryStrc[], + nbTileEntries: number, + nRarity: number, + nStyle: number, + nSeq: number, +): D2TileLibraryEntryStrc { + for (let nEntryIndex = 0; nEntryIndex < nbTileEntries; nEntryIndex += 1) { + if (D2CMP_10081_GetTileRarity(pTileLibraryEntries[nEntryIndex]!) === nRarity) { + return pTileLibraryEntries[nEntryIndex]! + } + } + throw new Error(`Animating tiles missing Rarity ${nRarity} Tile - Style ${nStyle} Seq ${nSeq}`) +} + +//D2Common.0x6FD75740 +export function DRLGANIM_AllocAnimationTileGrids( + pDrlgRoom: D2DrlgRoomStrc, + nAnimationSpeed: number, + pWallGrid: D2DrlgGridStrc[], + nWalls: number, + pFloorGrid: D2DrlgGridStrc[], + nFloors: number, + pShadowGrid: D2DrlgGridStrc, +): void { + const pTileGrid = tileGridOf(pDrlgRoom) + DRLGANIM_AllocAnimationTileGrid( + pDrlgRoom, + nAnimationSpeed, + pTileGrid.pTiles.pWallTiles, + pTileGrid.nWalls, + pWallGrid, + nWalls, + ) + DRLGANIM_AllocAnimationTileGrid( + pDrlgRoom, + nAnimationSpeed, + pTileGrid.pTiles.pFloorTiles, + pTileGrid.nFloors, + pFloorGrid, + nFloors, + ) + DRLGANIM_AllocAnimationTileGrid( + pDrlgRoom, + nAnimationSpeed, + pTileGrid.pTiles.pRoofTiles, + pTileGrid.nShadows, + [pShadowGrid], + 1, + ) +} + +//D2Common.0x6FD757B0 +export function DRLGANIM_AllocAnimationTileGrid( + pDrlgRoom: D2DrlgRoomStrc, + nAnimationSpeed: number, + pTiles: D2DrlgTileDataStrc[] | null, + nTiles: number, + pDrlgGrid: D2DrlgGridStrc[], + _nUnused: number, +): void { + if (!pTiles) { + return + } + if (nAnimationSpeed === 0) { + nAnimationSpeed = 80 + } + + const pTileGrid = tileGridOf(pDrlgRoom) + const pTileLibraryEntries: D2TileLibraryEntryStrc[] = [] + + for (let i = 0; i < nTiles; i += 1) { + const pCurrentTileData = pTiles[i]! + if (pCurrentTileData.pTile && D2CMP_10079_GetTileFlags(pCurrentTileData.pTile) & TILE_FLAGS_LAVA) { + const nGridIdx = GetMapTileLayer(pCurrentTileData.dwFlags) + const pLayerGrid = pDrlgGrid[nGridIdx < 0 ? 0 : nGridIdx]! + const nGridEntry = DRLGGRID_GetGridEntry(pLayerGrid, pCurrentTileData.nPosX, pCurrentTileData.nPosY) >>> 0 + + const nStyle = PackedTile.nTileStyle(nGridEntry) + const nSeq = PackedTile.nTileSequence(nGridEntry) + + const nFrames = D2CMP_10088_GetTiles( + pDrlgRoom.pTiles, + pCurrentTileData.nTileType, + nStyle, + nSeq, + pTileLibraryEntries, + 40, + ) + + const ppMapTileData = new Array(nFrames).fill(null) + const pDrlgAnimTileGrid: D2DrlgAnimTileGridStrc = { + ppMapTileData, + nFrames, + nCurrentFrame: 0, + nAnimationSpeed, + pNext: pTileGrid.pAnimTiles, + } + + pTileGrid.pAnimTiles = pDrlgAnimTileGrid + + // Animated tiles use rarity as frame index + pCurrentTileData.pTile = DRLGANIM_FindAnimatedTileFrame(pTileLibraryEntries, nFrames, 0, nStyle, nSeq) + pDrlgAnimTileGrid.ppMapTileData[0] = pCurrentTileData + + const nX = (pCurrentTileData.nPosX + pDrlgRoom.pDrlgCoord.nPosX) | 0 + const nY = (pCurrentTileData.nPosY + pDrlgRoom.pDrlgCoord.nPosY) | 0 + + for (let nRarity = 1; nRarity < nFrames; nRarity += 1) { + const pTileEntry = DRLGANIM_FindAnimatedTileFrame(pTileLibraryEntries, nFrames, nRarity, nStyle, nSeq) + + let pAddedTile: D2DrlgTileDataStrc + if (pCurrentTileData.nTileType === TILETYPE_FLOOR) { + pAddedTile = DRLGROOMTILE_InitFloorTileData(pDrlgRoom, null, nX, nY, nGridEntry, pTileEntry) + } else if (pCurrentTileData.nTileType === TILETYPE_SHADOW) { + pAddedTile = DRLGROOMTILE_InitShadowTileData(pDrlgRoom, null, nX, nY, nGridEntry, pTileEntry) + } else { + pAddedTile = DRLGROOMTILE_InitWallTileData( + pDrlgRoom, + null, + nX, + nY, + nGridEntry, + pTileEntry, + pCurrentTileData.nTileType, + ) + } + pDrlgAnimTileGrid.ppMapTileData[nRarity] = pAddedTile + // We start by displaying the first frame, hide all other frames tiles. + pAddedTile.dwFlags = (pAddedTile.dwFlags | MAPTILE_HIDDEN) >>> 0 + } + } + } +} diff --git a/src/game/drlg/drlg-d2cmp.ts b/src/game/drlg/drlg-d2cmp.ts index 2792ce9..2d8be72 100644 --- a/src/game/drlg/drlg-d2cmp.ts +++ b/src/game/drlg/drlg-d2cmp.ts @@ -83,18 +83,13 @@ function loadTileLibrary(env: DrlgEnv, szFileName: string): D2TileLibraryStrc { return lib } -//D2Cmp.#10087 +//D2Cmp.#10087 (1.13c D2CMP.dll 0x6fe25480) export function D2CMP_10087_LoadTileLibrarySlot( env: DrlgEnv, ppTileLibraryHash: D2TileLibraryStrc[], szFileName: string, ): void { const pLibrary = loadTileLibrary(env, szFileName) - for (let i = 0; i < ppTileLibraryHash.length; i += 1) { - if (ppTileLibraryHash[i] === pLibrary) { - throw new Error(`tile library ${pLibrary.szPath} loaded twice into the same slot array`) - } - } if (ppTileLibraryHash.length >= TILE_SLOTS) { throw new Error(`no free tile library slot for ${pLibrary.szPath}`) } diff --git a/src/game/drlg/drlg-drlg.ts b/src/game/drlg/drlg-drlg.ts index 278d8dd..a6cd293 100644 --- a/src/game/drlg/drlg-drlg.ts +++ b/src/game/drlg/drlg-drlg.ts @@ -29,6 +29,7 @@ import { LEVEL_LUTGHOLEIN, LEVEL_NONE, LEVEL_ROGUEENCAMPMENT, + LEVEL_TALRASHASTOMB1, LEVEL_THEPANDEMONIUMFORTRESS, newCoord, type D2DrlgCoordStrc, @@ -81,9 +82,23 @@ export function DRLG_AllocDrlg( switch (nActNo) { case 0: // ACT_I: D2CMP_10087_LoadTileLibrarySlot(pDrlg->pTiles, "DATA\\GLOBAL\\Tiles\\Act1\\Town\\Floor.dt1") break + case 1: { + // ACT_II + let nStaffLevelOffset = 0 + let nBossLevelOffset = 0 + do { + nStaffLevelOffset = (SEED_RollRandomNumber(pDrlg.pSeed) >>> 0) % 7 + nBossLevelOffset = (SEED_RollRandomNumber(pDrlg.pSeed) >>> 0) % 7 + } while (nStaffLevelOffset === nBossLevelOffset) + pDrlg.nStaffTombLevel = LEVEL_TALRASHASTOMB1 + nStaffLevelOffset + pDrlg.nBossTombLevel = LEVEL_TALRASHASTOMB1 + nBossLevelOffset + break + } + case 2: // ACT_III + pDrlg.bJungleInterlink = SEED_RollRandomNumber(pDrlg.pSeed) & 1 + break default: - // Acts II/III roll pDrlg->pSeed here (staff/boss tombs, jungle interlink); not ported yet. - throw new Error(`DRLG_AllocDrlg: act ${nActNo} is not ported`) + break } // DRLGACTIVATE_InitializeRoomExStatusLists(pDrlg): room status lists are not modelled. @@ -119,6 +134,8 @@ export function DRLG_AllocLevel(pDrlg: D2DrlgStrc, nLevelId: number): D2DrlgLeve nRoom_Center_Warp_X: [0, 0, 0, 0, 0, 0, 0, 0, 0], nRoom_Center_Warp_Y: [0, 0, 0, 0, 0, 0, 0, 0, 0], nRoomCoords: 0, + pJungleDefs: null, + nJungleDefs: 0, pBuild: null, pPresetMaps: null, pNextLevel: null, diff --git a/src/game/drlg/drlg-dump.ts b/src/game/drlg/drlg-dump.ts index 1cf7256..64f5f47 100644 --- a/src/game/drlg/drlg-dump.ts +++ b/src/game/drlg/drlg-dump.ts @@ -19,7 +19,11 @@ import { DRLGTYPE_MAZE, DRLGTYPE_OUTDOOR, DRLGTYPE_PRESET, + LEVEL_HARROGATH, + LEVEL_KURASTDOCKTOWN, + LEVEL_LUTGHOLEIN, LEVEL_ROGUEENCAMPMENT, + LEVEL_THEPANDEMONIUMFORTRESS, outdoorCoord, type D2DrlgCoordStrc, type D2DrlgGridStrc, @@ -574,7 +578,6 @@ function dumpMapTiles(index: DumpRefIndex, pRoom: D2DrlgRoomStrc, nLevelId: numb const where = `level ${nLevelId} room ${nRoom}` const pTileGrid = pRoom.pTileGrid if (!pTileGrid) throw new Error(`${where}: no pTileGrid after DRLGROOMTILE_AddRoomMapTiles`) - if (pTileGrid.pAnimTiles) throw new Error(`${where}: animated tiles (pAnimTiles) are not dumped`) try { const t = pTileGrid.pTiles const links: DumpTileLink[] = [] @@ -603,6 +606,7 @@ function dumpMapTiles(index: DumpRefIndex, pRoom: D2DrlgRoomStrc, nLevelId: numb // ---- driver (TypeScript twin of tools/d2moo-oracle/src/main.cpp) ---- export interface Act1RunOptions { + readonly act?: number readonly difficulty?: number /** A fresh act per requested level (proves a level does not depend on the others). */ readonly isolated?: boolean @@ -635,19 +639,27 @@ export interface Act1Run { readonly levels: readonly D2DrlgLevelStrc[] } -function allocAct1(env: DrlgEnv, nSeed: number, nDifficulty: number, trace: DrlgTraceWriter | undefined): D2DrlgStrc { +const TOWN_LEVEL_IDS = [ + LEVEL_ROGUEENCAMPMENT, + LEVEL_LUTGHOLEIN, + LEVEL_KURASTDOCKTOWN, + LEVEL_THEPANDEMONIUMFORTRESS, + LEVEL_HARROGATH, +] as const + +function allocActByNo(env: DrlgEnv, nAct: number, nSeed: number, nDifficulty: number, trace: DrlgTraceWriter | undefined): D2DrlgStrc { trace?.context('drlg') // D2Game allocates the act with the town level id; DRLG_AllocDrlg lays out the whole act // (DRLGOUTPLACE_CreateLevelConnections) and generates the town. - return DRLG_AllocDrlg(env, ACT_I, nSeed, LEVEL_ROGUEENCAMPMENT, 0, nDifficulty) + return DRLG_AllocDrlg(env, nAct, nSeed, TOWN_LEVEL_IDS[nAct]!, 0, nDifficulty) } /** Generation stage of one level: DRLG_InitLevel, everything that exists before any room is activated. */ -function initLevel(pDrlg: D2DrlgStrc, nLevelId: number, trace: DrlgTraceWriter | undefined): D2DrlgLevelStrc { +function initLevel(pDrlg: D2DrlgStrc, nLevelId: number, nAct: number, trace: DrlgTraceWriter | undefined): D2DrlgLevelStrc { trace?.context(`L${nLevelId}`) const pLevel = DRLG_GetLevel(pDrlg, nLevelId) - if (pLevel.nDrlgType !== DRLGTYPE_OUTDOOR || DRLG_GetActNoFromLevelId(nLevelId) !== ACT_I) { - throw new Error(`level ${nLevelId} is not an Act I outdoor level`) + if (pLevel.nDrlgType !== DRLGTYPE_OUTDOOR || DRLG_GetActNoFromLevelId(nLevelId) !== nAct) { + throw new Error(`level ${nLevelId} is not an Act ${nAct + 1} outdoor level`) } if (pLevel.pFirstRoomEx) throw new Error(`level ${nLevelId} was already generated before its own generation stage`) DRLG_InitLevel(pLevel) @@ -684,7 +696,7 @@ function activateLevel(pLevel: D2DrlgLevelStrc, trace: DrlgTraceWriter | undefin } /** - * Runs the Act I generation exactly like the oracle driver: DRLG_AllocDrlg (layout, links, town), then + * Runs the act generation exactly like the oracle driver: DRLG_AllocDrlg (layout, links, town), then * DRLG_InitLevel of every requested level, then the activation of every room of those levels (see * activateLevel). A non-isolated run generates every requested level before activating any room, because * activating a room initialises the levels behind its warps (sub_6FD77BB0 -> DRLG_InitLevel); an isolated @@ -698,18 +710,30 @@ export function runAct1( hooks: Act1RunHooks = {}, ): Act1Run { const nDifficulty = opts.difficulty ?? 0 + let nAct: number = ACT_I + if (opts.act !== undefined) { + if (opts.act < 0 || opts.act > 4) throw new Error('act must be 0..4') + nAct = opts.act + } else if (levels.length > 0) { + nAct = DRLG_GetActNoFromLevelId(levels[0]!) + } + for (const id of levels) { + if (DRLG_GetActNoFromLevelId(id) !== nAct) { + throw new Error(`level ${id} belongs to act ${DRLG_GetActNoFromLevelId(id)}, not act ${nAct}`) + } + } const trace = opts.trace if (trace) setD2SeedTraceSink(trace) try { let nActs = 0 const allocAct = (): D2DrlgStrc => { - const pDrlg = allocAct1(env, nSeed >>> 0, nDifficulty, trace) + const pDrlg = allocActByNo(env, nAct, nSeed >>> 0, nDifficulty, trace) hooks.onDrlgAllocated?.(pDrlg, nActs) nActs += 1 return pDrlg } const generate = (pDrlg: D2DrlgStrc, nLevelId: number): D2DrlgLevelStrc => { - const pLevel = initLevel(pDrlg, nLevelId, trace) + const pLevel = initLevel(pDrlg, nLevelId, nAct, trace) hooks.onLevelGenerated?.(pLevel) return pLevel } @@ -733,6 +757,8 @@ export function runAct1( } } +export const runAct = runAct1 + export interface DumpAct1Options extends Act1RunOptions { /** Stop after the act stage (the `levels` array stays empty). */ readonly actOnly?: boolean @@ -806,8 +832,12 @@ export function dumpAct1(env: DrlgEnv, nSeed: number, levels: readonly number[], levelGrids.set(pLevel.nLevelId, dumpLevelGrid(pLevel)) }) let run: Act1Run + const runOpts: DumpAct1Options = + opts.act === undefined && levels.length > 0 + ? { ...opts, act: DRLG_GetActNoFromLevelId(levels[0]!) } + : opts try { - run = runAct1(env, nSeed, opts.actOnly ? [] : levels, opts, { + run = runAct1(env, nSeed, opts.actOnly ? [] : levels, runOpts, { onDrlgAllocated(pDrlg, index) { if (index !== 0) return drlg = { startSeed: u32(pDrlg.dwStartSeed), seed: dumpSeed(pDrlg.pSeed) } @@ -815,7 +845,7 @@ export function dumpAct1(env: DrlgEnv, nSeed: number, levels: readonly number[], }, onLevelGenerated(pLevel) { const levelGrid = levelGrids.get(pLevel.nLevelId) - if (!levelGrid) throw new Error(`level ${pLevel.nLevelId}: InitAct1OutdoorLevel hook did not fire`) + if (!levelGrid) throw new Error(`level ${pLevel.nLevelId}: outdoor levelGrid hook did not fire`) heads.set(pLevel, dumpLevelHead(pLevel, levelGrid)) activations.set(pLevel, []) }, diff --git a/src/game/drlg/drlg-logic.ts b/src/game/drlg/drlg-logic.ts index 98fddb7..24b025a 100644 --- a/src/game/drlg/drlg-logic.ts +++ b/src/game/drlg/drlg-logic.ts @@ -203,6 +203,8 @@ const stru_6FDCE5C8: readonly { nX: number; nY: number }[] = [ { nX: 0, nY: -1 }, ] +const dword_6FDCE5EC_NEG: readonly number[] = [1, -1, 0, 0, -1] + const dword_6FDCE5EC: readonly number[] = [ 23, 0, 5, 21, 17, 15, 3, 0, 9, 7, 39, 0, 0, 5, 3, 31, 31, 31, 31, 31, 31, 31, 31, 31, 31, 0, ] @@ -239,16 +241,16 @@ function DRLGLOGIC_SetTileGridFlags(a1: D2UnkDrlgLogicStrc, nX: number, nY: numb nIndex = DRLGGRID_GetGridEntry(a1.pTileTypeGrid, nPosX, nPosY) } - // dword_6FDCE650 has 20 entries and dword_6FDCE5EC 26: any other subscript reads unrelated memory (UB; - // tile types 0 and 13 map to row -1, which lands before dword_6FDCE5EC). + // dword_6FDCE650 maps tile types 0 and 13 to row -1 (nFlagIndex = a4 - 4 in -5..-1), which in + // DrlgDrlgLogic.cpp reads the trailing 5 ints of stru_6FDCE5C8 immediately preceding dword_6FDCE5EC. const nRow = nIndex >= 0 && nIndex < dword_6FDCE650.length ? dword_6FDCE650[nIndex]! : undefined - const nFlagIndex = nRow === undefined ? -1 : (a4 + 5 * nRow + 1) | 0 - if (nRow === undefined || nFlagIndex < 0 || nFlagIndex >= dword_6FDCE5EC.length) { + const nFlagIndex = nRow === undefined ? -99 : (a4 + 5 * nRow + 1) | 0 + if (nRow === undefined || nFlagIndex < -5 || nFlagIndex >= dword_6FDCE5EC.length) { throw new RangeError( `DRLGLOGIC_SetTileGridFlags: out-of-bounds table read (tile type ${nIndex}, a4 ${a4}) at room tile (${nPosX}, ${nPosY})`, ) } - const nFlags = dword_6FDCE5EC[nFlagIndex]! + const nFlags = nFlagIndex < 0 ? dword_6FDCE5EC_NEG[nFlagIndex + 5]! : dword_6FDCE5EC[nFlagIndex]! if (nFlags & 1) { DRLGGRID_AlterGridFlag(a1.field_4, nPosX, nPosY, a1.nFlags, FLAG_OPERATION_OR) } diff --git a/src/game/drlg/drlg-maze.ts b/src/game/drlg/drlg-maze.ts index 0f7e28e..8d997a5 100644 --- a/src/game/drlg/drlg-maze.ts +++ b/src/game/drlg/drlg-maze.ts @@ -132,9 +132,139 @@ import { LVLPREST_ACT2_CORRUPT_HAREM_SW, LVLPREST_ACT2_DESERT_RUINS_ELDER, LVLPREST_ACT2_DURIELS_LAIR, + LVLPREST_ACT2_LAIR_E, + LVLPREST_ACT2_LAIR_N, + LVLPREST_ACT2_LAIR_NEXT_E, + LVLPREST_ACT2_LAIR_NEXT_N, + LVLPREST_ACT2_LAIR_NEXT_S, + LVLPREST_ACT2_LAIR_NEXT_W, + LVLPREST_ACT2_LAIR_PREV_E, + LVLPREST_ACT2_LAIR_PREV_N, + LVLPREST_ACT2_LAIR_PREV_S, + LVLPREST_ACT2_LAIR_PREV_W, + LVLPREST_ACT2_LAIR_S, LVLPREST_ACT2_LAIR_TIGHT_SPOT_S, + LVLPREST_ACT2_LAIR_TREASURE_E, + LVLPREST_ACT2_LAIR_TREASURE_N, + LVLPREST_ACT2_LAIR_TREASURE_S, + LVLPREST_ACT2_LAIR_TREASURE_W, + LVLPREST_ACT2_LAIR_W, + LVLPREST_ACT2_SEWER_CHEST_E, + LVLPREST_ACT2_SEWER_CHEST_N, + LVLPREST_ACT2_SEWER_CHEST_S, + LVLPREST_ACT2_SEWER_CHEST_W, + LVLPREST_ACT2_SEWER_E, + LVLPREST_ACT2_SEWER_N, + LVLPREST_ACT2_SEWER_NEXT_E, + LVLPREST_ACT2_SEWER_NEXT_N, + LVLPREST_ACT2_SEWER_NEXT_S, + LVLPREST_ACT2_SEWER_NEXT_W, + LVLPREST_ACT2_SEWER_PREV_E, + LVLPREST_ACT2_SEWER_PREV_N, + LVLPREST_ACT2_SEWER_PREV_NS, + LVLPREST_ACT2_SEWER_PREV_S, + LVLPREST_ACT2_SEWER_PREV_W, + LVLPREST_ACT2_SEWER_RADAMENTS_LAIR_E, + LVLPREST_ACT2_SEWER_RADAMENTS_LAIR_N, + LVLPREST_ACT2_SEWER_RADAMENTS_LAIR_S, + LVLPREST_ACT2_SEWER_RADAMENTS_LAIR_W, + LVLPREST_ACT2_SEWER_S, + LVLPREST_ACT2_SEWER_W, + LVLPREST_ACT2_SEWER_WAYPOINT_E, + LVLPREST_ACT2_SEWER_WAYPOINT_N, + LVLPREST_ACT2_SEWER_WAYPOINT_S, + LVLPREST_ACT2_SEWER_WAYPOINT_W, + LVLPREST_ACT2_TOMB_CHEST_E, + LVLPREST_ACT2_TOMB_CHEST_N, + LVLPREST_ACT2_TOMB_CHEST_S, + LVLPREST_ACT2_TOMB_CHEST_W, + LVLPREST_ACT2_TOMB_CUBE_E, + LVLPREST_ACT2_TOMB_CUBE_N, + LVLPREST_ACT2_TOMB_CUBE_S, + LVLPREST_ACT2_TOMB_CUBE_W, + LVLPREST_ACT2_TOMB_E, + LVLPREST_ACT2_TOMB_KAA_E, + LVLPREST_ACT2_TOMB_KAA_N, + LVLPREST_ACT2_TOMB_KAA_S, + LVLPREST_ACT2_TOMB_KAA_W, + LVLPREST_ACT2_TOMB_LEATHERARM_E, + LVLPREST_ACT2_TOMB_LEATHERARM_N, + LVLPREST_ACT2_TOMB_LEATHERARM_S, + LVLPREST_ACT2_TOMB_LEATHERARM_W, + LVLPREST_ACT2_TOMB_N, + LVLPREST_ACT2_TOMB_NEXT_E, + LVLPREST_ACT2_TOMB_NEXT_N, + LVLPREST_ACT2_TOMB_NEXT_S, + LVLPREST_ACT2_TOMB_NEXT_W, + LVLPREST_ACT2_TOMB_PREV_NEW, + LVLPREST_ACT2_TOMB_PREV_NSE, + LVLPREST_ACT2_TOMB_PREV_NSW, + LVLPREST_ACT2_TOMB_PREV_SEW, + LVLPREST_ACT2_TOMB_S, + LVLPREST_ACT2_TOMB_TAINTED_SUN_X, + LVLPREST_ACT2_TOMB_TALRASHA_E, + LVLPREST_ACT2_TOMB_TALRASHA_N, + LVLPREST_ACT2_TOMB_TALRASHA_S, + LVLPREST_ACT2_TOMB_TALRASHA_W, + LVLPREST_ACT2_TOMB_TREASURE_E, + LVLPREST_ACT2_TOMB_TREASURE_N, + LVLPREST_ACT2_TOMB_TREASURE_S, + LVLPREST_ACT2_TOMB_TREASURE_W, + LVLPREST_ACT2_TOMB_W, + LVLPREST_ACT2_TOMB_WAYPOINT_E, + LVLPREST_ACT2_TOMB_WAYPOINT_N, + LVLPREST_ACT2_TOMB_WAYPOINT_S, + LVLPREST_ACT2_TOMB_WAYPOINT_W, LVLPREST_ACT2_TOWN, + LVLPREST_ACT3_DUNGEON_E, + LVLPREST_ACT3_DUNGEON_N, + LVLPREST_ACT3_DUNGEON_NEXT_E, + LVLPREST_ACT3_DUNGEON_NEXT_N, + LVLPREST_ACT3_DUNGEON_NEXT_S, + LVLPREST_ACT3_DUNGEON_NEXT_W, + LVLPREST_ACT3_DUNGEON_PREV_E, + LVLPREST_ACT3_DUNGEON_PREV_N, + LVLPREST_ACT3_DUNGEON_PREV_S, + LVLPREST_ACT3_DUNGEON_PREV_W, + LVLPREST_ACT3_DUNGEON_S, LVLPREST_ACT3_DUNGEON_TREASURE_2, + LVLPREST_ACT3_DUNGEON_W, + LVLPREST_ACT3_MEPHISTO_E, + LVLPREST_ACT3_MEPHISTO_N, + LVLPREST_ACT3_MEPHISTO_NEXT_E, + LVLPREST_ACT3_MEPHISTO_NEXT_N, + LVLPREST_ACT3_MEPHISTO_NEXT_S, + LVLPREST_ACT3_MEPHISTO_NEXT_W, + LVLPREST_ACT3_MEPHISTO_PREV_E, + LVLPREST_ACT3_MEPHISTO_PREV_N, + LVLPREST_ACT3_MEPHISTO_PREV_S, + LVLPREST_ACT3_MEPHISTO_PREV_W, + LVLPREST_ACT3_MEPHISTO_S, + LVLPREST_ACT3_MEPHISTO_W, + LVLPREST_ACT3_MEPHISTO_WAYPOINT_E, + LVLPREST_ACT3_MEPHISTO_WAYPOINT_N, + LVLPREST_ACT3_MEPHISTO_WAYPOINT_S, + LVLPREST_ACT3_MEPHISTO_WAYPOINT_W, + LVLPREST_ACT3_SEWER_CHEST_E, + LVLPREST_ACT3_SEWER_CHEST_N, + LVLPREST_ACT3_SEWER_CHEST_S, + LVLPREST_ACT3_SEWER_CHEST_W, + LVLPREST_ACT3_SEWER_DRAIN_E, + LVLPREST_ACT3_SEWER_DRAIN_N, + LVLPREST_ACT3_SEWER_DRAIN_S, + LVLPREST_ACT3_SEWER_DRAIN_W, + LVLPREST_ACT3_SEWER_E, + LVLPREST_ACT3_SEWER_N, + LVLPREST_ACT3_SEWER_NE, + LVLPREST_ACT3_SEWER_NW, + LVLPREST_ACT3_SEWER_PREV_NE, + LVLPREST_ACT3_SEWER_PREV_NW, + LVLPREST_ACT3_SEWER_PREV_SE, + LVLPREST_ACT3_SEWER_PREV_SW, + LVLPREST_ACT3_SEWER_S, + LVLPREST_ACT3_SEWER_SE, + LVLPREST_ACT3_SEWER_SW, + LVLPREST_ACT3_SEWER_W, LVLPREST_ACT3_SPIDER_CHEST_NE, LVLPREST_ACT3_SPIDER_CHEST_NW, LVLPREST_ACT3_SPIDER_NE, @@ -142,8 +272,59 @@ import { LVLPREST_ACT3_SPIDER_SE, LVLPREST_ACT3_SPIDER_SW, LVLPREST_ACT3_TEMPLE_6, + LVLPREST_ACT4_BRIDGE_1, + LVLPREST_ACT4_BRIDGE_2, + LVLPREST_ACT4_LAVA_E, + LVLPREST_ACT4_LAVA_FORGE_E, + LVLPREST_ACT4_LAVA_FORGE_W, + LVLPREST_ACT4_LAVA_W, + LVLPREST_ACT4_LAVA_WARP_N, LVLPREST_ACT4_LAVA_X, + LVLPREST_ACT5_BAAL_E, + LVLPREST_ACT5_BAAL_N, + LVLPREST_ACT5_BAAL_NEXT_E, + LVLPREST_ACT5_BAAL_NEXT_N, + LVLPREST_ACT5_BAAL_NEXT_S, + LVLPREST_ACT5_BAAL_NEXT_W, + LVLPREST_ACT5_BAAL_PREV_NEW, + LVLPREST_ACT5_BAAL_PREV_NSE, + LVLPREST_ACT5_BAAL_PREV_NSW, + LVLPREST_ACT5_BAAL_PREV_SEW, + LVLPREST_ACT5_BAAL_S, + LVLPREST_ACT5_BAAL_W, + LVLPREST_ACT5_BAAL_WAYPOINT_E, + LVLPREST_ACT5_BAAL_WAYPOINT_N, + LVLPREST_ACT5_BAAL_WAYPOINT_S, + LVLPREST_ACT5_BAAL_WAYPOINT_W, LVLPREST_ACT5_BARRICADE_16_SNOW, + LVLPREST_ACT5_ICE_DOWN_E, + LVLPREST_ACT5_ICE_DOWN_N, + LVLPREST_ACT5_ICE_DOWN_S, + LVLPREST_ACT5_ICE_DOWN_W, + LVLPREST_ACT5_ICE_E, + LVLPREST_ACT5_ICE_N, + LVLPREST_ACT5_ICE_NEXT_E, + LVLPREST_ACT5_ICE_NEXT_N, + LVLPREST_ACT5_ICE_NEXT_S, + LVLPREST_ACT5_ICE_NEXT_W, + LVLPREST_ACT5_ICE_POOL_A, + LVLPREST_ACT5_ICE_POOL_B, + LVLPREST_ACT5_ICE_PREV_E, + LVLPREST_ACT5_ICE_PREV_N, + LVLPREST_ACT5_ICE_PREV_S, + LVLPREST_ACT5_ICE_PREV_W, + LVLPREST_ACT5_ICE_RIVER_A, + LVLPREST_ACT5_ICE_RIVER_B, + LVLPREST_ACT5_ICE_S, + LVLPREST_ACT5_ICE_THEME_E, + LVLPREST_ACT5_ICE_THEME_N, + LVLPREST_ACT5_ICE_THEME_S, + LVLPREST_ACT5_ICE_THEME_W, + LVLPREST_ACT5_ICE_W, + LVLPREST_ACT5_ICE_WAYPOINT_E, + LVLPREST_ACT5_ICE_WAYPOINT_N, + LVLPREST_ACT5_ICE_WAYPOINT_S, + LVLPREST_ACT5_ICE_WAYPOINT_W, LVLPREST_ACT5_LAVA_E, LVLPREST_ACT5_LAVA_EW, LVLPREST_ACT5_LAVA_N, @@ -151,9 +332,15 @@ import { LVLPREST_ACT5_LAVA_S, LVLPREST_ACT5_LAVA_W, LVLPREST_ACT5_TEMPLE_NE, + LVLPREST_ACT5_TEMPLE_NE_DOWN, + LVLPREST_ACT5_TEMPLE_NE_WAYPOINT, LVLPREST_ACT5_TEMPLE_NW, + LVLPREST_ACT5_TEMPLE_NW_DOWN, + LVLPREST_ACT5_TEMPLE_NW_WAYPOINT, LVLPREST_ACT5_TEMPLE_SE_UP, + LVLPREST_ACT5_TEMPLE_SE_WAYPOINT, LVLPREST_ACT5_TEMPLE_SW, + LVLPREST_ACT5_TEMPLE_SW_DOWN, LVLPREST_ACT5_TEMPLE_SW_WAYPOINT, LVLPREST_NONE, LVLTYPE_ACT1_BARRACKS, @@ -189,6 +376,7 @@ import { DRLG_GetRectanglesManhattanDistanceAndCheckNotOverlapping, } from './drlg-room.ts' import { + ACT_V, ALTDIR_EAST, ALTDIR_NORTH, ALTDIR_SOUTH, @@ -196,11 +384,51 @@ import { DIRECTION_INVALID, DRLGPRESETROOMFLAG_HAS_MAP_DS1, DRLGTYPE_PRESET, + LEVEL_ANCIENTTUNNELS, + LEVEL_BARRACKS, + LEVEL_CATACOMBSLEV1, + LEVEL_CATACOMBSLEV2, LEVEL_CAVELEV1, + LEVEL_CELLAROFPITY, + LEVEL_CHAOSSANCTUM, + LEVEL_CLAWVIPERTEMPLELEV1, + LEVEL_CLAWVIPERTEMPLELEV2, LEVEL_CRYPT, + LEVEL_CRYSTALIZEDCAVERNLEV1, + LEVEL_CRYSTALIZEDCAVERNLEV2, LEVEL_DENOFEVIL, + LEVEL_DURANCEOFHATELEV1, + LEVEL_DURANCEOFHATELEV2, + LEVEL_ECHOCHAMBER, + LEVEL_GLACIALCAVESLEV1, + LEVEL_GLACIALCAVESLEV2, + LEVEL_HALLSOFDEATHSCALLING, + LEVEL_HALLSOFTHEDEADLEV2, + LEVEL_HALLSOFTHEDEADLEV3, + LEVEL_HALLSOFVAUGHT, + LEVEL_JAILLEV1, + LEVEL_JAILLEV2, + LEVEL_JAILLEV3, + LEVEL_MAGGOTLAIRLEV3, LEVEL_MAUSOLEUM, LEVEL_OUTERCLOISTER, + LEVEL_PALACECELLARLEV1, + LEVEL_PALACECELLARLEV3, + LEVEL_PANDEMONIUMRUN1, + LEVEL_RIVEROFFLAME, + LEVEL_SEWERSA3LEV1, + LEVEL_SEWERSLEV1, + LEVEL_SEWERSLEV2, + LEVEL_SEWERSLEV3, + LEVEL_SPIDERCAVE, + LEVEL_SPIDERCAVERN, + LEVEL_STONYTOMBLEV1, + LEVEL_STONYTOMBLEV2, + LEVEL_TALRASHASTOMB1, + LEVEL_TALRASHASTOMB7, + LEVEL_THEWORLDSTONEKEEPLEV2, + LEVEL_TOWERCELLARLEV1, + LEVEL_TOWERCELLARLEV4, LEVEL_UNDERGROUNDPASSAGELEV1, type D2DrlgBuildStrc, type D2DrlgLevelStrc, @@ -208,19 +436,6 @@ import { type D2DrlgRoomStrc, } from './drlg-types.ts' -const LEVEL_TOWERCELLARLEV1 = 21 -const LEVEL_TOWERCELLARLEV4 = 24 -const LEVEL_BARRACKS = 28 -const LEVEL_JAILLEV1 = 29 -const LEVEL_JAILLEV2 = 30 -const LEVEL_JAILLEV3 = 31 -const LEVEL_CATACOMBSLEV1 = 34 -const LEVEL_CATACOMBSLEV2 = 35 -const LEVEL_PALACECELLARLEV1 = 52 -const LEVEL_PALACECELLARLEV3 = 54 -const LEVEL_SPIDERCAVE = 84 -const LEVEL_SPIDERCAVERN = 85 - export interface D2MazeLevelIdStrc { nLevelPrestId1: number nLevelPrestId2: number @@ -296,6 +511,17 @@ export function DRLGMAZE_GetFreeLocationForRoomNorth(pLevel: D2DrlgLevelStrc): D return pResult } +// Helper function +export function DRLGMAZE_GetFreeLocationForRoomSouth(pLevel: D2DrlgLevelStrc): D2DrlgRoomStrc | null { + let pResult: D2DrlgRoomStrc | null = null + for (let pDrlgRoom = pLevel.pFirstRoomEx; pDrlgRoom; pDrlgRoom = pDrlgRoom.pDrlgRoomNext) { + if ((!pResult || pDrlgRoom.pDrlgCoord.nPosY < pResult.pDrlgCoord.nPosY) && DRLGMAZE_CheckIfMayPlaceAdjacentPresetRoom(pDrlgRoom, ALTDIR_NORTH)) { + pResult = pDrlgRoom + } + } + return pResult +} + //D2Common.0x6FD78F70 export function DRLGMAZE_PickRoomPreset(pDrlgRoom: D2DrlgRoomStrc, bResetFlag: boolean): void { const nHardcodedPresetsRemapping: readonly (readonly [number, number, number])[] = [ @@ -563,6 +789,13 @@ export function DRLGMAZE_GenerateLevel(pLevel: D2DrlgLevelStrc): void { { nLevelPrestId1: LVLPREST_ACT1_CATACOMBS_W, nLevelPrestId2: LVLPREST_ACT1_CATACOMBS_WAYPOINT_W, nPickedFile: -1, nDirection: 2 }, ] + const nAct2ArcaneSummonerIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT2_ARCANE_N, nLevelPrestId2: LVLPREST_ACT2_ARCANE_SUMMONER_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT2_ARCANE_E, nLevelPrestId2: LVLPREST_ACT2_ARCANE_SUMMONER_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT2_ARCANE_S, nLevelPrestId2: LVLPREST_ACT2_ARCANE_SUMMONER_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT2_ARCANE_W, nLevelPrestId2: LVLPREST_ACT2_ARCANE_SUMMONER_W, nPickedFile: -1, nDirection: 2 }, + ] + const pDrlgRoom = DRLGROOM_AllocRoomEx(pLevel, DRLGTYPE_PRESET) pDrlgRoom.pDrlgCoord.nWidth = pDrlgRoom.pLevel.pMaze!.dwSizeX pDrlgRoom.pDrlgCoord.nHeight = pDrlgRoom.pLevel.pMaze!.dwSizeY @@ -632,7 +865,7 @@ export function DRLGMAZE_GenerateLevel(pLevel: D2DrlgLevelStrc): void { nRand = DRLGMAZE_ScanReplaceSpecialPreset(pLevel, nAct1CryptSpecialIds[nRand]!, nRand) } - if (pLevel.nLevelId === LEVEL_MAUSOLEUM) { + if (pLevel.nLevelId === LEVEL_MAUSOLEUM || pLevel.nLevelId === LEVEL_PANDEMONIUMRUN1) { nRand = DRLGMAZE_ScanReplaceSpecialPreset(pLevel, nAct1CryptChestIds[nRand]!, nRand) } @@ -732,12 +965,138 @@ export function DRLGMAZE_GenerateLevel(pLevel: D2DrlgLevelStrc): void { } break + case LVLTYPE_ACT2_SEWER: + DRLGMAZE_InitBasicMazeLayout(pLevel, 2) + + nRooms = pLevel.pMaze!.dwRooms[pLevel.pDrlg.nDifficulty]! + if (pLevel.nLevelId === pLevel.pDrlg.nStaffTombLevel) { + nRooms *= 3 + } else if (pLevel.nLevelId === pLevel.pDrlg.nBossTombLevel) { + nRooms *= 2 + } + + while (pLevel.nRooms < nRooms) { + const pRandomRoomEx = DRLGMAZE_GetRandomRoomExFromLevel(pLevel) + const nDirection = SEED_RollRandomNumber(pRandomRoomEx.pSeed) & 3 + if (!DRLGMAZE_HasMapDS1(pRandomRoomEx)) { + DRLGMAZE_AddAdjacentMazeRoom(pRandomRoomEx, nDirection, true) + } + } + + DRLGMAZE_ScanReplaceSpecialAct2SewersPresets(pLevel) + break + + case LVLTYPE_ACT2_TOMB: + if (pLevel.nLevelId === LEVEL_CLAWVIPERTEMPLELEV2) { + pDrlgRoom.pMaze!.nLevelPrest = LVLPREST_ACT2_TOMB_TAINTED_SUN_X + pDrlgRoom.pMaze!.nPickedFile = -1 + pDrlgRoom.pMaze!.dwFlags |= DRLGPRESETROOMFLAG_HAS_MAP_DS1 + } else { + DRLGMAZE_PlaceAct2TombPrev_Act5BaalPrev(pLevel) + DRLGMAZE_BuildBasicMaze(pLevel) + DRLGMAZE_PlaceAct2TombStuff(pLevel) + } + break + + case LVLTYPE_ACT2_LAIR: + DRLGMAZE_InitBasicMazeLayout(pLevel, 2) + DRLGMAZE_BuildBasicMaze(pLevel) + DRLGMAZE_PlaceAct2LairStuff(pLevel) + break + + case LVLTYPE_ACT2_ARCANE: + DRLGMAZE_PlaceArcaneSanctuary(pLevel) + + nRand = SEED_RollRandomNumber(pLevel.pSeed) & 3 + nRand = DRLGMAZE_ScanReplaceSpecialPreset(pLevel, nAct2ArcaneSummonerIds[nRand]!, nRand) + break + + case LVLTYPE_ACT2_HAREM: + case LVLTYPE_ACT2_BASEMENT: + case LVLTYPE_ACT3_SPIDER: + DRLGMAZE_InitBasicMazeLayout(pLevel, 2) + break + + case LVLTYPE_ACT3_KURAST: + DRLGMAZE_InitBasicMazeLayout(pLevel, 2) + DRLGMAZE_BuildBasicMaze(pLevel) + DRLGMAZE_PlaceAct3MephistoStuff(pLevel) + break + + case LVLTYPE_ACT3_DUNGEON: + DRLGMAZE_InitBasicMazeLayout(pLevel, 2) + DRLGMAZE_BuildBasicMaze(pLevel) + DRLGMAZE_PlaceAct3DungeonStuff(pLevel) + break + + case LVLTYPE_ACT3_SEWER: + if (pLevel.nLevelId === LEVEL_SEWERSA3LEV1) { + DRLGMAZE_InitBasicMazeLayout(pLevel, 5) + DRLGMAZE_ReplaceRoomPreset(pLevel, LVLPREST_ACT3_SEWER_SW, LVLPREST_ACT3_SEWER_PREV_SW, -1, false) + DRLGMAZE_ReplaceRoomPreset(pLevel, LVLPREST_ACT3_SEWER_SE, LVLPREST_ACT3_SEWER_PREV_SE, -1, false) + DRLGMAZE_ReplaceRoomPreset(pLevel, LVLPREST_ACT3_SEWER_NW, LVLPREST_ACT3_SEWER_PREV_NW, -1, false) + DRLGMAZE_ReplaceRoomPreset(pLevel, LVLPREST_ACT3_SEWER_NE, LVLPREST_ACT3_SEWER_PREV_NE, -1, false) + } else { + DRLGMAZE_InitBasicMazeLayout(pLevel, 2) + } + + DRLGMAZE_BuildBasicMaze(pLevel) + DRLGMAZE_PlaceAct3SewerStuff(pLevel) + break + + case LVLTYPE_ACT4_LAVA: + DRLGMAZE_BuildBasicMaze(pLevel) + break + + case LVLTYPE_ACT5_ICE_CAVES: + switch (pLevel.nLevelId) { + case LEVEL_CELLAROFPITY: + if (SEED_RollLimitedRandomNumber(pDrlgRoom.pLevel.pSeed, 2) !== 0) { + DRLGMAZE_SetPickedFileAndPresetId(pLevelFirstRoomEx, LVLPREST_ACT5_ICE_RIVER_A, -1, false) + } else { + DRLGMAZE_SetPickedFileAndPresetId(pLevelFirstRoomEx, LVLPREST_ACT5_ICE_RIVER_B, -1, false) + } + break + + case LEVEL_ECHOCHAMBER: + DRLGMAZE_SetPickedFileAndPresetId(pLevelFirstRoomEx, LVLPREST_ACT5_ICE_POOL_A, -1, false) + break + + case LEVEL_GLACIALCAVESLEV2: + DRLGMAZE_SetPickedFileAndPresetId(pLevelFirstRoomEx, LVLPREST_ACT5_ICE_POOL_B, -1, false) + break + + default: + DRLGMAZE_InitBasicMazeLayout(pLevel, 2) + DRLGMAZE_BuildBasicMaze(pLevel) + DRLGMAZE_PlaceAct5IceStuff(pLevel) + break + } + break + + case LVLTYPE_ACT5_TEMPLE: + DRLGMAZE_InitBasicMazeLayout(pLevel, 2) + DRLGMAZE_PlaceAct5TempleStuff(pLevel) + break + + case LVLTYPE_ACT5_BAAL: + DRLGMAZE_PlaceAct2TombPrev_Act5BaalPrev(pLevel) + DRLGMAZE_BuildBasicMaze(pLevel) + DRLGMAZE_PlaceAct5BaalStuff(pLevel) + break + + case LVLTYPE_ACT5_LAVA: + DRLGMAZE_PlaceAct5LavaPresets(pLevel) + break + default: - throw new Error(`MazeLevelGenerate(${pLevel.nLevelId}, type=${pLevel.nLevelType}) - not ported`) + throw new Error(`MazeLevelGenerate(${pLevel.nLevelId}, type=${pLevel.nLevelType}) - Some really bad voodoo here!`) } if (pLevel.nLevelId === LEVEL_BARRACKS) { DRLGMAZE_PlaceAct1Barracks(pLevel) + } else if (pLevel.nLevelId === LEVEL_RIVEROFFLAME) { + DRLGMAZE_PlaceAct4Lava(pLevel) } else { DRLG_UpdateRoomExCoordinates(pLevel) } @@ -1043,6 +1402,252 @@ export function DRLGMAZE_GetRandomRoomExFromLevel(pLevel: D2DrlgLevelStrc): D2Dr return pDrlgRoom } +//D2Common.0x6FD7A5D0 +export function DRLGMAZE_BuildBasicMaze(pLevel: D2DrlgLevelStrc): void { + let nRooms = pLevel.pMaze!.dwRooms[pLevel.pDrlg.nDifficulty]! + if (pLevel.nLevelId === pLevel.pDrlg.nStaffTombLevel) { + nRooms *= 3 + } + if (pLevel.nLevelId === pLevel.pDrlg.nBossTombLevel) { + nRooms *= 2 + } + + for (let i = pLevel.nRooms; i < nRooms; i = pLevel.nRooms) { + const pRandomRoomEx = DRLGMAZE_GetRandomRoomExFromLevel(pLevel) + const nDirection = SEED_RollRandomNumber(pRandomRoomEx.pSeed) & 3 + + if (!DRLGMAZE_HasMapDS1(pRandomRoomEx)) { + const pNewRoomEx = DRLGROOM_AllocRoomEx(pRandomRoomEx.pLevel, DRLGTYPE_PRESET) + pNewRoomEx.pDrlgCoord.nWidth = pNewRoomEx.pLevel.pMaze!.dwSizeX + pNewRoomEx.pDrlgCoord.nHeight = pNewRoomEx.pLevel.pMaze!.dwSizeY + + switch (nDirection) { + case 0: + pNewRoomEx.pDrlgCoord.nPosX = pRandomRoomEx.pDrlgCoord.nPosX - pRandomRoomEx.pDrlgCoord.nWidth + pNewRoomEx.pDrlgCoord.nPosY = pRandomRoomEx.pDrlgCoord.nPosY + break + case 1: + pNewRoomEx.pDrlgCoord.nPosX = pRandomRoomEx.pDrlgCoord.nPosX + pNewRoomEx.pDrlgCoord.nPosY = pRandomRoomEx.pDrlgCoord.nPosY - pRandomRoomEx.pDrlgCoord.nHeight + break + case 2: + pNewRoomEx.pDrlgCoord.nPosX = pRandomRoomEx.pDrlgCoord.nPosX + pRandomRoomEx.pDrlgCoord.nWidth + pNewRoomEx.pDrlgCoord.nPosY = pRandomRoomEx.pDrlgCoord.nPosY + break + case 3: + pNewRoomEx.pDrlgCoord.nPosX = pRandomRoomEx.pDrlgCoord.nPosX + pNewRoomEx.pDrlgCoord.nPosY = pRandomRoomEx.pDrlgCoord.nPosY + pRandomRoomEx.pDrlgCoord.nHeight + break + case 4: + pNewRoomEx.pDrlgCoord.nPosX = pRandomRoomEx.pDrlgCoord.nPosX - pRandomRoomEx.pDrlgCoord.nWidth + pNewRoomEx.pDrlgCoord.nPosY = pRandomRoomEx.pDrlgCoord.nPosY - pRandomRoomEx.pDrlgCoord.nHeight + break + case 5: + pNewRoomEx.pDrlgCoord.nPosX = pRandomRoomEx.pDrlgCoord.nPosX + pRandomRoomEx.pDrlgCoord.nWidth + pNewRoomEx.pDrlgCoord.nPosY = pRandomRoomEx.pDrlgCoord.nPosY - pRandomRoomEx.pDrlgCoord.nHeight + break + case 6: + pNewRoomEx.pDrlgCoord.nPosX = pRandomRoomEx.pDrlgCoord.nPosX + pRandomRoomEx.pDrlgCoord.nWidth + pNewRoomEx.pDrlgCoord.nPosY = pRandomRoomEx.pDrlgCoord.nPosY + pRandomRoomEx.pDrlgCoord.nHeight + break + case 7: + pNewRoomEx.pDrlgCoord.nPosX = pRandomRoomEx.pDrlgCoord.nPosX - pRandomRoomEx.pDrlgCoord.nWidth + pNewRoomEx.pDrlgCoord.nPosY = pRandomRoomEx.pDrlgCoord.nPosY + pRandomRoomEx.pDrlgCoord.nHeight + break + default: + break + } + + let pDrlgOrth = pRandomRoomEx.pDrlgOrth + while (pDrlgOrth && DRLG_CheckNotOverlappingUsingManhattanDistance(pNewRoomEx.pDrlgCoord, pDrlgOrth.pBox!, 0)) { + pDrlgOrth = pDrlgOrth.pNext + } + + if (!pDrlgOrth) { + if (DRLGMAZE_CheckRoomNotOverlaping(pNewRoomEx.pLevel, pNewRoomEx, pRandomRoomEx, 0)) { + DRLGROOM_AllocDrlgOrthsForRooms(pRandomRoomEx, pNewRoomEx, nDirection) + DRLGMAZE_MergeMazeRooms(pNewRoomEx) + DRLGROOM_AddRoomExToLevel(pRandomRoomEx.pLevel, pNewRoomEx) + DRLGMAZE_PickRoomPreset(pRandomRoomEx, true) + DRLGMAZE_PickRoomPreset(pNewRoomEx, true) + } else { + DRLGROOM_FreeRoomEx(pNewRoomEx) + } + } else { + DRLGROOM_FreeRoomEx(pNewRoomEx) + } + } + } +} + +const dword_6FDCE850: readonly (readonly [number, number, number])[] = [ + [LVLPREST_ACT5_LAVA_S, 1, 0], + [LVLPREST_ACT5_LAVA_N, 3, 1], + [LVLPREST_ACT5_LAVA_S, 1, 1], + [LVLPREST_ACT5_LAVA_N, 3, 0], + [LVLPREST_ACT5_LAVA_E, 0, 1], + [LVLPREST_ACT5_LAVA_W, 2, 0], + [LVLPREST_ACT5_LAVA_E, 0, 0], + [LVLPREST_ACT5_LAVA_W, 2, 1], +] + +function PlaceLavaPreset(pFirstRoomEx: D2DrlgRoomStrc, nSet: number): void { + const nDirection = dword_6FDCE850[nSet]![1] + + const pNewRoomEx = DRLGROOM_AllocRoomEx(pFirstRoomEx.pLevel, DRLGTYPE_PRESET) + pNewRoomEx.pDrlgCoord.nWidth = pNewRoomEx.pLevel.pMaze!.dwSizeX + pNewRoomEx.pDrlgCoord.nHeight = pNewRoomEx.pLevel.pMaze!.dwSizeY + + if (DRLGMAZE_LinkMazeRooms(pNewRoomEx, pFirstRoomEx, nDirection)) { + DRLGROOM_AllocDrlgOrthsForRooms(pFirstRoomEx, pNewRoomEx, nDirection) + DRLGROOM_AddRoomExToLevel(pFirstRoomEx.pLevel, pNewRoomEx) + DRLGMAZE_PickRoomPreset(pFirstRoomEx, true) + + pNewRoomEx.pMaze!.nLevelPrest = dword_6FDCE850[nSet]![0] + pNewRoomEx.pMaze!.nPickedFile = dword_6FDCE850[nSet]![2] + pNewRoomEx.pMaze!.dwFlags |= DRLGPRESETROOMFLAG_HAS_MAP_DS1 + } else { + DRLGROOM_FreeRoomEx(pNewRoomEx) + } +} + +//D2Common.0x6FD7A830 +export function DRLGMAZE_PlaceAct5LavaPresets(pLevel: D2DrlgLevelStrc): void { + const nSet = 2 * (SEED_RollRandomNumber(pLevel.pFirstRoomEx!.pSeed) & 3) + const pLevelFirstRoom = pLevel.pFirstRoomEx! + PlaceLavaPreset(pLevelFirstRoom, nSet) + PlaceLavaPreset(pLevelFirstRoom, nSet + 1) + DRLGMAZE_FillBlankMazeSpaces(pLevel, LVLPREST_ACT4_LAVA_X, null) +} + +//D2Common.0x6FD7A9B0 +export function DRLGMAZE_FillBlankMazeSpaces(pLevel: D2DrlgLevelStrc, nLevelPrest: number, pIgnoreRoomEx: D2DrlgRoomStrc | null): void { + const nRooms = pLevel.nRooms + if (nRooms > 0) { + const pDrlgRoomArray: D2DrlgRoomStrc[] = [] + let pTemp = pLevel.pFirstRoomEx + for (let i = 0; i < nRooms; i += 1) { + pDrlgRoomArray.push(pTemp!) + pTemp = pTemp!.pDrlgRoomNext + } + + for (let i = 0; i < nRooms; i += 1) { + const pCurrentRoomEx = pDrlgRoomArray[i]! + if (pCurrentRoomEx !== pIgnoreRoomEx) { + for (let j = 0; j < 8; j += 1) { + const pNewRoomEx = DRLGROOM_AllocRoomEx(pCurrentRoomEx.pLevel, DRLGTYPE_PRESET) + pNewRoomEx.pDrlgCoord.nWidth = pNewRoomEx.pLevel.pMaze!.dwSizeX + pNewRoomEx.pDrlgCoord.nHeight = pNewRoomEx.pLevel.pMaze!.dwSizeY + + if (DRLGMAZE_LinkMazeRooms(pNewRoomEx, pCurrentRoomEx, j)) { + DRLGROOM_AllocDrlgOrthsForRooms(pCurrentRoomEx, pNewRoomEx, j) + DRLGROOM_AddRoomExToLevel(pCurrentRoomEx.pLevel, pNewRoomEx) + DRLGMAZE_SetPickedFileAndPresetId(pNewRoomEx, nLevelPrest, -1, false) + } else { + DRLGROOM_FreeRoomEx(pNewRoomEx) + } + } + } + } + } +} + +//D2Common.0x6FD7AAC0 +export function DRLGMAZE_PlaceAct2TombPrev_Act5BaalPrev(pLevel: D2DrlgLevelStrc): void { + const dword_6FDCE8B4: readonly (readonly [number, number])[] = [ + [LVLPREST_ACT2_TOMB_PREV_NSE, LVLPREST_ACT5_BAAL_PREV_NSE], + [LVLPREST_ACT2_TOMB_PREV_SEW, LVLPREST_ACT5_BAAL_PREV_SEW], + [LVLPREST_ACT2_TOMB_PREV_NSW, LVLPREST_ACT5_BAAL_PREV_NSW], + [LVLPREST_ACT2_TOMB_PREV_NEW, LVLPREST_ACT5_BAAL_PREV_NEW], + ] + + const pLevelFirstRoomEx = pLevel.pFirstRoomEx! + let nDirection = SEED_RollRandomNumber(pLevel.pSeed) & 3 + for (let i = 0; i < 3; i += 1) { + const pDrlgRoom = DRLGROOM_AllocRoomEx(pLevelFirstRoomEx.pLevel, DRLGTYPE_PRESET) + DRLGMAZE_SetRoomSize(pDrlgRoom) + + if (DRLGMAZE_LinkMazeRooms(pDrlgRoom, pLevelFirstRoomEx, nDirection)) { + DRLGROOM_AllocDrlgOrthsForRooms(pLevelFirstRoomEx, pDrlgRoom, nDirection) + DRLGMAZE_MergeMazeRooms(pDrlgRoom) + DRLGROOM_AddRoomExToLevel(pLevelFirstRoomEx.pLevel, pDrlgRoom) + DRLGMAZE_PickRoomPreset(pLevelFirstRoomEx, true) + DRLGMAZE_PickRoomPreset(pDrlgRoom, true) + } else { + DRLGROOM_FreeRoomEx(pDrlgRoom) + } + + nDirection = (nDirection + 1) % 4 + } + + if (pLevel.nLevelType === LVLTYPE_ACT2_TOMB) { + pLevelFirstRoomEx.pMaze!.nLevelPrest = dword_6FDCE8B4[nDirection]![0] + pLevelFirstRoomEx.pMaze!.nPickedFile = -1 + pLevelFirstRoomEx.pMaze!.dwFlags |= DRLGPRESETROOMFLAG_HAS_MAP_DS1 + } else if (pLevel.nLevelType === LVLTYPE_ACT5_BAAL) { + pLevelFirstRoomEx.pMaze!.nLevelPrest = dword_6FDCE8B4[nDirection]![1] + pLevelFirstRoomEx.pMaze!.nPickedFile = -1 + pLevelFirstRoomEx.pMaze!.dwFlags |= DRLGPRESETROOMFLAG_HAS_MAP_DS1 + } +} + +function DRLGMAZE_PlaceRoomForArcaneBranch(pParentRoom: D2DrlgRoomStrc, nArcaneMapBranch: number): D2DrlgRoomStrc | null { + const pDrlgRoom = DRLGMAZE_PlaceAdjacentPresetRoom(pParentRoom, nArcaneMapBranch % 4, true) + if (pDrlgRoom) { + DRLGMAZE_PickRoomPreset(pParentRoom, true) + DRLGMAZE_PickRoomPreset(pDrlgRoom, true) + return pDrlgRoom + } + return null +} + +function DRLGMAZE_ArcaneSanctuaryDirectionFromRoomIdx(nBranchDirection: number, nBranchRoomIdx: number): number { + switch (nBranchRoomIdx) { + default: + return nBranchDirection + case 2: + case 12: + return nBranchDirection + 3 + case 7: + case 9: + return nBranchDirection + 1 + case 10: + case 11: + case 13: + case 14: + return nBranchDirection + 2 + } +} + +//D2Common.0x6FD7ABC0 +export function DRLGMAZE_PlaceArcaneSanctuary(pLevel: D2DrlgLevelStrc): void { + const pLevelFirstRoomEx = pLevel.pFirstRoomEx! + const nRand = SEED_RollRandomNumber(pLevel.pSeed) & 3 + const nRoomsPerBranch = 15 + const nBranchDirections = 4 + const pDrlgRoomArray: (D2DrlgRoomStrc | null)[] = new Array(nRoomsPerBranch * nBranchDirections).fill(null) + + for (let nBranchDirection = 0; nBranchDirection < nBranchDirections; nBranchDirection += 1) { + let pParentRoom: D2DrlgRoomStrc | null = pLevelFirstRoomEx + for (let nBranchRoomIdx = 0; nBranchRoomIdx < nRoomsPerBranch; nBranchRoomIdx += 1) { + const nRoomDirection = DRLGMAZE_ArcaneSanctuaryDirectionFromRoomIdx(nBranchDirection, nBranchRoomIdx) + const pNewRoom = DRLGMAZE_PlaceRoomForArcaneBranch(pParentRoom!, nRoomDirection) + if (nBranchRoomIdx !== 8 && nBranchRoomIdx !== 12) { + pDrlgRoomArray[nBranchRoomIdx + nBranchDirection * nRoomsPerBranch] = pNewRoom + pParentRoom = pNewRoom + } + } + } + + for (let i = 0; i < pDrlgRoomArray.length; i += 1) { + if (pDrlgRoomArray[i]) { + pDrlgRoomArray[i]!.pMaze!.nPickedFile = (nRand + Math.trunc(i / 15)) % 4 + } + } + + pLevelFirstRoomEx.pMaze!.nPickedFile = 4 +} + //D2Common.0x6FD7AFD0 export function DRLGMAZE_PlaceAdjacentPresetRoom( pParentRoomEx: D2DrlgRoomStrc, @@ -1145,6 +1750,137 @@ export function DRLGMAZE_ScanReplaceSpecialPreset( return nRand !== null ? (nRand + 1) % 4 : 0 } +//D2Common.0x6FD7B330 +export function DRLGMAZE_ScanReplaceSpecialAct2SewersPresets(pLevel: D2DrlgLevelStrc): void { + const nAct2SewerPrevIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT2_SEWER_N, nLevelPrestId2: LVLPREST_ACT2_SEWER_PREV_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT2_SEWER_E, nLevelPrestId2: LVLPREST_ACT2_SEWER_PREV_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT2_SEWER_S, nLevelPrestId2: LVLPREST_ACT2_SEWER_PREV_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT2_SEWER_W, nLevelPrestId2: LVLPREST_ACT2_SEWER_PREV_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nAct2SewerNextIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT2_SEWER_N, nLevelPrestId2: LVLPREST_ACT2_SEWER_NEXT_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT2_SEWER_E, nLevelPrestId2: LVLPREST_ACT2_SEWER_NEXT_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT2_SEWER_S, nLevelPrestId2: LVLPREST_ACT2_SEWER_NEXT_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT2_SEWER_W, nLevelPrestId2: LVLPREST_ACT2_SEWER_NEXT_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nAct2SewerWaypointIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT2_SEWER_N, nLevelPrestId2: LVLPREST_ACT2_SEWER_WAYPOINT_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT2_SEWER_E, nLevelPrestId2: LVLPREST_ACT2_SEWER_WAYPOINT_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT2_SEWER_S, nLevelPrestId2: LVLPREST_ACT2_SEWER_WAYPOINT_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT2_SEWER_W, nLevelPrestId2: LVLPREST_ACT2_SEWER_WAYPOINT_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nAct2SewerRadamentIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT2_SEWER_N, nLevelPrestId2: LVLPREST_ACT2_SEWER_RADAMENTS_LAIR_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT2_SEWER_E, nLevelPrestId2: LVLPREST_ACT2_SEWER_RADAMENTS_LAIR_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT2_SEWER_S, nLevelPrestId2: LVLPREST_ACT2_SEWER_RADAMENTS_LAIR_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT2_SEWER_W, nLevelPrestId2: LVLPREST_ACT2_SEWER_RADAMENTS_LAIR_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nAct2SewerChestIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT2_SEWER_N, nLevelPrestId2: LVLPREST_ACT2_SEWER_CHEST_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT2_SEWER_E, nLevelPrestId2: LVLPREST_ACT2_SEWER_CHEST_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT2_SEWER_S, nLevelPrestId2: LVLPREST_ACT2_SEWER_CHEST_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT2_SEWER_W, nLevelPrestId2: LVLPREST_ACT2_SEWER_CHEST_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nDirection = (2 * (SEED_RollRandomNumber(pLevel.pSeed) & 1)) | 1 + let nRand = SEED_RollRandomNumber(pLevel.pSeed) & 3 + + switch (pLevel.nLevelId) { + case LEVEL_SEWERSLEV1: { + let pDrlgRoom1: D2DrlgRoomStrc | null = null + for (let i = pLevel.pFirstRoomEx; i; i = i.pDrlgRoomNext) { + if ((!pDrlgRoom1 || i.pDrlgCoord.nPosY < pDrlgRoom1.pDrlgCoord.nPosY) && DRLGMAZE_CheckIfMayPlaceAdjacentPresetRoom(i, ALTDIR_NORTH)) { + pDrlgRoom1 = i + } + } + + let pDrlgRoom2 = DRLGMAZE_PlaceAdjacentPresetRoom(pDrlgRoom1!, ALTDIR_NORTH, true) + if (pDrlgRoom2) { + DRLGMAZE_PickRoomPreset(pDrlgRoom1!, true) + DRLGMAZE_PickRoomPreset(pDrlgRoom2, true) + } + + let pDrlgRoom3 = DRLGMAZE_PlaceAdjacentPresetRoom(pDrlgRoom2!, ALTDIR_NORTH, true) + if (pDrlgRoom3) { + DRLGMAZE_PickRoomPreset(pDrlgRoom2!, true) + DRLGMAZE_PickRoomPreset(pDrlgRoom3, true) + } + + let pDrlgRoom4 = DRLGMAZE_PlaceAdjacentPresetRoom(pDrlgRoom3!, ALTDIR_WEST, false) + if (pDrlgRoom4) { + DRLGMAZE_PickRoomPreset(pDrlgRoom3!, true) + DRLGMAZE_SetPickedFileAndPresetId(pDrlgRoom4, LVLPREST_ACT2_SEWER_PREV_E, 0, false) + } + + pDrlgRoom1 = null + for (let i = pLevel.pFirstRoomEx; i; i = i.pDrlgRoomNext) { + if ((!pDrlgRoom1 || i.pDrlgCoord.nPosX > pDrlgRoom1.pDrlgCoord.nPosX) && DRLGMAZE_CheckIfMayPlaceAdjacentPresetRoom(i, ALTDIR_EAST)) { + pDrlgRoom1 = i + } + } + + pDrlgRoom2 = DRLGMAZE_PlaceAdjacentPresetRoom(pDrlgRoom1!, ALTDIR_EAST, true) + if (pDrlgRoom2) { + DRLGMAZE_PickRoomPreset(pDrlgRoom1!, true) + DRLGMAZE_PickRoomPreset(pDrlgRoom2, true) + } + + pDrlgRoom3 = DRLGMAZE_PlaceAdjacentPresetRoom(pDrlgRoom2!, ALTDIR_EAST, true) + if (pDrlgRoom3) { + DRLGMAZE_PickRoomPreset(pDrlgRoom2!, true) + DRLGMAZE_PickRoomPreset(pDrlgRoom3, true) + } + + pDrlgRoom4 = DRLGMAZE_PlaceAdjacentPresetRoom(pDrlgRoom3!, nDirection, false) + if (pDrlgRoom4) { + DRLGMAZE_PickRoomPreset(pDrlgRoom3!, true) + DRLGMAZE_SetPickedFileAndPresetId(pDrlgRoom4, LVLPREST_ACT2_SEWER_PREV_NS, 0, false) + } + + const pDrlgRoom5 = DRLGMAZE_PlaceAdjacentPresetRoom(pDrlgRoom4!, nDirection, false) + if (pDrlgRoom5) { + DRLGMAZE_PickRoomPreset(pDrlgRoom5, true) + } + + const pMazeLevelIds = nAct2SewerNextIds[nRand]! + if (!DRLGMAZE_ReplaceRoomPreset(pLevel, pMazeLevelIds.nLevelPrestId1, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false)) { + DRLGMAZE_AddSpecialPreset(pLevel, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile) + } + break + } + + case LEVEL_SEWERSLEV2: { + const pMazeLevelIds = nAct2SewerPrevIds[nRand]! + if (!DRLGMAZE_ReplaceRoomPreset(pLevel, pMazeLevelIds.nLevelPrestId1, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false)) { + DRLGMAZE_AddSpecialPreset(pLevel, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile) + } + + nRand = (nRand + 1) % 4 + nRand = DRLGMAZE_ScanReplaceSpecialPreset(pLevel, nAct2SewerWaypointIds[nRand]!, nRand) + nRand = DRLGMAZE_ScanReplaceSpecialPreset(pLevel, nAct2SewerNextIds[nRand]!, nRand) + break + } + + case LEVEL_SEWERSLEV3: + nRand = DRLGMAZE_ScanReplaceSpecialPreset(pLevel, nAct2SewerPrevIds[nRand]!, nRand) + nRand = DRLGMAZE_ScanReplaceSpecialPreset(pLevel, nAct2SewerRadamentIds[nRand]!, nRand) + break + + case LEVEL_ANCIENTTUNNELS: + nRand = DRLGMAZE_ScanReplaceSpecialPreset(pLevel, nAct2SewerPrevIds[nRand]!, nRand) + nRand = DRLGMAZE_ScanReplaceSpecialPreset(pLevel, nAct2SewerChestIds[nRand]!, nRand) + break + + default: + return + } +} + //D2Common.0x6FD7B660 export function DRLGMAZE_AddSpecialPreset(pLevel: D2DrlgLevelStrc, nDirection: number, nLvlPrestId: number, nFile: number): void { for (let pDrlgRoom = pLevel.pFirstRoomEx; pDrlgRoom; pDrlgRoom = pDrlgRoom.pDrlgRoomNext) { @@ -1170,6 +1906,163 @@ export function DRLGMAZE_CheckIfMayPlaceAdjacentPresetRoom(pDrlgRoom: D2DrlgRoom return pNewRoomEx !== null } +//D2Common.0x6FD7B8B0 +export function DRLGMAZE_PlaceAct2TombStuff(pLevel: D2DrlgLevelStrc): void { + const nAct2TombNextIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT2_TOMB_N, nLevelPrestId2: LVLPREST_ACT2_TOMB_NEXT_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_E, nLevelPrestId2: LVLPREST_ACT2_TOMB_NEXT_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_S, nLevelPrestId2: LVLPREST_ACT2_TOMB_NEXT_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_W, nLevelPrestId2: LVLPREST_ACT2_TOMB_NEXT_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nAct2TombWaypointIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT2_TOMB_N, nLevelPrestId2: LVLPREST_ACT2_TOMB_WAYPOINT_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_E, nLevelPrestId2: LVLPREST_ACT2_TOMB_WAYPOINT_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_S, nLevelPrestId2: LVLPREST_ACT2_TOMB_WAYPOINT_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_W, nLevelPrestId2: LVLPREST_ACT2_TOMB_WAYPOINT_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nAct2TombChestIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT2_TOMB_N, nLevelPrestId2: LVLPREST_ACT2_TOMB_CHEST_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_E, nLevelPrestId2: LVLPREST_ACT2_TOMB_CHEST_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_S, nLevelPrestId2: LVLPREST_ACT2_TOMB_CHEST_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_W, nLevelPrestId2: LVLPREST_ACT2_TOMB_CHEST_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nAct2TombLeatherarmIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT2_TOMB_N, nLevelPrestId2: LVLPREST_ACT2_TOMB_LEATHERARM_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_E, nLevelPrestId2: LVLPREST_ACT2_TOMB_LEATHERARM_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_S, nLevelPrestId2: LVLPREST_ACT2_TOMB_LEATHERARM_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_W, nLevelPrestId2: LVLPREST_ACT2_TOMB_LEATHERARM_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nAct2TombCubeIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT2_TOMB_N, nLevelPrestId2: LVLPREST_ACT2_TOMB_CUBE_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_E, nLevelPrestId2: LVLPREST_ACT2_TOMB_CUBE_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_S, nLevelPrestId2: LVLPREST_ACT2_TOMB_CUBE_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_W, nLevelPrestId2: LVLPREST_ACT2_TOMB_CUBE_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nAct2TombTreasureIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT2_TOMB_N, nLevelPrestId2: LVLPREST_ACT2_TOMB_TREASURE_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_E, nLevelPrestId2: LVLPREST_ACT2_TOMB_TREASURE_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_S, nLevelPrestId2: LVLPREST_ACT2_TOMB_TREASURE_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_W, nLevelPrestId2: LVLPREST_ACT2_TOMB_TREASURE_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nAct2TombTalRashaIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT2_TOMB_N, nLevelPrestId2: LVLPREST_ACT2_TOMB_TALRASHA_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_E, nLevelPrestId2: LVLPREST_ACT2_TOMB_TALRASHA_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_S, nLevelPrestId2: LVLPREST_ACT2_TOMB_TALRASHA_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_W, nLevelPrestId2: LVLPREST_ACT2_TOMB_TALRASHA_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nAct2TombKaaIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT2_TOMB_N, nLevelPrestId2: LVLPREST_ACT2_TOMB_KAA_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_E, nLevelPrestId2: LVLPREST_ACT2_TOMB_KAA_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_S, nLevelPrestId2: LVLPREST_ACT2_TOMB_KAA_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT2_TOMB_W, nLevelPrestId2: LVLPREST_ACT2_TOMB_KAA_W, nPickedFile: -1, nDirection: 2 }, + ] + + let pPresetRoom = pLevel.pFirstRoomEx!.pMaze! + for (let i = pLevel.pFirstRoomEx; i; i = i.pDrlgRoomNext) { + pPresetRoom = i.pMaze! + if (pPresetRoom.nLevelPrest > 428) { + break + } + } + + let nDirection = 0 + switch (pPresetRoom.nLevelPrest) { + case LVLPREST_ACT2_TOMB_PREV_NSW: + nDirection = 0 + break + case LVLPREST_ACT2_TOMB_PREV_NEW: + nDirection = 1 + break + case LVLPREST_ACT2_TOMB_PREV_NSE: + nDirection = 2 + break + case LVLPREST_ACT2_TOMB_PREV_SEW: + nDirection = 3 + break + default: + throw new Error("sPlaceTombStuff() - Why isn't the first room the preset warp room?") + } + + if (pLevel.nLevelId >= LEVEL_STONYTOMBLEV1 && pLevel.nLevelId <= LEVEL_CLAWVIPERTEMPLELEV1) { + const pMazeLevelIds = nAct2TombNextIds[nDirection]! + if (!DRLGMAZE_ReplaceRoomPreset(pLevel, pMazeLevelIds.nLevelPrestId1, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false)) { + for (let i = pLevel.pFirstRoomEx; i; i = i.pDrlgRoomNext) { + if (!DRLGMAZE_HasMapDS1(i) && DRLGMAZE_InitRoomFixedPreset(i, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, true)) { + break + } + } + } + nDirection = (nDirection + 1) % 4 + } + + if (pLevel.nLevelId === LEVEL_HALLSOFTHEDEADLEV2) { + const pMazeLevelIds = nAct2TombWaypointIds[nDirection]! + if (!DRLGMAZE_ReplaceRoomPreset(pLevel, pMazeLevelIds.nLevelPrestId1, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false)) { + DRLGMAZE_AddSpecialPreset(pLevel, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile) + } + nDirection = (nDirection + 1) % 4 + } + + if (pLevel.nLevelId === LEVEL_STONYTOMBLEV2 || pLevel.nLevelId === LEVEL_CLAWVIPERTEMPLELEV2) { + const pMazeLevelIds = nAct2TombChestIds[nDirection]! + if (!DRLGMAZE_ReplaceRoomPreset(pLevel, pMazeLevelIds.nLevelPrestId1, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false)) { + DRLGMAZE_AddSpecialPreset(pLevel, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile) + } + nDirection = (nDirection + 1) % 4 + } + + if (pLevel.nLevelId >= LEVEL_TALRASHASTOMB1 && pLevel.nLevelId <= LEVEL_TALRASHASTOMB7 && pLevel.nLevelId !== pLevel.pDrlg.nStaffTombLevel) { + const pMazeLevelIds = nAct2TombChestIds[nDirection]! + if (!DRLGMAZE_ReplaceRoomPreset(pLevel, pMazeLevelIds.nLevelPrestId1, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false)) { + DRLGMAZE_AddSpecialPreset(pLevel, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile) + } + nDirection = (nDirection + 1) % 4 + } + + if (pLevel.nLevelId === LEVEL_STONYTOMBLEV2) { + const pMazeLevelIds = nAct2TombLeatherarmIds[nDirection]! + if (!DRLGMAZE_ReplaceRoomPreset(pLevel, pMazeLevelIds.nLevelPrestId1, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false)) { + DRLGMAZE_AddSpecialPreset(pLevel, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile) + } + nDirection = (nDirection + 1) % 4 + } + + if (pLevel.nLevelId === LEVEL_HALLSOFTHEDEADLEV3) { + const pMazeLevelIds = nAct2TombCubeIds[nDirection]! + if (!DRLGMAZE_ReplaceRoomPreset(pLevel, pMazeLevelIds.nLevelPrestId1, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false)) { + DRLGMAZE_AddSpecialPreset(pLevel, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile) + } + nDirection = (nDirection + 1) % 4 + } + + if (pLevel.nLevelId === LEVEL_STONYTOMBLEV2 || pLevel.nLevelId === LEVEL_CLAWVIPERTEMPLELEV2) { + const pMazeLevelIds = nAct2TombTreasureIds[nDirection]! + if (!DRLGMAZE_ReplaceRoomPreset(pLevel, pMazeLevelIds.nLevelPrestId1, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false)) { + DRLGMAZE_AddSpecialPreset(pLevel, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile) + } + nDirection = (nDirection + 1) % 4 + } + + if (pLevel.nLevelId === pLevel.pDrlg.nStaffTombLevel) { + const pMazeLevelIds = nAct2TombTalRashaIds[nDirection]! + if (!DRLGMAZE_ReplaceRoomPreset(pLevel, pMazeLevelIds.nLevelPrestId1, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false)) { + DRLGMAZE_AddSpecialPreset(pLevel, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile) + } + nDirection = (nDirection + 1) % 4 + } + + if (pLevel.nLevelId === pLevel.pDrlg.nBossTombLevel) { + nDirection = DRLGMAZE_ScanReplaceSpecialPreset(pLevel, nAct2TombKaaIds[nDirection]!, nDirection) + } +} + //D2Common.0x6FD7BC40 export function DRLGMAZE_InitRoomFixedPreset( pDrlgRoom: D2DrlgRoomStrc, @@ -1199,6 +2092,432 @@ export function DRLGMAZE_InitRoomFixedPreset( } } +//D2Common.0x6FD7BCD0 +export function DRLGMAZE_PlaceAct2LairStuff(pLevel: D2DrlgLevelStrc): void { + const nAct2LairPrevIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT2_LAIR_N, nLevelPrestId2: LVLPREST_ACT2_LAIR_PREV_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT2_LAIR_E, nLevelPrestId2: LVLPREST_ACT2_LAIR_PREV_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT2_LAIR_S, nLevelPrestId2: LVLPREST_ACT2_LAIR_PREV_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT2_LAIR_W, nLevelPrestId2: LVLPREST_ACT2_LAIR_PREV_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nAct2LairSpecialIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT2_LAIR_N, nLevelPrestId2: LVLPREST_ACT2_LAIR_NEXT_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT2_LAIR_E, nLevelPrestId2: LVLPREST_ACT2_LAIR_NEXT_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT2_LAIR_S, nLevelPrestId2: LVLPREST_ACT2_LAIR_NEXT_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT2_LAIR_W, nLevelPrestId2: LVLPREST_ACT2_LAIR_NEXT_W, nPickedFile: -1, nDirection: 2 }, + { nLevelPrestId1: LVLPREST_ACT2_LAIR_N, nLevelPrestId2: LVLPREST_ACT2_LAIR_TREASURE_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT2_LAIR_E, nLevelPrestId2: LVLPREST_ACT2_LAIR_TREASURE_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT2_LAIR_S, nLevelPrestId2: LVLPREST_ACT2_LAIR_TREASURE_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT2_LAIR_W, nLevelPrestId2: LVLPREST_ACT2_LAIR_TREASURE_W, nPickedFile: -1, nDirection: 2 }, + { nLevelPrestId1: LVLPREST_ACT2_LAIR_S, nLevelPrestId2: LVLPREST_ACT2_LAIR_TIGHT_SPOT_S, nPickedFile: -1, nDirection: 1 }, + ] + + const nRand = SEED_RollRandomNumber(pLevel.pSeed) & 3 + let nIndex = 0 + + if (pLevel.nLevelId === LEVEL_MAGGOTLAIRLEV3) { + let pDrlgRoom = pLevel.pFirstRoomEx + while (pDrlgRoom) { + if (!DRLGMAZE_HasMapDS1(pDrlgRoom) && pDrlgRoom.pMaze!.nLevelPrest === LVLPREST_ACT2_LAIR_S) { + DRLGMAZE_SetPickedFileAndPresetId(pDrlgRoom, LVLPREST_ACT2_LAIR_TIGHT_SPOT_S, -1, false) + break + } + pDrlgRoom = pDrlgRoom.pDrlgRoomNext + } + + if (!pDrlgRoom) { + pDrlgRoom = pLevel.pFirstRoomEx + while (pDrlgRoom) { + if (!DRLGMAZE_HasMapDS1(pDrlgRoom) && DRLGMAZE_InitRoomFixedPreset(pDrlgRoom, 1, LVLPREST_ACT2_LAIR_TIGHT_SPOT_S, -1, true)) { + break + } + pDrlgRoom = pDrlgRoom.pDrlgRoomNext + } + } + + pDrlgRoom = pLevel.pFirstRoomEx + while (pDrlgRoom) { + if (!DRLGMAZE_HasMapDS1(pDrlgRoom) && pDrlgRoom.pMaze!.nLevelPrest === LVLPREST_ACT2_LAIR_W) { + DRLGMAZE_SetPickedFileAndPresetId(pDrlgRoom, LVLPREST_ACT2_LAIR_TREASURE_W, -1, false) + break + } + pDrlgRoom = pDrlgRoom.pDrlgRoomNext + } + + if (!pDrlgRoom) { + DRLGMAZE_AddSpecialPreset(pLevel, 2, LVLPREST_ACT2_LAIR_TREASURE_W, -1) + } + + nIndex = 0 + } else { + const pMazeLevelIds = nAct2LairSpecialIds[nRand]! + let pDrlgRoom = pLevel.pFirstRoomEx + while (pDrlgRoom) { + if (!DRLGMAZE_HasMapDS1(pDrlgRoom) && pDrlgRoom.pMaze!.nLevelPrest === pMazeLevelIds.nLevelPrestId1) { + DRLGMAZE_SetPickedFileAndPresetId(pDrlgRoom, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false) + break + } + pDrlgRoom = pDrlgRoom.pDrlgRoomNext + } + + if (!pDrlgRoom) { + DRLGMAZE_AddSpecialPreset(pLevel, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile) + } + + nIndex = (nRand + 1) % 4 + } + + const pMazeLevelIds = nAct2LairPrevIds[nIndex]! + if (!DRLGMAZE_ReplaceRoomPreset(pLevel, pMazeLevelIds.nLevelPrestId1, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false)) { + DRLGMAZE_AddSpecialPreset(pLevel, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile) + } +} + +//D2Common.0x6FD7BE60 +export function DRLGMAZE_PlaceAct3DungeonStuff(pLevel: D2DrlgLevelStrc): void { + const nAct3DungeonPrevIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT3_DUNGEON_N, nLevelPrestId2: LVLPREST_ACT3_DUNGEON_PREV_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT3_DUNGEON_E, nLevelPrestId2: LVLPREST_ACT3_DUNGEON_PREV_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT3_DUNGEON_S, nLevelPrestId2: LVLPREST_ACT3_DUNGEON_PREV_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT3_DUNGEON_W, nLevelPrestId2: LVLPREST_ACT3_DUNGEON_PREV_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nAct3DungeonNextIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT3_DUNGEON_N, nLevelPrestId2: LVLPREST_ACT3_DUNGEON_NEXT_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT3_DUNGEON_E, nLevelPrestId2: LVLPREST_ACT3_DUNGEON_NEXT_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT3_DUNGEON_S, nLevelPrestId2: LVLPREST_ACT3_DUNGEON_NEXT_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT3_DUNGEON_W, nLevelPrestId2: LVLPREST_ACT3_DUNGEON_NEXT_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nDirection = SEED_RollRandomNumber(pLevel.pSeed) & 3 + + let pMazeLevelIds = nAct3DungeonPrevIds[nDirection]! + let pDrlgRoom = pLevel.pFirstRoomEx + while (pDrlgRoom) { + if (!DRLGMAZE_HasMapDS1(pDrlgRoom) && pDrlgRoom.pMaze!.nLevelPrest === pMazeLevelIds.nLevelPrestId1) { + pDrlgRoom.pMaze!.nLevelPrest = pMazeLevelIds.nLevelPrestId2 + pDrlgRoom.pMaze!.nPickedFile = pMazeLevelIds.nPickedFile + pDrlgRoom.pMaze!.dwFlags |= DRLGPRESETROOMFLAG_HAS_MAP_DS1 + break + } + pDrlgRoom = pDrlgRoom.pDrlgRoomNext + } + + if (!pDrlgRoom) { + for (let i = pLevel.pFirstRoomEx; i; i = i.pDrlgRoomNext) { + if (!DRLGMAZE_HasMapDS1(i)) { + pDrlgRoom = DRLGMAZE_PlaceAdjacentPresetRoom(i, pMazeLevelIds.nDirection, false) + if (pDrlgRoom) { + DRLGMAZE_PickRoomPreset(i, true) + DRLGMAZE_SetPickedFileAndPresetId(pDrlgRoom, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false) + break + } + } + } + } + + pMazeLevelIds = nAct3DungeonNextIds[(nDirection + 1) % 4]! + pDrlgRoom = pLevel.pFirstRoomEx + while (pDrlgRoom) { + if (!DRLGMAZE_HasMapDS1(pDrlgRoom) && pDrlgRoom.pMaze!.nLevelPrest === pMazeLevelIds.nLevelPrestId1) { + pDrlgRoom.pMaze!.nLevelPrest = pMazeLevelIds.nLevelPrestId2 + pDrlgRoom.pMaze!.nPickedFile = pMazeLevelIds.nPickedFile + pDrlgRoom.pMaze!.dwFlags |= DRLGPRESETROOMFLAG_HAS_MAP_DS1 + break + } + pDrlgRoom = pDrlgRoom.pDrlgRoomNext + } + + if (!pDrlgRoom) { + for (let i = pLevel.pFirstRoomEx; i; i = i.pDrlgRoomNext) { + if (!DRLGMAZE_HasMapDS1(i)) { + pDrlgRoom = DRLGMAZE_PlaceAdjacentPresetRoom(i, pMazeLevelIds.nDirection, false) + if (pDrlgRoom) { + DRLGMAZE_PickRoomPreset(i, true) + DRLGMAZE_SetPickedFileAndPresetId(pDrlgRoom, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false) + break + } + } + } + } +} + +//D2Common.0x6FD7C000 +export function DRLGMAZE_PlaceAct3SewerStuff(pLevel: D2DrlgLevelStrc): void { + const nAct3SewerDrainIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT3_SEWER_N, nLevelPrestId2: LVLPREST_ACT3_SEWER_DRAIN_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT3_SEWER_E, nLevelPrestId2: LVLPREST_ACT3_SEWER_DRAIN_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT3_SEWER_S, nLevelPrestId2: LVLPREST_ACT3_SEWER_DRAIN_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT3_SEWER_W, nLevelPrestId2: LVLPREST_ACT3_SEWER_DRAIN_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nAct3SewerChestIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT3_SEWER_N, nLevelPrestId2: LVLPREST_ACT3_SEWER_CHEST_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT3_SEWER_E, nLevelPrestId2: LVLPREST_ACT3_SEWER_CHEST_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT3_SEWER_S, nLevelPrestId2: LVLPREST_ACT3_SEWER_CHEST_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT3_SEWER_W, nLevelPrestId2: LVLPREST_ACT3_SEWER_CHEST_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nDirection = SEED_RollRandomNumber(pLevel.pSeed) & 3 + + let pMazeLevelIds = nAct3SewerDrainIds[nDirection]! + let pDrlgRoom = pLevel.pFirstRoomEx + while (pDrlgRoom) { + if (!DRLGMAZE_HasMapDS1(pDrlgRoom) && pDrlgRoom.pMaze!.nLevelPrest === pMazeLevelIds.nLevelPrestId1) { + pDrlgRoom.pMaze!.nLevelPrest = pMazeLevelIds.nLevelPrestId2 + pDrlgRoom.pMaze!.nPickedFile = pMazeLevelIds.nPickedFile + pDrlgRoom.pMaze!.dwFlags |= DRLGPRESETROOMFLAG_HAS_MAP_DS1 + break + } + pDrlgRoom = pDrlgRoom.pDrlgRoomNext + } + + if (!pDrlgRoom) { + for (let i = pLevel.pFirstRoomEx; i; i = i.pDrlgRoomNext) { + if (!DRLGMAZE_HasMapDS1(i)) { + const pTemp = DRLGMAZE_PlaceAdjacentPresetRoom(i, pMazeLevelIds.nDirection, false) + if (pTemp) { + DRLGMAZE_PickRoomPreset(i, true) + DRLGMAZE_SetPickedFileAndPresetId(pTemp, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false) + break + } + } + } + } + + pMazeLevelIds = nAct3SewerChestIds[(nDirection + 1) % 4]! + pDrlgRoom = pLevel.pFirstRoomEx + while (pDrlgRoom) { + if (!DRLGMAZE_HasMapDS1(pDrlgRoom) && pDrlgRoom.pMaze!.nLevelPrest === pMazeLevelIds.nLevelPrestId1) { + pDrlgRoom.pMaze!.nLevelPrest = pMazeLevelIds.nLevelPrestId2 + pDrlgRoom.pMaze!.nPickedFile = pMazeLevelIds.nPickedFile + pDrlgRoom.pMaze!.dwFlags |= DRLGPRESETROOMFLAG_HAS_MAP_DS1 + break + } + pDrlgRoom = pDrlgRoom.pDrlgRoomNext + } + + if (!pDrlgRoom) { + for (let i = pLevel.pFirstRoomEx; i; i = i.pDrlgRoomNext) { + if (!DRLGMAZE_HasMapDS1(i)) { + const pTemp = DRLGMAZE_PlaceAdjacentPresetRoom(i, pMazeLevelIds.nDirection, false) + if (pTemp) { + DRLGMAZE_PickRoomPreset(i, true) + DRLGMAZE_SetPickedFileAndPresetId(pTemp, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false) + break + } + } + } + } +} + +//D2Common.0x6FD7C1A0 +export function DRLGMAZE_PlaceAct3MephistoStuff(pLevel: D2DrlgLevelStrc): void { + const nAct3MephistoPrevIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT3_MEPHISTO_N, nLevelPrestId2: LVLPREST_ACT3_MEPHISTO_PREV_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT3_MEPHISTO_E, nLevelPrestId2: LVLPREST_ACT3_MEPHISTO_PREV_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT3_MEPHISTO_S, nLevelPrestId2: LVLPREST_ACT3_MEPHISTO_PREV_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT3_MEPHISTO_W, nLevelPrestId2: LVLPREST_ACT3_MEPHISTO_PREV_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nAct3MephistoWaypointIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT3_MEPHISTO_N, nLevelPrestId2: LVLPREST_ACT3_MEPHISTO_WAYPOINT_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT3_MEPHISTO_E, nLevelPrestId2: LVLPREST_ACT3_MEPHISTO_WAYPOINT_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT3_MEPHISTO_S, nLevelPrestId2: LVLPREST_ACT3_MEPHISTO_WAYPOINT_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT3_MEPHISTO_W, nLevelPrestId2: LVLPREST_ACT3_MEPHISTO_WAYPOINT_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nAct3MephistoNextIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT3_MEPHISTO_N, nLevelPrestId2: LVLPREST_ACT3_MEPHISTO_NEXT_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT3_MEPHISTO_E, nLevelPrestId2: LVLPREST_ACT3_MEPHISTO_NEXT_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT3_MEPHISTO_S, nLevelPrestId2: LVLPREST_ACT3_MEPHISTO_NEXT_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT3_MEPHISTO_W, nLevelPrestId2: LVLPREST_ACT3_MEPHISTO_NEXT_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nRand = SEED_RollRandomNumber(pLevel.pSeed) & 3 + + let pMazeLevelIds = nAct3MephistoPrevIds[nRand]! + let pDrlgRoom = pLevel.pFirstRoomEx + while (pDrlgRoom) { + if (!DRLGMAZE_HasMapDS1(pDrlgRoom) && pDrlgRoom.pMaze!.nLevelPrest === pMazeLevelIds.nLevelPrestId1) { + DRLGMAZE_SetPickedFileAndPresetId(pDrlgRoom, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false) + break + } + pDrlgRoom = pDrlgRoom.pDrlgRoomNext + } + + if (!pDrlgRoom) { + for (let i = pLevel.pFirstRoomEx; i; i = i.pDrlgRoomNext) { + if (!DRLGMAZE_HasMapDS1(i) && DRLGMAZE_InitRoomFixedPreset(i, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, true)) { + break + } + } + } + + let v9 = (nRand + 1) % 4 + if (pLevel.nLevelId === LEVEL_DURANCEOFHATELEV2) { + pMazeLevelIds = nAct3MephistoWaypointIds[v9]! + pDrlgRoom = pLevel.pFirstRoomEx + while (pDrlgRoom) { + if (!DRLGMAZE_HasMapDS1(pDrlgRoom) && pDrlgRoom.pMaze!.nLevelPrest === pMazeLevelIds.nLevelPrestId1) { + DRLGMAZE_SetPickedFileAndPresetId(pDrlgRoom, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false) + break + } + pDrlgRoom = pDrlgRoom.pDrlgRoomNext + } + + if (!pDrlgRoom) { + DRLGMAZE_AddSpecialPreset(pLevel, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile) + } + + const v14 = (v9 + 1) % 4 + pMazeLevelIds = nAct3MephistoNextIds[v14]! + pDrlgRoom = pLevel.pFirstRoomEx + while (pDrlgRoom) { + if (!DRLGMAZE_HasMapDS1(pDrlgRoom) && pDrlgRoom.pMaze!.nLevelPrest === pMazeLevelIds.nLevelPrestId1) { + DRLGMAZE_SetPickedFileAndPresetId(pDrlgRoom, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false) + break + } + pDrlgRoom = pDrlgRoom.pDrlgRoomNext + } + + if (!pDrlgRoom) { + DRLGMAZE_AddSpecialPreset(pLevel, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile) + } + + v9 = (v14 + 1) % 4 + } + + if (pLevel.nLevelId === LEVEL_DURANCEOFHATELEV1) { + pMazeLevelIds = nAct3MephistoNextIds[v9]! + if (!DRLGMAZE_ReplaceRoomPreset(pLevel, pMazeLevelIds.nLevelPrestId1, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false)) { + DRLGMAZE_AddSpecialPreset(pLevel, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile) + } + } +} + +//D2Common.0x6FD7C380 +export function DRLGMAZE_PlaceAct5TempleStuff(pLevel: D2DrlgLevelStrc): void { + const nAct5TempleSpecialIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT5_TEMPLE_NE, nLevelPrestId2: LVLPREST_ACT5_TEMPLE_NE_DOWN, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT5_TEMPLE_NW, nLevelPrestId2: LVLPREST_ACT5_TEMPLE_NW_DOWN, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT5_TEMPLE_SW, nLevelPrestId2: LVLPREST_ACT5_TEMPLE_SW_DOWN, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT5_TEMPLE_NE, nLevelPrestId2: LVLPREST_ACT5_TEMPLE_NE_WAYPOINT, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT5_TEMPLE_NW, nLevelPrestId2: LVLPREST_ACT5_TEMPLE_NW_WAYPOINT, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT5_TEMPLE_SW, nLevelPrestId2: LVLPREST_ACT5_TEMPLE_SW_WAYPOINT, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT5_TEMPLE_SE_UP, nLevelPrestId2: LVLPREST_ACT5_TEMPLE_SE_WAYPOINT, nPickedFile: -1, nDirection: 2 }, + ] + + let nDirection = (SEED_RollRandomNumber(pLevel.pSeed) >>> 0) % 3 + + if (pLevel.nLevelId !== LEVEL_HALLSOFVAUGHT) { + const pMazeLevelIds = nAct5TempleSpecialIds[nDirection]! + let pDrlgRoom = pLevel.pFirstRoomEx + while (pDrlgRoom) { + if (!DRLGMAZE_HasMapDS1(pDrlgRoom) && pDrlgRoom.pMaze!.nLevelPrest === pMazeLevelIds.nLevelPrestId1) { + pDrlgRoom.pMaze!.nLevelPrest = pMazeLevelIds.nLevelPrestId2 + pDrlgRoom.pMaze!.nPickedFile = pMazeLevelIds.nPickedFile + pDrlgRoom.pMaze!.dwFlags |= DRLGPRESETROOMFLAG_HAS_MAP_DS1 + break + } + pDrlgRoom = pDrlgRoom.pDrlgRoomNext + } + + if (!pDrlgRoom) { + for (let i = pLevel.pFirstRoomEx; i; i = i.pDrlgRoomNext) { + if (!DRLGMAZE_HasMapDS1(i) && DRLGMAZE_InitRoomFixedPreset(i, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, true)) { + break + } + } + } + + nDirection = (nDirection + 1) % 4 + } + + if (pLevel.nLevelId === LEVEL_HALLSOFDEATHSCALLING) { + const pMazeLevelIds = nAct5TempleSpecialIds[nDirection + 3]! + let pDrlgRoom = pLevel.pFirstRoomEx + while (pDrlgRoom) { + if (!DRLGMAZE_HasMapDS1(pDrlgRoom) && pDrlgRoom.pMaze!.nLevelPrest === pMazeLevelIds.nLevelPrestId1) { + pDrlgRoom.pMaze!.nPickedFile = pMazeLevelIds.nPickedFile + pDrlgRoom.pMaze!.nLevelPrest = pMazeLevelIds.nLevelPrestId2 + pDrlgRoom.pMaze!.dwFlags |= DRLGPRESETROOMFLAG_HAS_MAP_DS1 + break + } + pDrlgRoom = pDrlgRoom.pDrlgRoomNext + } + + if (!pDrlgRoom) { + for (let i = pLevel.pFirstRoomEx; i; i = i.pDrlgRoomNext) { + if (!DRLGMAZE_HasMapDS1(i) && DRLGMAZE_InitRoomFixedPreset(i, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, true)) { + break + } + } + } + } +} + +//D2Common.0x6FD7C500 +export function DRLGMAZE_PlaceAct5BaalStuff(pLevel: D2DrlgLevelStrc): void { + const nAct5BaalNextIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT5_BAAL_N, nLevelPrestId2: LVLPREST_ACT5_BAAL_NEXT_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT5_BAAL_E, nLevelPrestId2: LVLPREST_ACT5_BAAL_NEXT_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT5_BAAL_S, nLevelPrestId2: LVLPREST_ACT5_BAAL_NEXT_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT5_BAAL_W, nLevelPrestId2: LVLPREST_ACT5_BAAL_NEXT_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nAct5BaalWaypointIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT5_BAAL_N, nLevelPrestId2: LVLPREST_ACT5_BAAL_WAYPOINT_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT5_BAAL_E, nLevelPrestId2: LVLPREST_ACT5_BAAL_WAYPOINT_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT5_BAAL_S, nLevelPrestId2: LVLPREST_ACT5_BAAL_WAYPOINT_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT5_BAAL_W, nLevelPrestId2: LVLPREST_ACT5_BAAL_WAYPOINT_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nDirection = SEED_RollRandomNumber(pLevel.pSeed) & 3 + let pMazeLevelIds = nAct5BaalNextIds[nDirection]! + let pDrlgRoom = pLevel.pFirstRoomEx + while (pDrlgRoom) { + if (!DRLGMAZE_HasMapDS1(pDrlgRoom) && pDrlgRoom.pMaze!.nLevelPrest === pMazeLevelIds.nLevelPrestId1) { + pDrlgRoom.pMaze!.nLevelPrest = pMazeLevelIds.nLevelPrestId2 + pDrlgRoom.pMaze!.nPickedFile = pMazeLevelIds.nPickedFile + pDrlgRoom.pMaze!.dwFlags |= DRLGPRESETROOMFLAG_HAS_MAP_DS1 + break + } + pDrlgRoom = pDrlgRoom.pDrlgRoomNext + } + + if (!pDrlgRoom) { + for (let i = pLevel.pFirstRoomEx; i; i = i.pDrlgRoomNext) { + if (!DRLGMAZE_HasMapDS1(i) && DRLGMAZE_InitRoomFixedPreset(i, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, true)) { + break + } + } + } + + if (pLevel.nLevelId === LEVEL_THEWORLDSTONEKEEPLEV2) { + pMazeLevelIds = nAct5BaalWaypointIds[(nDirection + 1) % 4]! + pDrlgRoom = pLevel.pFirstRoomEx + while (pDrlgRoom) { + if (!DRLGMAZE_HasMapDS1(pDrlgRoom) && pDrlgRoom.pMaze!.nLevelPrest === pMazeLevelIds.nLevelPrestId1) { + pDrlgRoom.pMaze!.nPickedFile = pMazeLevelIds.nPickedFile + pDrlgRoom.pMaze!.nLevelPrest = pMazeLevelIds.nLevelPrestId2 + pDrlgRoom.pMaze!.dwFlags |= DRLGPRESETROOMFLAG_HAS_MAP_DS1 + break + } + pDrlgRoom = pDrlgRoom.pDrlgRoomNext + } + + if (!pDrlgRoom) { + for (let i = pLevel.pFirstRoomEx; i; i = i.pDrlgRoomNext) { + if (!DRLGMAZE_HasMapDS1(i) && DRLGMAZE_InitRoomFixedPreset(i, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, true)) { + break + } + } + } + } +} + //D2Common.0x6FD7C660 export function DRLGMAZE_PlaceAct1Barracks(pLevel: D2DrlgLevelStrc): void { const pfGetBarracksEntryRoom = [ @@ -1330,6 +2649,172 @@ export function DRLGMAZE_SetRoomSize(pDrlgRoom: D2DrlgRoomStrc): void { pDrlgRoom.pDrlgCoord.nHeight = pDrlgRoom.pLevel.pMaze!.dwSizeY } +//D2Common.0x6FD7CA40 +export function DRLGMAZE_PlaceAct4Lava(pLevel: D2DrlgLevelStrc): void { + const nAct4LavaForgeIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT4_LAVA_W, nLevelPrestId2: LVLPREST_ACT4_LAVA_FORGE_W, nPickedFile: -1, nDirection: 2 }, + { nLevelPrestId1: LVLPREST_ACT4_LAVA_E, nLevelPrestId2: LVLPREST_ACT4_LAVA_FORGE_E, nPickedFile: -1, nDirection: 0 }, + ] + + if (pLevel.pDrlg.nAct !== ACT_V) { + const pChaosSanctum = DRLG_GetLevel(pLevel.pDrlg, LEVEL_CHAOSSANCTUM) + let pParentRoom = DRLGMAZE_GetFreeLocationForRoomNorth(pLevel)! + + const pNewRoomEx = DRLGROOM_AllocRoomEx(pParentRoom.pLevel, DRLGTYPE_PRESET) + DRLGMAZE_SetRoomSize(pNewRoomEx) + + if (DRLGMAZE_LinkMazeRooms(pNewRoomEx, pParentRoom, ALTDIR_SOUTH)) { + DRLGROOM_AllocDrlgOrthsForRooms(pParentRoom, pNewRoomEx, ALTDIR_SOUTH) + DRLGROOM_AddRoomExToLevel(pParentRoom.pLevel, pNewRoomEx) + if (pNewRoomEx) { + DRLGMAZE_PickRoomPreset(pParentRoom, true) + DRLGMAZE_SetPickedFileAndPresetId(pNewRoomEx, LVLPREST_ACT4_LAVA_WARP_N, -1, false) + } + } else { + DRLGROOM_FreeRoomEx(pNewRoomEx) + } + + pParentRoom = DRLGMAZE_GetFreeLocationForRoomSouth(pLevel)! + + const pBridgeRoomEx1 = DRLGMAZE_PlaceAdjacentPresetRoom(pParentRoom, ALTDIR_NORTH, false) + if (pBridgeRoomEx1) { + DRLGMAZE_PickRoomPreset(pParentRoom, true) + DRLGMAZE_SetPickedFileAndPresetId(pBridgeRoomEx1, LVLPREST_ACT4_BRIDGE_1, -1, false) + } + + const pBridgeRoomEx2 = DRLGMAZE_PlaceAdjacentPresetRoom(pBridgeRoomEx1!, ALTDIR_NORTH, false) + if (pBridgeRoomEx2) { + DRLGMAZE_SetPickedFileAndPresetId(pBridgeRoomEx2, LVLPREST_ACT4_BRIDGE_2, -1, false) + } + + const pBridgeRoomEx3 = DRLGMAZE_PlaceAdjacentPresetRoom(pBridgeRoomEx2!, ALTDIR_NORTH, false)! + if (pBridgeRoomEx3) { + DRLGMAZE_SetPickedFileAndPresetId(pBridgeRoomEx3, LVLPREST_ACT4_BRIDGE_2, -1, false) + } + + pBridgeRoomEx3.pDrlgOrth = DRLGROOM_AddOrth(pBridgeRoomEx3.pDrlgOrth, pChaosSanctum, ALTDIR_NORTH, false) + + const nX = pChaosSanctum.pLevelCoords.nPosX + 2 * pBridgeRoomEx3.pDrlgCoord.nWidth - pBridgeRoomEx3.pDrlgCoord.nPosX + const nY = pChaosSanctum.pLevelCoords.nPosY + pChaosSanctum.pLevelCoords.nHeight - pBridgeRoomEx3.pDrlgCoord.nPosY + + DRLGMAZE_ScanReplaceSpecialPreset(pLevel, nAct4LavaForgeIds[SEED_RollRandomNumber(pLevel.pSeed) & 1]!, null) + DRLGMAZE_FillBlankMazeSpaces(pLevel, LVLPREST_ACT4_LAVA_X, pBridgeRoomEx3) + + for (let i = pLevel.pFirstRoomEx; i; i = i.pDrlgRoomNext) { + i.pDrlgCoord.nPosX += nX + i.pDrlgCoord.nPosY += nY + } + + const { nTileMinX, nTileMinY, nTileMaxX, nTileMaxY } = DRLG_GetMinAndMaxCoordinatesFromLevel(pLevel) + pLevel.pLevelCoords.nPosX = nTileMinX + pLevel.pLevelCoords.nPosY = nTileMinY + pLevel.pLevelCoords.nWidth = nTileMaxX - nTileMinX + pLevel.pLevelCoords.nHeight = nTileMaxY - nTileMinY + } +} + +//D2Common.0x6FD7CCB0 +export function DRLGMAZE_PlaceAct5IceStuff(pLevel: D2DrlgLevelStrc): void { + const nAct5IcePrevIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT5_ICE_N, nLevelPrestId2: LVLPREST_ACT5_ICE_PREV_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT5_ICE_E, nLevelPrestId2: LVLPREST_ACT5_ICE_PREV_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT5_ICE_S, nLevelPrestId2: LVLPREST_ACT5_ICE_PREV_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT5_ICE_W, nLevelPrestId2: LVLPREST_ACT5_ICE_PREV_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nAct5IceNextIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT5_ICE_N, nLevelPrestId2: LVLPREST_ACT5_ICE_NEXT_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT5_ICE_E, nLevelPrestId2: LVLPREST_ACT5_ICE_NEXT_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT5_ICE_S, nLevelPrestId2: LVLPREST_ACT5_ICE_NEXT_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT5_ICE_W, nLevelPrestId2: LVLPREST_ACT5_ICE_NEXT_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nAct5IceDownIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT5_ICE_N, nLevelPrestId2: LVLPREST_ACT5_ICE_DOWN_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT5_ICE_E, nLevelPrestId2: LVLPREST_ACT5_ICE_DOWN_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT5_ICE_S, nLevelPrestId2: LVLPREST_ACT5_ICE_DOWN_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT5_ICE_W, nLevelPrestId2: LVLPREST_ACT5_ICE_DOWN_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nAct5IceThemeIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT5_ICE_N, nLevelPrestId2: LVLPREST_ACT5_ICE_THEME_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT5_ICE_E, nLevelPrestId2: LVLPREST_ACT5_ICE_THEME_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT5_ICE_S, nLevelPrestId2: LVLPREST_ACT5_ICE_THEME_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT5_ICE_W, nLevelPrestId2: LVLPREST_ACT5_ICE_THEME_W, nPickedFile: -1, nDirection: 2 }, + ] + + const nAct5IceWaypointIds: readonly D2MazeLevelIdStrc[] = [ + { nLevelPrestId1: LVLPREST_ACT5_ICE_N, nLevelPrestId2: LVLPREST_ACT5_ICE_WAYPOINT_N, nPickedFile: -1, nDirection: 3 }, + { nLevelPrestId1: LVLPREST_ACT5_ICE_E, nLevelPrestId2: LVLPREST_ACT5_ICE_WAYPOINT_E, nPickedFile: -1, nDirection: 0 }, + { nLevelPrestId1: LVLPREST_ACT5_ICE_S, nLevelPrestId2: LVLPREST_ACT5_ICE_WAYPOINT_S, nPickedFile: -1, nDirection: 1 }, + { nLevelPrestId1: LVLPREST_ACT5_ICE_W, nLevelPrestId2: LVLPREST_ACT5_ICE_WAYPOINT_W, nPickedFile: -1, nDirection: 2 }, + ] + + let nDirection = SEED_RollRandomNumber(pLevel.pSeed) & 3 + let pMazeLevelIds = nAct5IcePrevIds[nDirection]! + + let pDrlgRoom = pLevel.pFirstRoomEx + while (pDrlgRoom) { + if (!DRLGMAZE_HasMapDS1(pDrlgRoom) && pDrlgRoom.pMaze!.nLevelPrest === pMazeLevelIds.nLevelPrestId1) { + DRLGMAZE_SetPickedFileAndPresetId(pDrlgRoom, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false) + break + } + pDrlgRoom = pDrlgRoom.pDrlgRoomNext + } + + if (!pDrlgRoom) { + DRLGMAZE_AddSpecialPreset(pLevel, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile) + } + + nDirection = (nDirection + 1) % 4 + pMazeLevelIds = nAct5IceNextIds[nDirection]! + pDrlgRoom = pLevel.pFirstRoomEx + while (pDrlgRoom) { + if (!DRLGMAZE_HasMapDS1(pDrlgRoom) && pDrlgRoom.pMaze!.nLevelPrest === pMazeLevelIds.nLevelPrestId1) { + DRLGMAZE_SetPickedFileAndPresetId(pDrlgRoom, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false) + break + } + pDrlgRoom = pDrlgRoom.pDrlgRoomNext + } + + if (!pDrlgRoom) { + DRLGMAZE_AddSpecialPreset(pLevel, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile) + } + + nDirection = (nDirection + 1) % 4 + pMazeLevelIds = nAct5IceDownIds[nDirection]! + pDrlgRoom = pLevel.pFirstRoomEx + while (pDrlgRoom) { + if (!DRLGMAZE_HasMapDS1(pDrlgRoom) && pDrlgRoom.pMaze!.nLevelPrest === pMazeLevelIds.nLevelPrestId1) { + DRLGMAZE_SetPickedFileAndPresetId(pDrlgRoom, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false) + break + } + pDrlgRoom = pDrlgRoom.pDrlgRoomNext + } + + if (!pDrlgRoom) { + DRLGMAZE_AddSpecialPreset(pLevel, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile) + } + + nDirection = (nDirection + 1) % 4 + if (pLevel.nLevelId !== LEVEL_CRYSTALIZEDCAVERNLEV1) { + if (pLevel.nLevelId === LEVEL_CRYSTALIZEDCAVERNLEV2) { + pMazeLevelIds = nAct5IceThemeIds[nDirection]! + if (!DRLGMAZE_ReplaceRoomPreset(pLevel, pMazeLevelIds.nLevelPrestId1, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false)) { + DRLGMAZE_AddSpecialPreset(pLevel, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile) + } + nDirection = (nDirection + 1) % 4 + } else if (pLevel.nLevelId !== LEVEL_GLACIALCAVESLEV1) { + return + } + } + + pMazeLevelIds = nAct5IceWaypointIds[nDirection]! + if (!DRLGMAZE_ReplaceRoomPreset(pLevel, pMazeLevelIds.nLevelPrestId1, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile, false)) { + DRLGMAZE_AddSpecialPreset(pLevel, pMazeLevelIds.nDirection, pMazeLevelIds.nLevelPrestId2, pMazeLevelIds.nPickedFile) + } +} + //D2Common.0x6FD7CEA0 export function DRLGMAZE_RollAct_1_2_3_BasicPresets(pLevel: D2DrlgLevelStrc): void { const pOffsets = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14] diff --git a/src/game/drlg/drlg-outdesr.ts b/src/game/drlg/drlg-outdesr.ts new file mode 100644 index 0000000..fd4358c --- /dev/null +++ b/src/game/drlg/drlg-outdesr.ts @@ -0,0 +1,413 @@ +/** + * D2Common Act II outdoor desert DRLG: 1:1 port of D2MOO + * `source/D2Common/src/Drlg/DrlgOutDesr.cpp`. + */ + +import { SEED_RollLimitedRandomNumber, SEED_RollRandomNumber } from '../d2-rng.ts' +import { + LVLPREST_ACT2_DESERT_BORDER_1, + LVLPREST_ACT2_DESERT_CLIFF_LEFT_ENDS, + LVLPREST_ACT2_DESERT_CLIFF_LEFT_KING_ENDS, + LVLPREST_ACT2_DESERT_CLIFF_LEFT_KING_TOMB, + LVLPREST_ACT2_DESERT_CLIFF_LEFT_PATH, + LVLPREST_ACT2_DESERT_CLIFF_LEFT_WALL, + LVLPREST_ACT2_DESERT_CLIFF_RIGHT_ENDS, + LVLPREST_ACT2_DESERT_CLIFF_RIGHT_KING_ENDS, + LVLPREST_ACT2_DESERT_CLIFF_RIGHT_KING_TOMB, + LVLPREST_ACT2_DESERT_CLIFF_RIGHT_PATH, + LVLPREST_ACT2_DESERT_CLIFF_RIGHT_WALL, + LVLPREST_ACT2_DESERT_CLIFF_TOP, + LVLPREST_ACT2_DESERT_CLIFF_TOP_KING_TOMB, + LVLPREST_ACT2_DESERT_FILL_BERMS_1, + LVLPREST_ACT2_DESERT_FILL_BERMS_2, + LVLPREST_ACT2_DESERT_FILL_BERMS_3, + LVLPREST_ACT2_DESERT_FILL_BERMS_4, + LVLPREST_ACT2_DESERT_FILL_BONE_1, + LVLPREST_ACT2_DESERT_FILL_BONE_2, + LVLPREST_ACT2_DESERT_FILL_HEAD_1, + LVLPREST_ACT2_DESERT_FILL_HEAD_2, + LVLPREST_ACT2_DESERT_FILL_MESA_1, + LVLPREST_ACT2_DESERT_FILL_WAGON_1, + LVLPREST_ACT2_DESERT_LAIR_1, + LVLPREST_ACT2_DESERT_OASIS_1, + LVLPREST_ACT2_DESERT_OASIS_2, + LVLPREST_ACT2_DESERT_OASIS_3, + LVLPREST_ACT2_DESERT_RUINS_08X08, + LVLPREST_ACT2_DESERT_RUINS_08X16, + LVLPREST_ACT2_DESERT_RUINS_16X08, + LVLPREST_ACT2_DESERT_RUINS_16X16, + LVLPREST_ACT2_DESERT_RUINS_ELDER, + LVLPREST_ACT2_DESERT_RUINS_SEWER, + LVLPREST_ACT2_DESERT_TOMB_1, + LVLPREST_ACT2_DESERT_TOMB_2, + LVLPREST_ACT2_DESERT_TRANSITION_N, + LVLPREST_ACT2_DESERT_TRANSITION_W, + LVLPREST_ACT2_DESERT_VALLEY_RUIN_1, + LVLPREST_ACT2_DESERT_VALLEY_RUIN_2, + LVLPREST_ACT2_DESERT_VALLEY_WARP, +} from './drlg-ids.ts' +import { + DRLGOUTDOORS_AddAct124SecondaryBorder, + DRLGOUTDOORS_SpawnAct12Shrines, + DRLGOUTDOORS_SpawnAct12Waypoint, + DRLGOUTDOORS_SpawnOutdoorLevelPreset, + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx, +} from './drlg-outdoors.ts' +import { DRLGOUTPLACE_PlaceAct1245OutdoorBorders, DRLGOUTPLACE_SetOutGridLinkFlags } from './drlg-outplace.ts' +import { DATATBLS_GetLvlPrestTxtRecord } from './drlg-tables.ts' +import { + type D2DrlgLevelStrc, + LEVEL_CANYONOFTHEMAGI, + LEVEL_DRYHILLS, + LEVEL_FAROASIS, + LEVEL_LOSTCITY, + LEVEL_LUTGHOLEIN, + LEVEL_PANDEMONIUMRUN2, + LEVEL_ROCKYWASTE, + LEVEL_VALLEYOFSNAKES, +} from './drlg-types.ts' + +interface D2DrlgOutDesertInitStrc { + readonly nLvlPrestId: number + readonly nRand: number + readonly nX: number + readonly nY: number +} + +// Inlined in D2Common.0x6FD7D430 +export function DRLGOUTDESR_PlaceDesertTransitionToTown(pLevel: D2DrlgLevelStrc): void { + let pTownRoomData = pLevel.pOutdoors!.pRoomData + + while (pTownRoomData && pTownRoomData.pLevel!.nLevelId !== LEVEL_LUTGHOLEIN) { + pTownRoomData = pTownRoomData.pNext + } + + if (pTownRoomData) { + if (pTownRoomData.nDirection === 3) { + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx( + pLevel, + 0, + pLevel.pOutdoors!.nGridHeight - 1, + LVLPREST_ACT2_DESERT_TRANSITION_N, + -1, + false, + ) + } else { + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx( + pLevel, + pLevel.pOutdoors!.nGridWidth - 1, + 0, + LVLPREST_ACT2_DESERT_TRANSITION_W, + -1, + false, + ) + } + } +} + +// 1.10f: D2Common.0x6FD7D430 +// 1.11 : D2Common.0x6FDD58A0 +export function DRLGOUTDESR_InitAct2OutdoorLevel(pLevel: D2DrlgLevelStrc): void { + const nLevelPrestIds1: readonly number[] = [ + LVLPREST_ACT2_DESERT_OASIS_1, + LVLPREST_ACT2_DESERT_RUINS_08X08, + LVLPREST_ACT2_DESERT_FILL_BONE_1, + LVLPREST_ACT2_DESERT_FILL_BONE_2, + LVLPREST_ACT2_DESERT_FILL_HEAD_1, + LVLPREST_ACT2_DESERT_FILL_MESA_1, + LVLPREST_ACT2_DESERT_FILL_WAGON_1, + ] + const nLevelPrestIds2: readonly number[] = [ + LVLPREST_ACT2_DESERT_OASIS_1, + LVLPREST_ACT2_DESERT_RUINS_08X08, + LVLPREST_ACT2_DESERT_FILL_HEAD_2, + LVLPREST_ACT2_DESERT_FILL_MESA_1, + ] + const nLevelPrestIds3: readonly number[] = [ + LVLPREST_ACT2_DESERT_FILL_BERMS_1, + LVLPREST_ACT2_DESERT_FILL_BERMS_2, + LVLPREST_ACT2_DESERT_FILL_BERMS_3, + LVLPREST_ACT2_DESERT_FILL_BERMS_4, + ] + const nLevelPrestIds4: readonly number[] = [LVLPREST_ACT2_DESERT_OASIS_2, LVLPREST_ACT2_DESERT_OASIS_3] + + DRLGOUTPLACE_SetOutGridLinkFlags(pLevel) + DRLGOUTPLACE_PlaceAct1245OutdoorBorders(pLevel) + + switch (pLevel.nLevelId) { + case LEVEL_ROCKYWASTE: + DRLGOUTDESR_PlaceDesertTransitionToTown(pLevel) + DRLGOUTDESR_PlaceBorders(pLevel) + DRLGOUTDESR_AddExits(pLevel) + DRLGOUTDOORS_SpawnAct12Shrines(pLevel, 5) + DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds1, nLevelPrestIds1.length, false) + break + + case LEVEL_DRYHILLS: + DRLGOUTDESR_PlaceCliffs(pLevel) + DRLGOUTDESR_PlaceBorders(pLevel) + DRLGOUTDESR_AddExits(pLevel) + DRLGOUTDOORS_SpawnAct12Waypoint(pLevel) + DRLGOUTDOORS_SpawnAct12Shrines(pLevel, 5) + DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds2, nLevelPrestIds2.length, false) + DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds3, nLevelPrestIds3.length, true) + break + + case LEVEL_FAROASIS: + DRLGOUTDESR_PlaceCliffs(pLevel) + DRLGOUTDESR_PlaceBorders(pLevel) + DRLGOUTDESR_AddExits(pLevel) + DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds4, nLevelPrestIds4.length, false) + DRLGOUTDOORS_SpawnAct12Waypoint(pLevel) + DRLGOUTDOORS_SpawnAct12Shrines(pLevel, 5) + DRLGOUTDESR_PlaceFillsInFarOasis(pLevel) + break + + case LEVEL_LOSTCITY: + DRLGOUTDESR_PlaceCliffs(pLevel) + DRLGOUTDESR_PlaceBorders(pLevel) + DRLGOUTDESR_AddExits(pLevel) + DRLGOUTDESR_PlaceRuinsInLostCity(pLevel) + DRLGOUTDOORS_SpawnAct12Waypoint(pLevel) + DRLGOUTDOORS_SpawnAct12Shrines(pLevel, 5) + DRLGOUTDESR_PlaceFillsInLostCity(pLevel) + break + + case LEVEL_VALLEYOFSNAKES: + DRLGOUTDESR_AddExits(pLevel) + break + + case LEVEL_CANYONOFTHEMAGI: + DRLGOUTDESR_PlaceTombEntriesInCanyon(pLevel) + DRLGOUTDESR_PlaceBorders(pLevel) + DRLGOUTDOORS_SpawnAct12Shrines(pLevel, 5) + DRLGOUTDESR_PlaceFillsInCanyon(pLevel) + break + + case LEVEL_PANDEMONIUMRUN2: + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 4, 4, LVLPREST_ACT2_DESERT_VALLEY_WARP, -1, false) + DRLGOUTDESR_PlaceBorders(pLevel) + DRLGOUTDESR_PlaceFillsInCanyon(pLevel) + DRLGOUTDOORS_SpawnAct12Shrines(pLevel, 5) + break + + default: + return + } +} + +// D2Common.0x6FD7D870 +export function DRLGOUTDESR_PlacePresetVariants( + pLevel: D2DrlgLevelStrc, + pLevelPrestIds: readonly number[], + nVariants: number, + bIterateFiles: boolean, +): void { + let nRand = SEED_RollLimitedRandomNumber(pLevel.pSeed, nVariants) >>> 0 + + for (let i = 0; i < nVariants; i += 1) { + const nPrestId = pLevelPrestIds[nRand]! + if (bIterateFiles) { + const dwFiles = DATATBLS_GetLvlPrestTxtRecord(pLevel.pDrlg.env.tables, nPrestId).dwFiles >>> 0 + for (let nFile = 0; nFile < dwFiles; nFile += 1) { + DRLGOUTDOORS_SpawnOutdoorLevelPreset(pLevel, nPrestId, nFile, 0, 15) + } + } else { + DRLGOUTDOORS_SpawnOutdoorLevelPreset(pLevel, nPrestId, -1, 0, 15) + } + + nRand = (nRand + 1) % nVariants + } +} + +const pOutDesertCliffsInit: readonly (readonly D2DrlgOutDesertInitStrc[])[] = [ + [ + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_ENDS, nRand: 1, nX: 0, nY: 4 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_PATH, nRand: -1, nX: 2, nY: 4 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_WALL, nRand: -1, nX: 4, nY: 4 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_WALL, nRand: -1, nX: 6, nY: 4 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_ENDS, nRand: 2, nX: 8, nY: 4 }, + ], + [ + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_ENDS, nRand: 1, nX: 0, nY: 4 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_WALL, nRand: -1, nX: 2, nY: 4 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_PATH, nRand: -1, nX: 4, nY: 4 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_WALL, nRand: -1, nX: 6, nY: 4 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_ENDS, nRand: 2, nX: 8, nY: 4 }, + ], + [ + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_ENDS, nRand: 1, nX: 0, nY: 4 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_WALL, nRand: -1, nX: 2, nY: 4 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_WALL, nRand: -1, nX: 4, nY: 4 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_PATH, nRand: -1, nX: 6, nY: 4 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_ENDS, nRand: 2, nX: 8, nY: 4 }, + ], + [ + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_ENDS, nRand: 2, nX: 8, nY: 4 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_WALL, nRand: -1, nX: 6, nY: 4 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_TOP, nRand: -1, nX: 4, nY: 4 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_PATH, nRand: -1, nX: 4, nY: 6 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_ENDS, nRand: 2, nX: 4, nY: 8 }, + ], + [ + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_ENDS, nRand: 2, nX: 8, nY: 4 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_PATH, nRand: -1, nX: 6, nY: 4 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_TOP, nRand: -1, nX: 4, nY: 4 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_WALL, nRand: -1, nX: 4, nY: 6 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_ENDS, nRand: 2, nX: 4, nY: 8 }, + ], + [ + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_ENDS, nRand: 1, nX: 4, nY: 0 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_PATH, nRand: -1, nX: 4, nY: 2 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_WALL, nRand: -1, nX: 4, nY: 4 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_WALL, nRand: -1, nX: 4, nY: 6 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_ENDS, nRand: 2, nX: 4, nY: 8 }, + ], + [ + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_ENDS, nRand: 1, nX: 4, nY: 0 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_WALL, nRand: -1, nX: 4, nY: 2 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_PATH, nRand: -1, nX: 4, nY: 4 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_WALL, nRand: -1, nX: 4, nY: 6 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_ENDS, nRand: 2, nX: 4, nY: 8 }, + ], + [ + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_ENDS, nRand: 1, nX: 4, nY: 0 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_WALL, nRand: -1, nX: 4, nY: 2 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_WALL, nRand: -1, nX: 4, nY: 4 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_PATH, nRand: -1, nX: 4, nY: 6 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_ENDS, nRand: 2, nX: 4, nY: 8 }, + ], +] + +// D2Common.0x6FD7D950 +export function DRLGOUTDESR_PlaceCliffs(pLevel: D2DrlgLevelStrc): void { + const nRand = SEED_RollRandomNumber(pLevel.pSeed) & 7 + const row = pOutDesertCliffsInit[nRand]! + + for (let i = 0; i < 5; i += 1) { + const entry = row[i]! + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, entry.nX, entry.nY, entry.nLvlPrestId, entry.nRand, false) + } +} + +// D2Common.0x6FD7D9B0 +export function DRLGOUTDESR_PlaceBorders(pLevel: D2DrlgLevelStrc): void { + DRLGOUTDOORS_AddAct124SecondaryBorder(pLevel, 2, LVLPREST_ACT2_DESERT_BORDER_1) + DRLGOUTDOORS_AddAct124SecondaryBorder(pLevel, 1, LVLPREST_ACT2_DESERT_BORDER_1) + DRLGOUTDOORS_AddAct124SecondaryBorder(pLevel, 3, LVLPREST_ACT2_DESERT_BORDER_1) +} + +// D2Common.0x6FD7D9F0 +export function DRLGOUTDESR_AddExits(pLevel: D2DrlgLevelStrc): void { + let nLevelPrestId = 0 + + switch (pLevel.nLevelId) { + case LEVEL_ROCKYWASTE: + case LEVEL_DRYHILLS: + nLevelPrestId = LVLPREST_ACT2_DESERT_TOMB_1 + break + + case LEVEL_FAROASIS: + nLevelPrestId = LVLPREST_ACT2_DESERT_LAIR_1 + break + + case LEVEL_LOSTCITY: + nLevelPrestId = LVLPREST_ACT2_DESERT_RUINS_SEWER + break + + case LEVEL_VALLEYOFSNAKES: + nLevelPrestId = LVLPREST_ACT2_DESERT_TOMB_2 + break + + default: + throw new Error('sAddTheExitsAndStuff() - Could not add the exit!') + } + + if (!DRLGOUTDOORS_SpawnOutdoorLevelPreset(pLevel, nLevelPrestId, -1, 0, 15)) { + throw new Error('sAddTheExitsAndStuff() - Could not add the exit!') + } +} + +// D2Common.0x6FD7DA60 +export function DRLGOUTDESR_PlaceFillsInFarOasis(pLevel: D2DrlgLevelStrc): void { + const nLevelPrestIds1: readonly number[] = [ + LVLPREST_ACT2_DESERT_RUINS_08X08, + LVLPREST_ACT2_DESERT_FILL_HEAD_1, + LVLPREST_ACT2_DESERT_FILL_MESA_1, + LVLPREST_ACT2_DESERT_FILL_WAGON_1, + ] + const nLevelPrestIds2: readonly number[] = [LVLPREST_ACT2_DESERT_OASIS_1] + + DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds1, nLevelPrestIds1.length, false) + DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds2, nLevelPrestIds2.length, true) + DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds2, nLevelPrestIds2.length, true) +} + +// D2Common.0x6FD7DAC0 +export function DRLGOUTDESR_PlaceRuinsInLostCity(pLevel: D2DrlgLevelStrc): void { + const nLevelPrestIds: readonly number[] = [ + LVLPREST_ACT2_DESERT_RUINS_ELDER, + LVLPREST_ACT2_DESERT_RUINS_16X16, + LVLPREST_ACT2_DESERT_RUINS_16X08, + LVLPREST_ACT2_DESERT_RUINS_08X16, + ] + + DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds, nLevelPrestIds.length, false) +} + +// D2Common.0x6FD7DB00 +export function DRLGOUTDESR_PlaceFillsInLostCity(pLevel: D2DrlgLevelStrc): void { + const nLevelPrestIds1: readonly number[] = [ + LVLPREST_ACT2_DESERT_OASIS_1, + LVLPREST_ACT2_DESERT_FILL_HEAD_2, + LVLPREST_ACT2_DESERT_FILL_MESA_1, + LVLPREST_ACT2_DESERT_FILL_BERMS_1, + LVLPREST_ACT2_DESERT_FILL_BERMS_2, + ] + const nLevelPrestIds2: readonly number[] = [LVLPREST_ACT2_DESERT_RUINS_08X08] + + DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds1, nLevelPrestIds1.length, false) + DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds2, nLevelPrestIds2.length, true) + DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds2, nLevelPrestIds2.length, true) +} + +const pOutDesertCanyonInit: readonly D2DrlgOutDesertInitStrc[] = [ + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_KING_ENDS, nRand: 0, nX: 8, nY: 0 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_KING_TOMB, nRand: 2, nX: 6, nY: 0 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_KING_TOMB, nRand: 1, nX: 4, nY: 0 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_RIGHT_KING_TOMB, nRand: 0, nX: 2, nY: 0 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_TOP_KING_TOMB, nRand: 0, nX: 0, nY: 0 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_KING_TOMB, nRand: 0, nX: 0, nY: 2 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_KING_TOMB, nRand: 1, nX: 0, nY: 4 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_KING_TOMB, nRand: 2, nX: 0, nY: 6 }, + { nLvlPrestId: LVLPREST_ACT2_DESERT_CLIFF_LEFT_KING_ENDS, nRand: 0, nX: 0, nY: 8 }, +] + +// D2Common.0x6FD7DB70 +export function DRLGOUTDESR_PlaceTombEntriesInCanyon(pLevel: D2DrlgLevelStrc): void { + for (let i = 0; i < pOutDesertCanyonInit.length; i += 1) { + const entry = pOutDesertCanyonInit[i]! + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, entry.nX, entry.nY, entry.nLvlPrestId, entry.nRand, false) + } + + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 4, 4, LVLPREST_ACT2_DESERT_VALLEY_WARP, -1, false) +} + +// 1.10f: D2Common.0x6FD7DBC0 +// 1.11 : D2Common.0x6FDD5600 +export function DRLGOUTDESR_PlaceFillsInCanyon(pLevel: D2DrlgLevelStrc): void { + const nLevelPrestIds1: readonly number[] = [ + LVLPREST_ACT2_DESERT_FILL_BONE_1, + LVLPREST_ACT2_DESERT_FILL_BONE_2, + LVLPREST_ACT2_DESERT_FILL_BERMS_3, + LVLPREST_ACT2_DESERT_FILL_BERMS_4, + LVLPREST_ACT2_DESERT_FILL_WAGON_1, + ] + const nLevelPrestIds2: readonly number[] = [ + LVLPREST_ACT2_DESERT_VALLEY_RUIN_1, + LVLPREST_ACT2_DESERT_VALLEY_RUIN_2, + ] + + DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds1, nLevelPrestIds1.length, false) + DRLGOUTDESR_PlacePresetVariants(pLevel, nLevelPrestIds2, nLevelPrestIds2.length, true) +} diff --git a/src/game/drlg/drlg-outdoors.ts b/src/game/drlg/drlg-outdoors.ts index 1f1dce9..102d256 100644 --- a/src/game/drlg/drlg-outdoors.ts +++ b/src/game/drlg/drlg-outdoors.ts @@ -36,6 +36,7 @@ import { LEVEL_BLOODMOOR, LEVEL_COLDPLAINS, LEVEL_MONASTERYGATE, + LEVEL_PANDEMONIUMRUN2, LEVEL_ROGUEENCAMPMENT, grid2PickedFile, grid2PickedFileBits, @@ -96,13 +97,16 @@ import { DRLGROOM_AreXYInsideCoordinates, DRLGROOM_GetVisArrayFromLevelId } from import { DRLGPRESET_AllocDrlgMap, DRLGPRESET_BuildArea, DRLGPRESET_SetPickedFileInDrlgMap } from './drlg-preset.ts' import { DRLG_GetActNoFromLevelId } from './drlg-drlg.ts' import { notifyLevelGrid } from './drlg-hooks.ts' +import { DRLGOUTDESR_InitAct2OutdoorLevel } from './drlg-outdesr.ts' import { DRLGOUTWILD_GetBridgeCoords, DRLGOUTWILD_InitAct1OutdoorLevel } from './drlg-outwild.ts' import { DRLGOUTPLACE_CreateOutdoorRoomEx, + DRLGOUTPLACE_InitAct3OutdoorLevel, DRLGOUTPLACE_PlaceAct1245OutdoorBorders, DRLGOUTPLACE_SetOutGridLinkFlags, sub_6FD80750, } from './drlg-outplace.ts' +import { DRLGOUTSIEGE_InitAct5OutdoorLevel } from './drlg-outsiege.ts' import { DRLGTILESUB_AddSecondaryBorder } from './drlg-tilesub.ts' // D2C_Levels (DataTbls/LevelsIds.h) used only by the Act IV code of this file. @@ -794,34 +798,36 @@ export function DRLGOUTDOORS_GenerateLevel(pLevel: D2DrlgLevelStrc): void { pVertex = pVertex.pNext! } while (pVertex !== pOutdoorInfo.pVertex) - // #ifdef D2_VERSION_HAS_UBERS: LEVEL_PANDEMONIUMRUN2 -> ACT_II. Not compiled in the oracle, which - // builds D2MOO with the default D2_VERSION_110F of D2BuildInformation.h (either way not Act I). - const nAct = DRLG_GetActNoFromLevelId(pLevel.nLevelId) + const nAct = + pLevel.nLevelId === LEVEL_PANDEMONIUMRUN2 + ? ACT_II + : DRLG_GetActNoFromLevelId(pLevel.nLevelId) switch (nAct) { case ACT_I: DRLGOUTWILD_InitAct1OutdoorLevel(pLevel) - // Mirrors the oracle's link-time `--wrap=DRLGOUTWILD_InitAct1OutdoorLevel` - // (tools/d2moo-oracle/src/main.cpp ~line 241), which captures the `levelGrid` stage right after - // the real DRLGOUTWILD_InitAct1OutdoorLevel returns and before any room of the level exists. notifyLevelGrid(pLevel) break case ACT_II: - // DRLGOUTDESR_InitAct2OutdoorLevel(pLevel); - throw new Error('DRLGOUTDOORS_GenerateLevel: act 2 is not ported') + DRLGOUTDESR_InitAct2OutdoorLevel(pLevel) + notifyLevelGrid(pLevel) + break case ACT_III: - // DRLGOUTPLACE_InitAct3OutdoorLevel(pLevel); - throw new Error('DRLGOUTDOORS_GenerateLevel: act 3 is not ported') + DRLGOUTPLACE_InitAct3OutdoorLevel(pLevel) + notifyLevelGrid(pLevel) + break case ACT_IV: - // DRLGOUTDOORS_InitAct4OutdoorLevel(pLevel); (ported below, but the Act IV pipeline is out of scope) - throw new Error('DRLGOUTDOORS_GenerateLevel: act 4 is not ported') + DRLGOUTDOORS_InitAct4OutdoorLevel(pLevel) + notifyLevelGrid(pLevel) + break case ACT_V: - // DRLGOUTSIEGE_InitAct5OutdoorLevel(pLevel); - throw new Error('DRLGOUTDOORS_GenerateLevel: act 5 is not ported') + DRLGOUTSIEGE_InitAct5OutdoorLevel(pLevel) + notifyLevelGrid(pLevel) + break default: break diff --git a/src/game/drlg/drlg-outjung.ts b/src/game/drlg/drlg-outjung.ts new file mode 100644 index 0000000..7accf4d --- /dev/null +++ b/src/game/drlg/drlg-outjung.ts @@ -0,0 +1,280 @@ +/** + * D2Common Act III outdoor jungle/Kurast DRLG: 1:1 port of D2MOO + * `source/D2Common/src/Drlg/DrlgOutJung.cpp`. + */ + +import { SEED_RollLimitedRandomNumber } from '../d2-rng.ts' +import { + LVLPREST_ACT3_BURBS_BORDER_E, + LVLPREST_ACT3_BURBS_BORDER_N, + LVLPREST_ACT3_BURBS_BORDER_NE, + LVLPREST_ACT3_BURBS_BORDER_NW, + LVLPREST_ACT3_BURBS_BORDER_S, + LVLPREST_ACT3_BURBS_BORDER_SE, + LVLPREST_ACT3_BURBS_BORDER_SW, + LVLPREST_ACT3_BURBS_BORDER_W, + LVLPREST_ACT3_JUNGLE_HEAD, + LVLPREST_ACT3_JUNGLE_TAIL, + LVLPREST_ACT3_METRO_BORDER_E, + LVLPREST_ACT3_METRO_BORDER_N, + LVLPREST_ACT3_METRO_BORDER_NE, + LVLPREST_ACT3_METRO_BORDER_NW, + LVLPREST_ACT3_METRO_BORDER_S, + LVLPREST_ACT3_METRO_BORDER_SE, + LVLPREST_ACT3_METRO_BORDER_SW, + LVLPREST_ACT3_METRO_BORDER_W, + LVLPREST_ACT3_SLUMS_BORDER_E, + LVLPREST_ACT3_SLUMS_BORDER_N, + LVLPREST_ACT3_SLUMS_BORDER_NE, + LVLPREST_ACT3_SLUMS_BORDER_NW, + LVLPREST_ACT3_SLUMS_BORDER_S, + LVLPREST_ACT3_SLUMS_BORDER_SE, + LVLPREST_ACT3_SLUMS_BORDER_SW, + LVLPREST_ACT3_SLUMS_BORDER_W, +} from './drlg-ids.ts' +import { DRLGOUTDOORS_SpawnOutdoorLevelPresetEx, DRLGOUTDOORS_TestOutdoorLevelPreset } from './drlg-outdoors.ts' +import { DATATBLS_GetLevelDefRecord } from './drlg-tables.ts' +import { type D2DrlgLevelStrc, idiv, imod, LEVEL_FLAYERJUNGLE, LEVEL_SPIDERFOREST } from './drlg-types.ts' + +const dword_6FDCFB18: readonly number[] = [0, 10, 20, 0] + +const dword_6FDCFB28: readonly number[] = [0, 1, 2, 1, 0, 2, 0, 2, 1, 1, 2, 0, 2, 0, 1, 2, 1, 0] + +// D2Common.0x6FD7FC20 +export function DRLGOUTJUNG_BuildJungle(pLevel: D2DrlgLevelStrc): void { + if (pLevel.nLevelId >= LEVEL_SPIDERFOREST && pLevel.nLevelId <= LEVEL_FLAYERJUNGLE) { + const pLevelDefBin = DATATBLS_GetLevelDefRecord(pLevel.pDrlg.env.tables, LEVEL_SPIDERFOREST) + const nSizeX = pLevelDefBin.dwSizeX[pLevel.pDrlg.nDifficulty]! >> 5 + const nSizeY = pLevelDefBin.dwSizeY[pLevel.pDrlg.nDifficulty]! >> 5 + + const nRand = + SEED_RollLimitedRandomNumber(pLevel.pSeed, 4 * (pLevel.nJungleDefs === 3 ? 1 : 0) + 2) | 0 + + const pJungleDefs = pLevel.pJungleDefs! + let nDefId = 0 + let nFileIndex = 0 + + for (let i = 0; i < nSizeY; i += 1) { + if (pLevel.nLevelId === LEVEL_SPIDERFOREST && i === nSizeY - 1) { + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx( + pLevel, + 0, + 4 * i, + LVLPREST_ACT3_JUNGLE_HEAD, + pJungleDefs[nSizeX * nSizeY - 1] === 0 ? 1 : 0, + false, + ) + nDefId += 2 + } else if (pLevel.nLevelId === LEVEL_FLAYERJUNGLE && i === 0) { + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx( + pLevel, + 0, + 0, + LVLPREST_ACT3_JUNGLE_TAIL, + pJungleDefs[1] === 0 ? 1 : 0, + false, + ) + nDefId += 2 + } else { + for (let j = 0; j < nSizeX; j += 1) { + nDefId += 1 + let nJungleDef = pJungleDefs[nDefId - 1]! + let v19 = 0 + if (nJungleDef > LVLPREST_ACT3_JUNGLE_TAIL) { + nJungleDef += dword_6FDCFB18[pLevel.nLevelId - 76]! + v19 = dword_6FDCFB28[nFileIndex + 3 * nRand]! + nFileIndex += 1 + } else { + v19 = -1 + } + + if (nJungleDef) { + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 4 * j, 4 * i, nJungleDef, v19, false) + } + } + } + } + } +} + +// D2Common.0x6FD7FE50 +export function DRLGOUTJUNG_BuildLowerKurast(pLevel: D2DrlgLevelStrc): void { + const nWidth = idiv(pLevel.pLevelCoords.nWidth, 8) - 1 + const nHeight = idiv(pLevel.pLevelCoords.nHeight, 8) - 1 + const v5 = idiv(idiv(pLevel.pLevelCoords.nWidth, 8) - 1, 2) + const v10 = pLevel.pDrlg.bJungleInterlink ? 1 : nWidth - 1 + + for (let i = 1; i < nWidth; i += 1) { + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx( + pLevel, + i, + 0, + 8 * (i === v10 ? 1 : 0) + LVLPREST_ACT3_SLUMS_BORDER_N, + -1, + false, + ) + } + + for (let i = 1; i < nWidth; i += (i === v5 ? 1 : 0) + 1) { + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx( + pLevel, + i, + nHeight, + 8 * (i === v5 ? 1 : 0) + LVLPREST_ACT3_SLUMS_BORDER_S, + -1, + false, + ) + } + + for (let i = 1; i < nHeight; i += 1) { + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nWidth, i, LVLPREST_ACT3_SLUMS_BORDER_E, -1, false) + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, i, LVLPREST_ACT3_SLUMS_BORDER_W, -1, false) + } + + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, 0, LVLPREST_ACT3_SLUMS_BORDER_NW, -1, false) + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nWidth, 0, LVLPREST_ACT3_SLUMS_BORDER_NE, -1, false) + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, nHeight, LVLPREST_ACT3_SLUMS_BORDER_SW, -1, false) + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nWidth, nHeight, LVLPREST_ACT3_SLUMS_BORDER_SE, -1, false) +} + +// D2Common.0x6FD7FFA0 +export function DRLGOUTJUNG_BuildKurastBazaar(pLevel: D2DrlgLevelStrc): void { + const nWidth = idiv(pLevel.pLevelCoords.nWidth, 8) - 1 + const nHeight = idiv(pLevel.pLevelCoords.nHeight, 8) - 1 + let v7 = 0 + let v8 = 0 + + if (pLevel.pDrlg.bJungleInterlink) { + v7 = idiv(pLevel.pLevelCoords.nWidth, 8) - 2 + v8 = 1 + } else { + v7 = 1 + v8 = idiv(pLevel.pLevelCoords.nWidth, 8) - 2 + } + + for (let i = 1; i < nWidth; i += 1) { + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx( + pLevel, + i, + 0, + 8 * (i === v7 ? 1 : 0) + LVLPREST_ACT3_BURBS_BORDER_N, + -1, + false, + ) + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx( + pLevel, + i, + nHeight, + 8 * (i === v8 ? 1 : 0) + LVLPREST_ACT3_BURBS_BORDER_S, + -1, + false, + ) + } + + for (let i = 1; i < nHeight; i += 1) { + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nWidth, i, LVLPREST_ACT3_BURBS_BORDER_E, -1, false) + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, i, LVLPREST_ACT3_BURBS_BORDER_W, -1, false) + } + + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, 0, LVLPREST_ACT3_BURBS_BORDER_NW, -1, false) + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nWidth, 0, LVLPREST_ACT3_BURBS_BORDER_NE, -1, false) + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, nHeight, LVLPREST_ACT3_BURBS_BORDER_SW, -1, false) + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nWidth, nHeight, LVLPREST_ACT3_BURBS_BORDER_SE, -1, false) +} + +// D2Common.0x6FD800E0 +export function DRLGOUTJUNG_BuildUpperKurast(pLevel: D2DrlgLevelStrc): void { + const nWidth = idiv(pLevel.pLevelCoords.nWidth, 8) - 1 + const nHeight = idiv(pLevel.pLevelCoords.nHeight, 8) - 1 + const v8 = pLevel.pDrlg.bJungleInterlink ? nWidth - 1 : 1 + const nHalfWidth = idiv(nWidth, 2) + + for (let i = 1; i < nWidth; i += (i === nHalfWidth ? 1 : 0) + 1) { + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx( + pLevel, + i, + 0, + 8 * (i === nHalfWidth ? 1 : 0) + LVLPREST_ACT3_METRO_BORDER_N, + -1, + false, + ) + } + + for (let i = 1; i < nWidth; i += 1) { + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx( + pLevel, + i, + nHeight, + 8 * (i === v8 ? 1 : 0) + LVLPREST_ACT3_METRO_BORDER_S, + -1, + false, + ) + } + + for (let i = 1; i < nHeight; i += 1) { + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nWidth, i, LVLPREST_ACT3_METRO_BORDER_E, -1, false) + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, i, LVLPREST_ACT3_METRO_BORDER_W, -1, false) + } + + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, 0, LVLPREST_ACT3_METRO_BORDER_NW, -1, false) + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nWidth, 0, LVLPREST_ACT3_METRO_BORDER_NE, -1, false) + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, nHeight, LVLPREST_ACT3_METRO_BORDER_SW, -1, false) + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nWidth, nHeight, LVLPREST_ACT3_METRO_BORDER_SE, -1, false) +} + +// D2Common.0x6FD80230 +export function DRLGOUTJUNG_SpawnRandomPreset( + pLevel: D2DrlgLevelStrc, + nLevelPrestId1: number, + nLevelPrestId2: number, + a4: number, +): void { + const nVariants = (nLevelPrestId2 - nLevelPrestId1 + 1) >>> 0 + let v34 = 0 + + const pOutdoors = pLevel.pOutdoors! + const v8 = Math.imul(pOutdoors.nGridWidth, pOutdoors.nGridHeight) + + if (v8) { + const v11 = new Int32Array(2 * v8) + + for (let i = 0; i < v8; i += 1) { + const v12 = idiv(i, pOutdoors.nGridWidth) + const v13 = imod(i, pOutdoors.nGridWidth) + + v11[2 * i] = v13 + v11[2 * i + 1] = v12 + } + + for (let i = 0; i < v8; i += 1) { + const v14 = SEED_RollLimitedRandomNumber(pLevel.pSeed, v8) >>> 0 + const v19 = SEED_RollLimitedRandomNumber(pLevel.pSeed, v8) >>> 0 + + const v23 = v11[2 * v14]! + const v24 = v11[2 * v14 + 1]! + + v11[2 * v14] = v11[2 * v19]! + v11[2 * v14 + 1] = v11[2 * v19 + 1]! + + v11[2 * v19] = v23 + v11[2 * v19 + 1] = v24 + } + + for (let i = 0; i < v8; i += 1) { + const a2b = v11[2 * i]! + const a3a = v11[2 * i + 1]! + + const v26 = SEED_RollLimitedRandomNumber(pLevel.pSeed, nVariants) >>> 0 + + if (DRLGOUTDOORS_TestOutdoorLevelPreset(pLevel, a2b, a3a, v26 + nLevelPrestId1, 0, 15)) { + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, a2b, a3a, v26 + nLevelPrestId1, -1, false) + + v34 += 1 + + if (a4 > 0 && v34 >= a4) { + break + } + } + } + } +} diff --git a/src/game/drlg/drlg-outplace.ts b/src/game/drlg/drlg-outplace.ts index d3494f3..ce44065 100644 --- a/src/game/drlg/drlg-outplace.ts +++ b/src/game/drlg/drlg-outplace.ts @@ -46,6 +46,30 @@ import { LVLPREST_ACT2_DESERT_BORDER_10, LVLPREST_ACT2_DESERT_BORDER_11, LVLPREST_ACT2_DESERT_BORDER_12, + LVLPREST_ACT3_BRIDGE, + LVLPREST_ACT3_BURBS_08X08, + LVLPREST_ACT3_BURBS_08X16, + LVLPREST_ACT3_BURBS_16X08, + LVLPREST_ACT3_BURBS_16X16, + LVLPREST_ACT3_BURBS_SEWER, + LVLPREST_ACT3_BURBS_TEMPLE, + LVLPREST_ACT3_BURBS_WAYPOINT, + LVLPREST_ACT3_METRO_08X08, + LVLPREST_ACT3_METRO_08X16, + LVLPREST_ACT3_METRO_16X08, + LVLPREST_ACT3_METRO_16X16, + LVLPREST_ACT3_METRO_SEWER, + LVLPREST_ACT3_METROTEMPLE, + LVLPREST_ACT3_SLUMS_08X08, + LVLPREST_ACT3_SLUMS_08X16, + LVLPREST_ACT3_SLUMS_16X08, + LVLPREST_ACT3_SLUMS_16X16, + LVLPREST_ACT3_TRAVINCAL_N, + LVLPREST_ACT3_TRAVINCAL_NE, + LVLPREST_ACT3_TRAVINCAL_NW, + LVLPREST_ACT3_TRAVINCAL_S, + LVLPREST_ACT3_TRAVINCAL_SE, + LVLPREST_ACT3_TRAVINCAL_SW, LVLPREST_ACT4_MESA_BORDER_1, LVLPREST_ACT4_MESA_BORDER_2, LVLPREST_ACT4_MESA_BORDER_3, @@ -95,8 +119,17 @@ import { DRLG_OUTDOORS_GenerateDirtPath, DRLGOUTDOORS_GetOutLinkVisFlag, DRLGOUTDOORS_GetPackedGrid2Info, + DRLGOUTDOORS_SpawnOutdoorLevelPreset, DRLGOUTDOORS_SpawnOutdoorLevelPresetEx, } from './drlg-outdoors.ts' +import { + DRLGOUTJUNG_BuildJungle, + DRLGOUTJUNG_BuildKurastBazaar, + DRLGOUTJUNG_BuildLowerKurast, + DRLGOUTJUNG_BuildUpperKurast, + DRLGOUTJUNG_SpawnRandomPreset, +} from './drlg-outjung.ts' +import { sub_6FD84100 } from './drlg-outsiege.ts' import { DRLGROOM_AddRoomExToLevel, DRLGROOM_AllocRoomEx, DRLGROOM_AreXYInsideCoordinates } from './drlg-room.ts' import { DRLGROOMTILE_AllocTileGrid } from './drlg-roomtile.ts' import { DATATBLS_GetLevelDefRecord } from './drlg-tables.ts' @@ -129,12 +162,16 @@ import { i32, idiv, LEVEL_BURIALGROUNDS, + LEVEL_KURASTBAZAAR, + LEVEL_KURASTCAUSEWAY, + LEVEL_LOWERKURAST, + LEVEL_TRAVINCAL, + LEVEL_TUNDRAWASTELANDS, + LEVEL_UPPERKURAST, } from './drlg-types.ts' import { DRLGVER_AllocVertex, DRLGVER_GetCoordDiff } from './drlg-vertex.ts' import { sub_6FDAB750 } from './path-dir.ts' -const LEVEL_TUNDRAWASTELANDS = 112 - const gnBarricadeCliffBorderIds: readonly (readonly [number, number])[] = [ [LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_1, LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_1_SNOW], [LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_2, LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_2_SNOW], @@ -152,6 +189,101 @@ const gnBarricadeCliffBorderIds: readonly (readonly [number, number])[] = [ [-1, 0], ] +//D2Common.0x6FD80480 +export function DRLGOUTPLACE_BuildKurast(pLevel: D2DrlgLevelStrc): void { + switch (pLevel.nLevelId) { + case LEVEL_LOWERKURAST: + DRLGOUTJUNG_BuildLowerKurast(pLevel) + break + + case LEVEL_KURASTBAZAAR: + DRLGOUTJUNG_BuildKurastBazaar(pLevel) + break + + case LEVEL_UPPERKURAST: + DRLGOUTJUNG_BuildUpperKurast(pLevel) + break + + default: + break + } + + switch (pLevel.nLevelId) { + case LEVEL_LOWERKURAST: + DRLGOUTDOORS_SpawnOutdoorLevelPreset(pLevel, LVLPREST_ACT3_BURBS_WAYPOINT, 0, 0, 15) + DRLGOUTJUNG_SpawnRandomPreset(pLevel, LVLPREST_ACT3_SLUMS_16X16, LVLPREST_ACT3_SLUMS_16X16, 4) + DRLGOUTJUNG_SpawnRandomPreset(pLevel, LVLPREST_ACT3_SLUMS_08X16, LVLPREST_ACT3_SLUMS_16X08, 0) + DRLGOUTJUNG_SpawnRandomPreset(pLevel, LVLPREST_ACT3_SLUMS_08X08, LVLPREST_ACT3_SLUMS_08X08, 0) + break + + case LEVEL_KURASTBAZAAR: + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 3, 3, LVLPREST_ACT3_BURBS_SEWER, 0, false) + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx( + pLevel, + idiv(pLevel.pLevelCoords.nWidth, 8) - 4, + 3, + LVLPREST_ACT3_BURBS_SEWER, + 1, + false, + ) + DRLGOUTDOORS_SpawnOutdoorLevelPreset(pLevel, LVLPREST_ACT3_BURBS_TEMPLE, 0, 0, 15) + DRLGOUTDOORS_SpawnOutdoorLevelPreset(pLevel, LVLPREST_ACT3_BURBS_TEMPLE, 1, 0, 15) + DRLGOUTDOORS_SpawnOutdoorLevelPreset(pLevel, LVLPREST_ACT3_BURBS_WAYPOINT, 0, 0, 15) + DRLGOUTJUNG_SpawnRandomPreset(pLevel, LVLPREST_ACT3_BURBS_16X16, LVLPREST_ACT3_BURBS_16X16, 4) + DRLGOUTJUNG_SpawnRandomPreset(pLevel, LVLPREST_ACT3_BURBS_08X16, LVLPREST_ACT3_BURBS_16X08, 0) + DRLGOUTJUNG_SpawnRandomPreset(pLevel, LVLPREST_ACT3_BURBS_08X08, LVLPREST_ACT3_BURBS_08X08, 0) + break + + case LEVEL_UPPERKURAST: + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx( + pLevel, + 3, + idiv(pLevel.pLevelCoords.nHeight, 8) - 4, + LVLPREST_ACT3_METRO_SEWER, + 0, + false, + ) + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx( + pLevel, + idiv(pLevel.pLevelCoords.nWidth, 8) - 4, + idiv(pLevel.pLevelCoords.nHeight, 8) - 4, + LVLPREST_ACT3_METRO_SEWER, + 1, + false, + ) + DRLGOUTDOORS_SpawnOutdoorLevelPreset(pLevel, LVLPREST_ACT3_METROTEMPLE, 0, 0, 15) + DRLGOUTDOORS_SpawnOutdoorLevelPreset(pLevel, LVLPREST_ACT3_METROTEMPLE, 1, 0, 15) + DRLGOUTDOORS_SpawnOutdoorLevelPreset(pLevel, LVLPREST_ACT3_BURBS_WAYPOINT, 0, 0, 15) + DRLGOUTJUNG_SpawnRandomPreset(pLevel, LVLPREST_ACT3_METRO_16X16, LVLPREST_ACT3_METRO_16X16, 4) + DRLGOUTJUNG_SpawnRandomPreset(pLevel, LVLPREST_ACT3_METRO_08X16, LVLPREST_ACT3_METRO_16X08, 0) + DRLGOUTJUNG_SpawnRandomPreset(pLevel, LVLPREST_ACT3_METRO_08X08, LVLPREST_ACT3_METRO_08X08, 0) + break + + case LEVEL_KURASTCAUSEWAY: + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, 0, LVLPREST_ACT3_BRIDGE, 0, false) + break + + default: + return + } +} + +//D2Common.0x6FD806A0 +export function DRLGOUTPLACE_InitAct3OutdoorLevel(pLevel: D2DrlgLevelStrc): void { + DRLGOUTPLACE_SetOutGridLinkFlags(pLevel) + DRLGOUTJUNG_BuildJungle(pLevel) + DRLGOUTPLACE_BuildKurast(pLevel) + + if (pLevel.nLevelId === LEVEL_TRAVINCAL) { + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, 0, LVLPREST_ACT3_TRAVINCAL_NW, -1, false) + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 2, 0, LVLPREST_ACT3_TRAVINCAL_N, -1, false) + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 6, 0, LVLPREST_ACT3_TRAVINCAL_NE, -1, false) + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, 4, LVLPREST_ACT3_TRAVINCAL_SW, -1, false) + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 2, 4, LVLPREST_ACT3_TRAVINCAL_S, -1, false) + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 6, 4, LVLPREST_ACT3_TRAVINCAL_SE, -1, false) + } +} + interface D2UnkOutPlaceStrc12 { field_0: number field_4: number @@ -655,7 +787,8 @@ export function DRLGOUTPLACE_PlaceAct1245OutdoorBorders(pLevel: D2DrlgLevelStrc) break case LVLTYPE_ACT5_BARRICADE: - throw new Error('DRLGOUTPLACE_PlaceAct1245OutdoorBorders: act 5 is not ported') + nLevelPrestId = sub_6FD80BE0(nCurrentDiffX, nCurrentDiffY, sub_6FD84100(pLevel)) + break default: nLevelPrestId = sub_6FD80BE0(nCurrentDiffX, nCurrentDiffY, -1) @@ -750,7 +883,8 @@ export function DRLGOUTPLACE_PlaceAct1245OutdoorBorders(pLevel: D2DrlgLevelStrc) break case LVLTYPE_ACT5_BARRICADE: - throw new Error('DRLGOUTPLACE_PlaceAct1245OutdoorBorders: act 5 is not ported') + v41 = sub_6FD84100(pLevel) + break default: v41 = -1 diff --git a/src/game/drlg/drlg-outsiege.ts b/src/game/drlg/drlg-outsiege.ts new file mode 100644 index 0000000..dcf39e2 --- /dev/null +++ b/src/game/drlg/drlg-outsiege.ts @@ -0,0 +1,714 @@ +/** + * D2Common Act V outdoor siege/barricade DRLG: 1:1 port of D2MOO + * `source/D2Common/src/Drlg/DrlgOutSiege.cpp`. + */ + +import { SEED_RollLimitedRandomNumber } from '../d2-rng.ts' +import { DRLGGRID_AlterGridFlag, DRLGGRID_GetGridEntry } from './drlg-grid.ts' +import { + LVLPREST_ACT5_BARRICADE_1, + LVLPREST_ACT5_BARRICADE_1_SNOW, + LVLPREST_ACT5_BARRICADE_8, + LVLPREST_ACT5_BARRICADE_BUILDING, + LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_1, + LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_1_SNOW, + LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_3, + LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_3_SNOW, + LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_4, + LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_4_SNOW, + LVLPREST_ACT5_BARRICADE_CLIFF_RAVINE_BORDER_7, + LVLPREST_ACT5_BARRICADE_CLIFF_RAVINE_BORDER_7_SNOW, + LVLPREST_ACT5_BARRICADE_ENTRANCE_16X32, + LVLPREST_ACT5_BARRICADE_ENTRANCE_32X16, + LVLPREST_ACT5_BARRICADE_EXIT_16X32, + LVLPREST_ACT5_BARRICADE_EXIT_32X16, + LVLPREST_ACT5_BARRICADE_FILLER, + LVLPREST_ACT5_BARRICADE_FILLER_TREASURE, + LVLPREST_ACT5_BARRICADE_FROM_CAVE_16X32_SNOW, + LVLPREST_ACT5_BARRICADE_FROM_CAVE_32X16_SNOW, + LVLPREST_ACT5_BARRICADE_HELL_PORTAL_N, + LVLPREST_ACT5_BARRICADE_HELL_PORTAL_W, + LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_1, + LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_1_SNOW, + LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_3, + LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_3_SNOW, + LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_4, + LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_4_SNOW, + LVLPREST_ACT5_BARRICADE_RAVINE_CLIFF_BORDER_5, + LVLPREST_ACT5_BARRICADE_RAVINE_CLIFF_BORDER_5_SNOW, + LVLPREST_ACT5_BARRICADE_RUINS_N_1, + LVLPREST_ACT5_BARRICADE_RUINS_N_2, + LVLPREST_ACT5_BARRICADE_RUINS_N_TREASURE, + LVLPREST_ACT5_BARRICADE_RUINS_W_1, + LVLPREST_ACT5_BARRICADE_RUINS_W_2, + LVLPREST_ACT5_BARRICADE_RUINS_W_TREASURE, + LVLPREST_ACT5_BARRICADE_SNOW_LAKE_1, + LVLPREST_ACT5_BARRICADE_SNOW_LAKE_2, + LVLPREST_ACT5_BARRICADE_SNOW_OTHER, + LVLPREST_ACT5_BARRICADE_SNOW_TREASURE, + LVLPREST_ACT5_BARRICADE_TO_CAVE_16X32, + LVLPREST_ACT5_BARRICADE_TO_CAVE_16X32_SNOW, + LVLPREST_ACT5_BARRICADE_TO_CAVE_32X16, + LVLPREST_ACT5_BARRICADE_TO_CAVE_32X16_SNOW, + LVLPREST_ACT5_BARRICADE_TO_SIEGE, + LVLPREST_ACT5_BARRICADE_WAYPOINT_DIRT, + LVLPREST_ACT5_BARRICADE_WAYPOINT_SNOW, + LVLPREST_ACT5_SIEGE_TO_TOWN, + LVLSUB_ACT5_BARRICADE, +} from './drlg-ids.ts' +import { + DRLGOUTDOORS_AlterAdjacentPresetGridCells, + DRLGOUTDOORS_GetPackedGrid2Info, + DRLGOUTDOORS_GetPresetIndexFromGridCell, + DRLGOUTDOORS_SetBlankGridCell, + DRLGOUTDOORS_SpawnOutdoorLevelPreset, + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx, + DRLGOUTDOORS_TestGridCellNonLvlLink, +} from './drlg-outdoors.ts' +import { DRLGOUTPLACE_SetOutGridLinkFlags, sub_6FD80BE0, sub_6FD80C10 } from './drlg-outplace.ts' +import { DATATBLS_GetLvlPrestTxtRecord } from './drlg-tables.ts' +import { DRLGTILESUB_AddSecondaryBorder } from './drlg-tilesub.ts' +import { + type D2DrlgLevelStrc, + FLAG_OPERATION_OR, + GRID2_LVL_LINK, + GRID2_UNKB00, + idiv, + imod, + LEVEL_ARREATPLATEAU, + LEVEL_BLOODYFOOTHILLS, + LEVEL_ID_ACT5_BARRICADE_1, + LEVEL_TUNDRAWASTELANDS, + outdoorCoord, +} from './drlg-types.ts' +import { DRLGVER_GetCoordDiff } from './drlg-vertex.ts' + +// D2Common.0x6FD84100 +export function sub_6FD84100(pLevel: D2DrlgLevelStrc): number { + return (pLevel.nLevelId === LEVEL_TUNDRAWASTELANDS ? 1 : 0) + 4 +} + +const stru_6FDD09C8: readonly (readonly [number, number])[] = [ + [-1, 0], + [0, -1], + [1, 0], + [0, 1], + [0, -1], + [1, 0], + [0, 1], + [-1, 0], + [-1, 0], + [0, -1], + [1, 0], + [0, 1], +] + +// D2Common.0x6FD84110 +export function DRLGOUTSIEGE_InitAct5OutdoorLevel(pLevel: D2DrlgLevelStrc): void { + const pOutdoors = pLevel.pOutdoors! + if (pLevel.nLevelId === LEVEL_BLOODYFOOTHILLS) { + const pLvlPrestTxtRecord = DATATBLS_GetLvlPrestTxtRecord(pLevel.pDrlg.env.tables, LVLPREST_ACT5_SIEGE_TO_TOWN) + let nSize = pOutdoors.nGridWidth - idiv(pLvlPrestTxtRecord.nSizeX, 8) + for (let i = 0; i < 15; i += 1) { + if (nSize < 0) { + throw new Error('Siege Level is the wrong size') + } + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nSize, 0, i + LVLPREST_ACT5_SIEGE_TO_TOWN, 0, false) + nSize -= idiv(pLvlPrestTxtRecord.nSizeX, 8) + } + } else { + DRLGOUTPLACE_SetOutGridLinkFlags(pLevel) + + let pPreviousVertex = pOutdoors.pVertex! + let pDrlgVertex = pPreviousVertex.pNext! + + const nLookupId = sub_6FD84100(pLevel) + do { + const prevDiff = DRLGVER_GetCoordDiff(pPreviousVertex) + const curDiff = DRLGVER_GetCoordDiff(pDrlgVertex) + const nPreviousDiffX = prevDiff.nDiffX + const nPreviousDiffY = prevDiff.nDiffY + const nCurrentDiffX = curDiff.nDiffX + const nCurrentDiffY = curDiff.nDiffY + + const nPreviousDiffXAbs = Math.abs(nPreviousDiffX) + const nPreviousDiffYAbs = Math.abs(nPreviousDiffY) + + let nPreviousX = pPreviousVertex.nPosX & ~1 + let nPreviousY = pPreviousVertex.nPosY & ~1 + + const nCurrentX = pDrlgVertex.nPosX & ~1 + const nCurrentY = pDrlgVertex.nPosY & ~1 + + const nLevelPrestId = sub_6FD80BE0(nPreviousDiffX, nPreviousDiffY, nLookupId) + + if ((pPreviousVertex.dwFlags & 2) === 0) { + while (nPreviousX !== nCurrentX || nPreviousY !== nCurrentY) { + nPreviousX += 2 * nPreviousDiffX + nPreviousY += 2 * nPreviousDiffY + + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nPreviousX, nPreviousY, nLevelPrestId, -1, false) + DRLGGRID_AlterGridFlag(pOutdoors.pGrid[2], nPreviousX, nPreviousY, GRID2_UNKB00, FLAG_OPERATION_OR) + } + } + + if (pPreviousVertex.dwFlags & 1) { + const nX = Math.max(pPreviousVertex.nPosX, pDrlgVertex.nPosX) + const nXCapped = (nX - 4 * nPreviousDiffXAbs) & ~1 + const nY = Math.max(pPreviousVertex.nPosY, pDrlgVertex.nPosY) + const nYCapped = (nY - 4 * nPreviousDiffYAbs) & ~1 + + DRLGGRID_AlterGridFlag(pOutdoors.pGrid[2], nXCapped, nYCapped, GRID2_LVL_LINK, FLAG_OPERATION_OR) + DRLGGRID_AlterGridFlag( + pOutdoors.pGrid[2], + nXCapped + 2 * nPreviousDiffXAbs, + nYCapped + 2 * nPreviousDiffYAbs, + GRID2_LVL_LINK, + FLAG_OPERATION_OR, + ) + } + + const nRand = sub_6FD80C10( + 2 * nPreviousDiffX, + 2 * nPreviousDiffY, + 2 * nCurrentDiffX, + 2 * nCurrentDiffY, + nLookupId, + ) + if (nRand) { + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nCurrentX, nCurrentY, nRand, -1, false) + DRLGGRID_AlterGridFlag(pOutdoors.pGrid[2], nCurrentX, nCurrentY, GRID2_UNKB00, FLAG_OPERATION_OR) + } + + pPreviousVertex = pDrlgVertex + pDrlgVertex = pDrlgVertex.pNext! + } while (pPreviousVertex !== pOutdoors.pVertex) + + if (pLevel.nLevelId === LEVEL_ID_ACT5_BARRICADE_1) { + sub_6FD846C0(pLevel) + } + + const nLastX = pOutdoors.nGridWidth - 2 + const nLastY = pOutdoors.nGridHeight - 2 + let nX = nLastX + let nY = 0 + + const nLevelPrestIdCliff = + pLevel.nLevelId === LEVEL_TUNDRAWASTELANDS + ? LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_1_SNOW + : LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_1 + const nLevelPrestIdRavine = + pLevel.nLevelId === LEVEL_TUNDRAWASTELANDS + ? LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_1_SNOW + : LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_1 + + while (nX !== 0 || nY !== nLastY) { + const nIndex = DRLGGRID_GetGridEntry(pOutdoors.pGrid[0], nX, nY) - nLevelPrestIdCliff + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nX, nY, nIndex + nLevelPrestIdRavine, -1, false) + + nX += 2 * stru_6FDD09C8[nIndex]![0] + nY += 2 * stru_6FDD09C8[nIndex]![1] + } + + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx( + pLevel, + pOutdoors.nGridWidth - 2, + 0, + pLevel.nLevelId === LEVEL_TUNDRAWASTELANDS + ? LVLPREST_ACT5_BARRICADE_CLIFF_RAVINE_BORDER_7_SNOW + : LVLPREST_ACT5_BARRICADE_CLIFF_RAVINE_BORDER_7, + -1, + false, + ) + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx( + pLevel, + 0, + pOutdoors.nGridHeight - 2, + pLevel.nLevelId === LEVEL_TUNDRAWASTELANDS + ? LVLPREST_ACT5_BARRICADE_RAVINE_CLIFF_BORDER_5_SNOW + : LVLPREST_ACT5_BARRICADE_RAVINE_CLIFF_BORDER_5, + -1, + false, + ) + + DRLGOUTSIEGE_PlaceBarricadeEntrancesAndExits(pLevel) + DRLGOUTSIEGE_PlaceCaves(pLevel) + + if (pLevel.nLevelId === LEVEL_ID_ACT5_BARRICADE_1) { + DRLGOUTSIEGE_ConnectBarricadeAndSiege(pLevel) + } + + DRLGOUTSIEGE_AddACt5SecondaryBorder(pLevel) + DRLGOUTSIEGE_PlacePrisons(pLevel) + DRLGOUTSIEGE_PlaceSpecialPresets(pLevel) + } +} + +interface D2DrlgOutSiegeInitStrc { + readonly nLevelId: number + readonly field_4: number + readonly field_8: number + readonly nLevelPrestId1: number + readonly nLevelPrestId2: number +} + +const stru_6FDD0988: readonly D2DrlgOutSiegeInitStrc[] = [ + { + nLevelId: LEVEL_ARREATPLATEAU, + field_4: 0, + field_8: 0, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_TO_CAVE_32X16, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_TO_CAVE_16X32, + }, + { + nLevelId: LEVEL_TUNDRAWASTELANDS, + field_4: 0, + field_8: 1, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_FROM_CAVE_32X16_SNOW, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_FROM_CAVE_16X32_SNOW, + }, + { + nLevelId: LEVEL_TUNDRAWASTELANDS, + field_4: 0, + field_8: 0, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_TO_CAVE_32X16_SNOW, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_TO_CAVE_16X32_SNOW, + }, +] + +// D2Common.0x6FD844F0 +export function DRLGOUTSIEGE_PlaceCaves(pLevel: D2DrlgLevelStrc): void { + let nLevelPrestId = 0 + let nX = 0 + let nY = 0 + + for (let i = 0; i < stru_6FDD0988.length; i += 1) { + const entry = stru_6FDD0988[i]! + if (pLevel.nLevelId === entry.nLevelId) { + if (pLevel.pLevelCoords.nWidth <= pLevel.pLevelCoords.nHeight) { + nLevelPrestId = entry.nLevelPrestId1 + nX = 2 + if (entry.field_8) { + nY = pLevel.pOutdoors!.nGridHeight - 2 + } else { + nY = 0 + } + } else { + nLevelPrestId = entry.nLevelPrestId2 + nY = 2 + if (entry.field_8) { + nX = pLevel.pOutdoors!.nGridWidth - 2 + } else { + nX = 0 + } + } + + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nX, nY, nLevelPrestId, entry.field_4, false) + } + } +} + +// D2Common.0x6FD84580 +export function DRLGOUTSIEGE_PlaceBarricadeEntrancesAndExits(pLevel: D2DrlgLevelStrc): void { + const pOutdoors = pLevel.pOutdoors! + const nPickedFile = 2 * (pLevel.nLevelId === LEVEL_BLOODYFOOTHILLS ? 1 : 0) - 1 + + for (let i = 0; i < pOutdoors.nGridWidth; i += 1) { + if (DRLGOUTDOORS_GetPackedGrid2Info(pOutdoors, i, 0) & GRID2_LVL_LINK) { + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, i, 0, LVLPREST_ACT5_BARRICADE_EXIT_32X16, nPickedFile, false) + break + } + } + + for (let i = 0; i < pOutdoors.nGridWidth; i += 1) { + if (DRLGOUTDOORS_GetPackedGrid2Info(pOutdoors, i, pOutdoors.nGridHeight - 2) & GRID2_LVL_LINK) { + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx( + pLevel, + i, + pOutdoors.nGridHeight - 2, + LVLPREST_ACT5_BARRICADE_ENTRANCE_32X16, + nPickedFile, + false, + ) + break + } + } + + for (let i = 0; i < pOutdoors.nGridHeight; i += 1) { + if (DRLGOUTDOORS_GetPackedGrid2Info(pOutdoors, 0, i) & GRID2_LVL_LINK) { + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, 0, i, LVLPREST_ACT5_BARRICADE_EXIT_16X32, nPickedFile, false) + break + } + } + + for (let i = 0; i < pOutdoors.nGridHeight; i += 1) { + if (DRLGOUTDOORS_GetPackedGrid2Info(pOutdoors, pOutdoors.nGridWidth - 2, i) & GRID2_LVL_LINK) { + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx( + pLevel, + pOutdoors.nGridWidth - 2, + i, + LVLPREST_ACT5_BARRICADE_ENTRANCE_16X32, + nPickedFile, + false, + ) + break + } + } +} + +// D2Common.0x6FD846C0 +export function sub_6FD846C0(pLevel: D2DrlgLevelStrc): void { + const pOutdoors = pLevel.pOutdoors! + DRLGGRID_AlterGridFlag( + pOutdoors.pGrid[2], + pOutdoors.nGridWidth - 2, + pOutdoors.nGridHeight - 4, + GRID2_LVL_LINK, + FLAG_OPERATION_OR, + ) + DRLGGRID_AlterGridFlag( + pOutdoors.pGrid[2], + pOutdoors.nGridWidth - 2, + pOutdoors.nGridHeight - 3, + GRID2_LVL_LINK, + FLAG_OPERATION_OR, + ) +} + +// D2Common.0x6FD84700 +export function DRLGOUTSIEGE_AddACt5SecondaryBorder(pLevel: D2DrlgLevelStrc): void { + if (pLevel.nLevelId !== LEVEL_BLOODYFOOTHILLS) { + const a1 = { + pLevel, + field_4: outdoorCoord(pLevel.pOutdoors!), + pGrid1: pLevel.pOutdoors!.pGrid[0], + pGrid2: pLevel.pOutdoors!.pGrid[2], + nLevelPrestId: 0, + field_14: -1, + nLvlSubId: LVLSUB_ACT5_BARRICADE, + field_1C: null, + field_20: null, + field_24: sub_6FD84820, + field_28: sub_6FD84780, + field_2C: DRLGOUTDOORS_AlterAdjacentPresetGridCells, + field_30: DRLGOUTDOORS_SetBlankGridCell, + field_34: DRLGOUTDOORS_SpawnOutdoorLevelPresetEx, + } + + DRLGTILESUB_AddSecondaryBorder(a1) + } +} + +interface D2DrlgOutSiegeInitStrc2 { + readonly nStyle: number + readonly field_4: number + readonly field_8: number + readonly nLevelPrestId1: number + readonly nLevelPrestId2: number +} + +const stru_6FDD0A28: readonly D2DrlgOutSiegeInitStrc2[] = [ + { nStyle: 49, field_4: 1, field_8: 16, nLevelPrestId1: LVLPREST_ACT5_BARRICADE_1, nLevelPrestId2: LVLPREST_ACT5_BARRICADE_1_SNOW }, + { nStyle: 49, field_4: 31, field_8: 46, nLevelPrestId1: LVLPREST_ACT5_BARRICADE_1, nLevelPrestId2: LVLPREST_ACT5_BARRICADE_1_SNOW }, + { + nStyle: 48, + field_4: 1, + field_8: 1, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_3, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_3_SNOW, + }, + { + nStyle: 48, + field_4: 2, + field_8: 3, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_1, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_1_SNOW, + }, + { + nStyle: 48, + field_4: 4, + field_8: 4, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_4, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_CLIFF_BORDER_4_SNOW, + }, + { + nStyle: 48, + field_4: 5, + field_8: 5, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_3, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_3_SNOW, + }, + { + nStyle: 48, + field_4: 6, + field_8: 7, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_1, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_1_SNOW, + }, + { + nStyle: 48, + field_4: 8, + field_8: 8, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_4, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_4_SNOW, + }, + { nStyle: 48, field_4: 30, field_8: 30, nLevelPrestId1: 0, nLevelPrestId2: 0 }, + { nStyle: 48, field_4: 31, field_8: 31, nLevelPrestId1: -5, nLevelPrestId2: -5 }, +] + +// D2Common.0x6FD84780 +export function sub_6FD84780(pLevel: D2DrlgLevelStrc, nStyle: number, a3: number): number { + if (nStyle !== 48 && nStyle !== 49) { + throw new Error(`sub_6FD84780: unexpected nStyle=${nStyle}`) + } + + for (let i = 0; i < stru_6FDD0A28.length; i += 1) { + const entry = stru_6FDD0A28[i]! + if (nStyle === entry.nStyle && a3 >= entry.field_4 && a3 <= entry.field_8) { + if (pLevel.nLevelId === LEVEL_TUNDRAWASTELANDS) { + return a3 + entry.nLevelPrestId2 - entry.field_4 + } else { + return a3 + entry.nLevelPrestId1 - entry.field_4 + } + } + } + + throw new Error(`sub_6FD84780: unreachable nStyle=${nStyle} a3=${a3}`) +} + +// D2Common.0x6FD84820 +export function sub_6FD84820( + pLevel: D2DrlgLevelStrc, + nX: number, + nY: number, + a4: number, + _a5: number, + a6: number, +): boolean { + const v8 = sub_6FD84780(pLevel, (a6 >>> 20) & 0x3f, (a6 >>> 8) & 0xff) + + if (v8 === -5) { + return true + } else if (v8 === a4) { + return DRLGOUTDOORS_TestGridCellNonLvlLink(pLevel, nX, nY) !== 0 + } + + return false +} + +interface D2DrlgOutSiegeInitStrc3 { + readonly nLevelId: number + readonly nLevelPrestId1: number + readonly nLevelPrestId2: number + readonly field_C: number + readonly field_10: number + readonly field_14: number + readonly bMustHave: boolean +} + +const stru_6FDD0AF8: readonly D2DrlgOutSiegeInitStrc3[] = [ + { + nLevelId: LEVEL_ID_ACT5_BARRICADE_1, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_HELL_PORTAL_N, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_HELL_PORTAL_W, + field_C: 0, + field_10: 25, + field_14: 1, + bMustHave: true, + }, + { + nLevelId: LEVEL_ARREATPLATEAU, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_HELL_PORTAL_N, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_HELL_PORTAL_W, + field_C: 0, + field_10: 25, + field_14: 1, + bMustHave: true, + }, + { + nLevelId: LEVEL_TUNDRAWASTELANDS, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_HELL_PORTAL_N, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_HELL_PORTAL_W, + field_C: 1, + field_10: 25, + field_14: 1, + bMustHave: true, + }, + { + nLevelId: LEVEL_ARREATPLATEAU, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_WAYPOINT_DIRT, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_WAYPOINT_DIRT, + field_C: -1, + field_10: 25, + field_14: 1, + bMustHave: true, + }, + { + nLevelId: LEVEL_TUNDRAWASTELANDS, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_WAYPOINT_SNOW, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_WAYPOINT_SNOW, + field_C: -1, + field_10: 25, + field_14: 1, + bMustHave: true, + }, + { + nLevelId: LEVEL_ID_ACT5_BARRICADE_1, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_RUINS_N_TREASURE, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_RUINS_W_TREASURE, + field_C: -1, + field_10: 10, + field_14: 1, + bMustHave: false, + }, + { + nLevelId: LEVEL_ID_ACT5_BARRICADE_1, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_RUINS_N_1, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_RUINS_W_1, + field_C: -1, + field_10: 20, + field_14: 4, + bMustHave: false, + }, + { + nLevelId: LEVEL_ID_ACT5_BARRICADE_1, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_RUINS_N_2, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_RUINS_W_2, + field_C: -1, + field_10: 20, + field_14: 4, + bMustHave: false, + }, + { + nLevelId: LEVEL_ARREATPLATEAU, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_FILLER_TREASURE, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_FILLER_TREASURE, + field_C: -1, + field_10: 10, + field_14: 1, + bMustHave: false, + }, + { + nLevelId: LEVEL_ARREATPLATEAU, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_BUILDING, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_BUILDING, + field_C: -1, + field_10: 8, + field_14: 1, + bMustHave: false, + }, + { + nLevelId: LEVEL_ARREATPLATEAU, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_FILLER, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_FILLER, + field_C: -1, + field_10: 15, + field_14: 5, + bMustHave: false, + }, + { + nLevelId: LEVEL_TUNDRAWASTELANDS, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_SNOW_LAKE_1, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_SNOW_LAKE_1, + field_C: -1, + field_10: 9, + field_14: 4, + bMustHave: false, + }, + { + nLevelId: LEVEL_TUNDRAWASTELANDS, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_SNOW_LAKE_2, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_SNOW_LAKE_2, + field_C: -1, + field_10: 9, + field_14: 4, + bMustHave: false, + }, + { + nLevelId: LEVEL_TUNDRAWASTELANDS, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_SNOW_OTHER, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_SNOW_OTHER, + field_C: -1, + field_10: 9, + field_14: 4, + bMustHave: false, + }, + { + nLevelId: LEVEL_TUNDRAWASTELANDS, + nLevelPrestId1: LVLPREST_ACT5_BARRICADE_SNOW_TREASURE, + nLevelPrestId2: LVLPREST_ACT5_BARRICADE_SNOW_TREASURE, + field_C: -1, + field_10: 5, + field_14: 3, + bMustHave: false, + }, +] + +// D2Common.0x6FD84870 +export function DRLGOUTSIEGE_PlaceSpecialPresets(pLevel: D2DrlgLevelStrc): void { + for (let i = 0; i < stru_6FDD0AF8.length; i += 1) { + const entry = stru_6FDD0AF8[i]! + if (pLevel.nLevelId === entry.nLevelId) { + const nLevelPrestId = + pLevel.pLevelCoords.nWidth < pLevel.pLevelCoords.nHeight ? entry.nLevelPrestId1 : entry.nLevelPrestId2 + + let bAdded = false + for (let j = 0; j < entry.field_14; j += 1) { + bAdded = DRLGOUTDOORS_SpawnOutdoorLevelPreset(pLevel, nLevelPrestId, entry.field_C, 0, 15) + } + + if (!bAdded && entry.bMustHave) { + throw new Error(`DRLGOUTSIEGE_PlaceSpecialPresets: failed to place required preset ${nLevelPrestId}`) + } + } + } +} + +// D2Common.0x6FD84910 +export function DRLGOUTSIEGE_PlacePrisons(pLevel: D2DrlgLevelStrc): void { + let nPrisonsPlaced = 0 + const nPrisonsToPlace = 3 + + if (pLevel.nLevelId === LEVEL_ID_ACT5_BARRICADE_1) { + const pOutdoors = pLevel.pOutdoors! + + for (let nAttemptNumber = 0; nAttemptNumber < 90 && nPrisonsPlaced < nPrisonsToPlace; nAttemptNumber += 1) { + const nRandX = 2 * (SEED_RollLimitedRandomNumber(pLevel.pSeed, idiv(pOutdoors.nGridWidth, 2)) | 0) + const nRandY = 2 * (SEED_RollLimitedRandomNumber(pLevel.pSeed, idiv(pOutdoors.nGridHeight, 2)) | 0) + + const nLevelPrestId = DRLGOUTDOORS_GetPresetIndexFromGridCell(pLevel, nRandX, nRandY) + if (nLevelPrestId >= LVLPREST_ACT5_BARRICADE_1 && nLevelPrestId <= LVLPREST_ACT5_BARRICADE_8) { + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nRandX, nRandY, nLevelPrestId + 16, -1, false) + nPrisonsPlaced += 1 + } + } + + const nRandX = 2 * (SEED_RollLimitedRandomNumber(pLevel.pSeed, idiv(pOutdoors.nGridWidth, 2)) | 0) + const nRandY = 2 * (SEED_RollLimitedRandomNumber(pLevel.pSeed, idiv(pOutdoors.nGridHeight, 2)) | 0) + + for (let j = 0; j < pOutdoors.nGridHeight && nPrisonsPlaced < nPrisonsToPlace; j += 1) { + for (let i = 0; i < pOutdoors.nGridWidth && nPrisonsPlaced < nPrisonsToPlace; i += 1) { + const nX = imod(i + nRandX, pOutdoors.nGridWidth) + const nY = imod(j + nRandY, pOutdoors.nGridHeight) + + const nLevelPrestId = DRLGOUTDOORS_GetPresetIndexFromGridCell(pLevel, nX, nY) + if (nLevelPrestId >= LVLPREST_ACT5_BARRICADE_1 && nLevelPrestId <= LVLPREST_ACT5_BARRICADE_8) { + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nX, nY, nLevelPrestId + 16, -1, false) + nPrisonsPlaced += 1 + } + } + } + } +} + +// D2Common.0x6FD84BB0 +export function DRLGOUTSIEGE_ConnectBarricadeAndSiege(pLevel: D2DrlgLevelStrc): void { + const pOutdoors = pLevel.pOutdoors! + const pLvlPrestTxtRecord = DATATBLS_GetLvlPrestTxtRecord(pLevel.pDrlg.env.tables, LVLPREST_ACT5_BARRICADE_TO_SIEGE) + + const nX = pOutdoors.nGridWidth - idiv(pLvlPrestTxtRecord.nSizeX, 8) + const nY = pOutdoors.nGridHeight - idiv(pLvlPrestTxtRecord.nSizeY, 8) + + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nX, nY, LVLPREST_ACT5_BARRICADE_TO_SIEGE, -1, false) + DRLGOUTDOORS_SpawnOutdoorLevelPresetEx(pLevel, nX, nY - 2, LVLPREST_ACT5_BARRICADE_RAVINE_BORDER_4, -1, false) +} diff --git a/src/game/drlg/drlg-preset.ts b/src/game/drlg/drlg-preset.ts index bd6b867..c26d838 100644 --- a/src/game/drlg/drlg-preset.ts +++ b/src/game/drlg/drlg-preset.ts @@ -11,6 +11,7 @@ */ import { SEED_RollLimitedRandomNumber, SEED_RollRandomNumber, type D2SeedStrc } from '../d2-rng.ts' +import { DRLGANIM_AllocAnimationTileGrids, DRLGANIM_TestLoadAnimatedRoomTiles } from './drlg-anim.ts' import { DATATBLS_GetLvlPrestTxtRecord, DATATBLS_GetLvlPrestTxtRecordFromLevelId, @@ -1142,11 +1143,6 @@ export function DRLGPRESET_InitPresetRoomGrids(pDrlgRoom: D2DrlgRoomStrc): void } } -// DrlgDrlgAnim.cpp is not ported: no Act I LvlPrest.txt row sets Animate (the call sites throw instead). -function DRLGANIM_NotPorted(szFunction: string): never { - throw new Error(`${szFunction} is not ported (LvlPrest.txt Animate is set)`) -} - //D2Common.0x6FD87130 export function DRLGPRESET_AddPresetRoomMapTiles(pDrlgRoom: D2DrlgRoomStrc): void { DRLGROOMTILE_AllocTileGrid(pDrlgRoom) @@ -1198,7 +1194,7 @@ export function DRLGPRESET_AddPresetRoomMapTiles(pDrlgRoom: D2DrlgRoomStrc): voi DRLGROOMTILE_CountAllTileTypes(pDrlgRoom, pMaze.pFloorGrid[i]!, bCheckCoordinatesValidity, bKillEdgeX, bKillEdgeY) if (pMazeMap.pLvlPrestTxtRecord.dwAnimate) { - DRLGANIM_NotPorted('DRLGANIM_TestLoadAnimatedRoomTiles') + DRLGANIM_TestLoadAnimatedRoomTiles(pDrlgRoom, pMaze.pFloorGrid[i]!, null, 0, bKillEdgeX, bKillEdgeY) } } @@ -1207,14 +1203,21 @@ export function DRLGPRESET_AddPresetRoomMapTiles(pDrlgRoom: D2DrlgRoomStrc): voi DRLGROOMTILE_CountAllTileTypes(pDrlgRoom, pMaze.pWallGrid[i]!, false, bKillEdgeX, bKillEdgeY) if (pMazeMap.pLvlPrestTxtRecord.dwAnimate) { - DRLGANIM_NotPorted('DRLGANIM_TestLoadAnimatedRoomTiles') + DRLGANIM_TestLoadAnimatedRoomTiles( + pDrlgRoom, + pMaze.pWallGrid[i]!, + pMaze.pTileTypeGrid[i]!, + TILETYPE_FLOOR, + bKillEdgeX, + bKillEdgeY, + ) } } DRLGROOMTILE_CountAllTileTypes(pDrlgRoom, pMaze.pCellGrid, false, bKillEdgeX, bKillEdgeY) if (pMazeMap.pLvlPrestTxtRecord.dwAnimate) { - DRLGANIM_NotPorted('DRLGANIM_TestLoadAnimatedRoomTiles') + DRLGANIM_TestLoadAnimatedRoomTiles(pDrlgRoom, pMaze.pCellGrid, null, TILETYPE_SHADOW, bKillEdgeX, bKillEdgeY) } if (bTownOrGraveyard) { @@ -1239,7 +1242,15 @@ export function DRLGPRESET_AddPresetRoomMapTiles(pDrlgRoom: D2DrlgRoomStrc): voi } if (pMazeMap.pLvlPrestTxtRecord.dwAnimate) { - DRLGANIM_NotPorted('DRLGANIM_AllocAnimationTileGrids') + DRLGANIM_AllocAnimationTileGrids( + pDrlgRoom, + pMazeMap.pLvlPrestTxtRecord.nAnimSpeed, + pMaze.pWallGrid, + pFile.nWallLayers, + pMaze.pFloorGrid, + pFile.nFloorLayers, + pMaze.pCellGrid, + ) } const pTileGrid = tileGridOf(pDrlgRoom) diff --git a/src/game/drlg/drlg-room.ts b/src/game/drlg/drlg-room.ts index d4d59a8..edcd78a 100644 --- a/src/game/drlg/drlg-room.ts +++ b/src/game/drlg/drlg-room.ts @@ -470,40 +470,12 @@ export function sub_6FD77BB0(pDrlgRoom: D2DrlgRoomStrc): void { pDrlgRoom.ppRoomsNear = ppRoomList pDrlgRoom.nRoomsNear = nNearRooms - for (let pOrth = pDrlgRoom.pDrlgOrth; pOrth; pOrth = pOrth.pNext) { - if (pOrth.bPreset) { - const pLevel = pOrth.pLevel - if (pLevel) { - for (let pNearRoom = pLevel.pFirstRoomEx; pNearRoom; pNearRoom = pNearRoom.pDrlgRoomNext) { - let nX = pDrlgRoom.pDrlgCoord.nPosX - pNearRoom.pDrlgCoord.nWidth - pNearRoom.pDrlgCoord.nPosX - if (nX < 0) { - nX = pNearRoom.pDrlgCoord.nPosX - (pDrlgRoom.pDrlgCoord.nPosX + pDrlgRoom.pDrlgCoord.nWidth) - } - - let nY = pDrlgRoom.pDrlgCoord.nPosY - pNearRoom.pDrlgCoord.nHeight - pNearRoom.pDrlgCoord.nPosY - if (nY < 0) { - nY = pNearRoom.pDrlgCoord.nPosY - (pDrlgRoom.pDrlgCoord.nPosY + pDrlgRoom.pDrlgCoord.nHeight) - } - - if (nX < 0) nX = 0 - if (nY < 0) nY = 0 - - if (nX < 6 && nY < 6) { - pDrlgRoom.ppRoomsNear.push(pNearRoom) - pDrlgRoom.nRoomsNear += 1 - DRLGROOM_SortRoomListByPosition(pDrlgRoom.ppRoomsNear, pDrlgRoom.nRoomsNear) - } - } - } - } - } - if (pDrlgRoom.dwFlags & DRLGROOMFLAG_HAS_WARP_MASK) { let nFlags = DRLGROOMFLAG_HAS_WARP_0 let nWarpId = 0 do { - if (nFlags & pDrlgRoom.dwFlags) { + if (nFlags & pDrlgRoom.dwFlags && pDrlgRoom.pLevel.nLevelId !== 133) { const pSourceVisArray = DRLGROOM_GetVisArrayFromLevelId(pDrlg, pDrlgRoom.pLevel.nLevelId) const pLevel = DRLG_GetLevel(pDrlg, pSourceVisArray[nWarpId]!) const pDestinationVisArray = DRLGROOM_GetVisArrayFromLevelId(pDrlg, pSourceVisArray[nWarpId]!) @@ -513,11 +485,22 @@ export function sub_6FD77BB0(pDrlgRoom: D2DrlgRoomStrc): void { } if (nWarpDestination !== -1) { + let nSourceVisCount = 0 + for (let i = 0; i <= nWarpId; i += 1) { + if (pSourceVisArray[i] === pLevel.nLevelId) { + nSourceVisCount += 1 + } + } + + let nDestinationVisCount = 0 for (let i = 0; i < 8; i += 1) { if (pDestinationVisArray[i] === pDrlgRoom.pLevel.nLevelId) { + nDestinationVisCount += 1 + } + + if (nSourceVisCount === nDestinationVisCount) { const pNearRoom = pLevel.pFirstRoomEx - const nWarpDest = DRLGWARP_GetWarpDestinationFromArray(pLevel, i) - if (nWarpDest === nWarpId && sub_6FD77F00(pDrlgRoom, nWarpId, pNearRoom, i, nWarpDestination)) { + if (sub_6FD77F00(pDrlgRoom, nWarpId, pNearRoom, i, nWarpDestination)) { bSkip = true break } @@ -529,8 +512,7 @@ export function sub_6FD77BB0(pDrlgRoom: D2DrlgRoomStrc): void { for (let i = 0; i < 8; i += 1) { if (pDestinationVisArray[i] === pDrlgRoom.pLevel.nLevelId) { const pNearRoom = pLevel.pFirstRoomEx - const nWarpDest = DRLGWARP_GetWarpDestinationFromArray(pLevel, i) - if (sub_6FD77F00(pDrlgRoom, nWarpId, pNearRoom, i, nWarpDest)) { + if (sub_6FD77F00(pDrlgRoom, nWarpId, pNearRoom, i, nWarpDestination)) { break } } diff --git a/src/game/drlg/drlg-roomtile.ts b/src/game/drlg/drlg-roomtile.ts index 8223f90..3fbd252 100644 --- a/src/game/drlg/drlg-roomtile.ts +++ b/src/game/drlg/drlg-roomtile.ts @@ -274,10 +274,9 @@ function DUNGEON_GameTileToClientCoords(nX: number, nY: number): { nX: number; n return { nX: Math.imul(80, (nX - nY) | 0), nY: (Math.imul(80, (nX + nY) | 0) / 2) | 0 } } -// MONSTERS_ValidateMonsterId (D2Common Monsters.cpp) needs the MonStats table logic, which the DRLG port -// does not include; the native oracle stubs it as a hard failure too. Only reached for Act V levels. -function MONSTERS_ValidateMonsterId(nMonsterId: number): number { - throw new Error(`MONSTERS_ValidateMonsterId(${nMonsterId}) is not ported (Act V tile preset units)`) +// MONSTERS_ValidateMonsterId (D2Common Monsters.cpp) +function MONSTERS_ValidateMonsterId(pDrlgRoom: D2DrlgRoomStrc, nMonsterId: number): number { + return nMonsterId >= 0 && nMonsterId < pDrlgRoom.pLevel.pDrlg.env.tables.nMonStatsTxtRecordCount ? nMonsterId : -1 } // --------------------------------------------------------------------------------------------- @@ -649,7 +648,7 @@ export function DRLGROOMTILE_AddTilePresetUnits( DRLGROOM_AllocPresetUnit( pDrlgRoom, UNIT_MONSTER, - MONSTERS_ValidateMonsterId(nUnitId), + MONSTERS_ValidateMonsterId(pDrlgRoom, nUnitId), MONMODE_NEUTRAL, nPosX, nPosY, @@ -936,8 +935,10 @@ export function DRLGROOMTILE_LoadInitRoomTiles( pTileCache, nTileType, ) - // D2_VERSION_HAS_UBERS is not defined for 1.13c. - if (nTileType === TILETYPE_WALL_RIGHT_EXIT || nTileType === TILETYPE_WALL_LEFT_EXIT) { + if ( + (nTileType === TILETYPE_WALL_RIGHT_EXIT || nTileType === TILETYPE_WALL_LEFT_EXIT) && + pDrlgRoom.pLevel.nLevelId !== 133 + ) { DRLGROOMTILE_LoadWallWarpTiles(pDrlgRoom, pWallTileData, nTileInformation, nTileType) } } @@ -1243,7 +1244,7 @@ const nRemapIndices: readonly number[] = [ 3, // [TILETYPE_WALL_TOP_RIGHT ] 4, // [TILETYPE_WALL_BOTTOM_LEFT ] 5, // [TILETYPE_WALL_BOTTOM_RIGHT ] - -2, -2, -1, -1, -1, -2, -1, -1, -1, -1, -1, + -2, -2, -1, -1, -1, -2, -1, -1, -1, -1, -1, 0, 0, ] //D2Common.0x6FDD1390 @@ -1270,7 +1271,7 @@ export function DRLGROOMTILE_LinkedTileDataManager( ): void { const nTileInformation = nPackedTileInformation >>> 0 if (nTileType < 0 || nTileType >= nRemapIndices.length) { - throw new RangeError(`DRLGROOMTILE_LinkedTileDataManager: nRemapIndices[${nTileType}] is outside the 19-entry table`) + throw new RangeError(`DRLGROOMTILE_LinkedTileDataManager: nRemapIndices[${nTileType}] is outside the table`) } const v10 = nRemapIndices[nTileType]! diff --git a/src/game/drlg/drlg-types.ts b/src/game/drlg/drlg-types.ts index 281ba0d..8298e03 100644 --- a/src/game/drlg/drlg-types.ts +++ b/src/game/drlg/drlg-types.ts @@ -144,7 +144,7 @@ export const FLAG_OPERATION_OVERWRITE_IF_ZERO = 4 export const FLAG_OPERATION_AND_NEGATED = 5 export type FlagOperation = 0 | 1 | 2 | 3 | 4 | 5 -/** D2C_Levels (DataTbls/LevelsIds.h), the ids the Act I DRLG code refers to. */ +/** D2C_Levels (DataTbls/LevelsIds.h). */ export const LEVEL_NONE = 0 export const LEVEL_ROGUEENCAMPMENT = 1 export const LEVEL_BLOODMOOR = 2 @@ -166,14 +166,122 @@ export const LEVEL_BURIALGROUNDS = 17 export const LEVEL_CRYPT = 18 export const LEVEL_MAUSOLEUM = 19 export const LEVEL_FORGOTTENTOWER = 20 +export const LEVEL_TOWERCELLARLEV1 = 21 +export const LEVEL_TOWERCELLARLEV2 = 22 +export const LEVEL_TOWERCELLARLEV3 = 23 +export const LEVEL_TOWERCELLARLEV4 = 24 +export const LEVEL_TOWERCELLARLEV5 = 25 export const LEVEL_MONASTERYGATE = 26 export const LEVEL_OUTERCLOISTER = 27 +export const LEVEL_BARRACKS = 28 +export const LEVEL_JAILLEV1 = 29 +export const LEVEL_JAILLEV2 = 30 +export const LEVEL_JAILLEV3 = 31 +export const LEVEL_INNERCLOISTER = 32 +export const LEVEL_CATHEDRAL = 33 +export const LEVEL_CATACOMBSLEV1 = 34 +export const LEVEL_CATACOMBSLEV2 = 35 +export const LEVEL_CATACOMBSLEV3 = 36 +export const LEVEL_CATACOMBSLEV4 = 37 export const LEVEL_TRISTRAM = 38 export const LEVEL_MOOMOOFARM = 39 export const LEVEL_LUTGHOLEIN = 40 +export const LEVEL_ROCKYWASTE = 41 +export const LEVEL_DRYHILLS = 42 +export const LEVEL_FAROASIS = 43 +export const LEVEL_LOSTCITY = 44 +export const LEVEL_VALLEYOFSNAKES = 45 +export const LEVEL_CANYONOFTHEMAGI = 46 +export const LEVEL_SEWERSLEV1 = 47 +export const LEVEL_SEWERSLEV2 = 48 +export const LEVEL_SEWERSLEV3 = 49 +export const LEVEL_HAREMLEV1 = 50 +export const LEVEL_HAREMLEV2 = 51 +export const LEVEL_PALACECELLARLEV1 = 52 +export const LEVEL_PALACECELLARLEV2 = 53 +export const LEVEL_PALACECELLARLEV3 = 54 +export const LEVEL_STONYTOMBLEV1 = 55 +export const LEVEL_HALLSOFTHEDEADLEV1 = 56 +export const LEVEL_HALLSOFTHEDEADLEV2 = 57 +export const LEVEL_CLAWVIPERTEMPLELEV1 = 58 +export const LEVEL_STONYTOMBLEV2 = 59 +export const LEVEL_HALLSOFTHEDEADLEV3 = 60 +export const LEVEL_CLAWVIPERTEMPLELEV2 = 61 +export const LEVEL_MAGGOTLAIRLEV1 = 62 +export const LEVEL_MAGGOTLAIRLEV2 = 63 +export const LEVEL_MAGGOTLAIRLEV3 = 64 +export const LEVEL_ANCIENTTUNNELS = 65 +export const LEVEL_TALRASHASTOMB1 = 66 +export const LEVEL_TALRASHASTOMB2 = 67 +export const LEVEL_TALRASHASTOMB3 = 68 +export const LEVEL_TALRASHASTOMB4 = 69 +export const LEVEL_TALRASHASTOMB5 = 70 +export const LEVEL_TALRASHASTOMB6 = 71 +export const LEVEL_TALRASHASTOMB7 = 72 +export const LEVEL_DURIELSLAIR = 73 +export const LEVEL_ARCANESANCTUARY = 74 export const LEVEL_KURASTDOCKTOWN = 75 +export const LEVEL_SPIDERFOREST = 76 +export const LEVEL_GREATMARSH = 77 +export const LEVEL_FLAYERJUNGLE = 78 +export const LEVEL_LOWERKURAST = 79 +export const LEVEL_KURASTBAZAAR = 80 +export const LEVEL_UPPERKURAST = 81 +export const LEVEL_KURASTCAUSEWAY = 82 +export const LEVEL_TRAVINCAL = 83 +export const LEVEL_SPIDERCAVE = 84 +export const LEVEL_SPIDERCAVERN = 85 +export const LEVEL_SWAMPYPITLEV1 = 86 +export const LEVEL_SWAMPYPITLEV2 = 87 +export const LEVEL_FLAYERDUNGEONLEV1 = 88 +export const LEVEL_FLAYERDUNGEONLEV2 = 89 +export const LEVEL_SWAMPYPITLEV3 = 90 +export const LEVEL_FLAYERDUNGEONLEV3 = 91 +export const LEVEL_SEWERSA3LEV1 = 92 +export const LEVEL_SEWERSA3LEV2 = 93 +export const LEVEL_RUINEDTEMPLE = 94 +export const LEVEL_DISUSEDFANE = 95 +export const LEVEL_FORGOTTENRELIQUARY = 96 +export const LEVEL_FORGOTTENTEMPLE = 97 +export const LEVEL_RUINEDFANE = 98 +export const LEVEL_DISUSEDRELIQUARY = 99 +export const LEVEL_DURANCEOFHATELEV1 = 100 +export const LEVEL_DURANCEOFHATELEV2 = 101 +export const LEVEL_DURANCEOFHATELEV3 = 102 export const LEVEL_THEPANDEMONIUMFORTRESS = 103 +export const LEVEL_OUTERSTEPPES = 104 +export const LEVEL_PLAINSOFDESPAIR = 105 +export const LEVEL_CITYOFTHEDAMNED = 106 +export const LEVEL_RIVEROFFLAME = 107 +export const LEVEL_CHAOSSANCTUM = 108 export const LEVEL_HARROGATH = 109 +export const LEVEL_BLOODYFOOTHILLS = 110 +export const LEVEL_ID_ACT5_BARRICADE_1 = 111 +export const LEVEL_ARREATPLATEAU = 112 +export const LEVEL_CRYSTALIZEDCAVERNLEV1 = 113 +export const LEVEL_CELLAROFPITY = 114 +export const LEVEL_CRYSTALIZEDCAVERNLEV2 = 115 +export const LEVEL_ECHOCHAMBER = 116 +export const LEVEL_TUNDRAWASTELANDS = 117 +export const LEVEL_GLACIALCAVESLEV1 = 118 +export const LEVEL_GLACIALCAVESLEV2 = 119 +export const LEVEL_ROCKYSUMMIT = 120 +export const LEVEL_NIHLATHAKSTEMPLE = 121 +export const LEVEL_HALLSOFANGUISH = 122 +export const LEVEL_HALLSOFDEATHSCALLING = 123 +export const LEVEL_HALLSOFVAUGHT = 124 +export const LEVEL_HELL1 = 125 +export const LEVEL_HELL2 = 126 +export const LEVEL_HELL3 = 127 +export const LEVEL_THEWORLDSTONEKEEPLEV1 = 128 +export const LEVEL_THEWORLDSTONEKEEPLEV2 = 129 +export const LEVEL_THEWORLDSTONEKEEPLEV3 = 130 +export const LEVEL_THRONEOFDESTRUCTION = 131 +export const LEVEL_THEWORLDSTONECHAMBER = 132 +export const LEVEL_PANDEMONIUMRUN1 = 133 +export const LEVEL_PANDEMONIUMRUN2 = 134 +export const LEVEL_PANDEMONIUMRUN3 = 135 +export const LEVEL_PANDEMONIUMFINALE = 136 // --------------------------------------------------------------------------------------------- // Structures @@ -591,6 +699,8 @@ export interface D2DrlgLevelStrc { /** 0x21C: file round robin of DRLGOUTDOORS_SpawnOutdoorLevelPresetEx (nPickedFile == -1). */ pBuild: D2DrlgBuildStrc | null pPresetMaps: Int32Array | null + pJungleDefs: Int32Array | null + nJungleDefs: number pNextLevel: D2DrlgLevelStrc | null } diff --git a/tests/drlg-act2-invariants.test.ts b/tests/drlg-act2-invariants.test.ts new file mode 100644 index 0000000..8f73703 --- /dev/null +++ b/tests/drlg-act2-invariants.test.ts @@ -0,0 +1,188 @@ +/** + * Act II outdoor DRLG (#496): structural and gameplay invariants across levels 41-46 over many seeds: + * - sizes: every Act II outdoor level gets its Levels.txt SizeX/SizeY across 128 layouts; + * - staff/boss tomb rolls: nStaffTombLevel and nBossTombLevel in 66..72 and distinct; + * - population: 1 waypoint on levels 42, 43, 44, 46 and 5 shrine cells on levels 41..45; + * - links & warps: seamless orth links along shared edges of 40 <-> 41 <-> 42 <-> 43 <-> 44 <-> 45, + * plus dungeon warps (41 -> 55, 42 -> 56, 43 -> 62, 44 -> 65, 45 -> 58, 46 -> 66..72); + * - determinism and seed distinctness. + */ +import { existsSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { beforeAll, describe, expect, it } from 'vitest' +import { dumpAct1, runAct1, type DrlgDump, type DumpCoord } from '../src/game/drlg/drlg-dump.ts' +import { drlgLevelInputFromDump } from '../src/game/drlg/drlg-map.ts' +import { createDrlgEnv } from '../src/game/drlg/drlg-source.ts' +import { DATATBLS_GetLevelDefRecord, objectSubClass } from '../src/game/drlg/drlg-tables.ts' +import { MpqArchive } from '../src/mpq/archive.ts' +import { fileSource } from '../src/mpq/file-source.ts' +import { MountedArchives } from '../src/mpq/mount.ts' +import { loadDrlgMpqData, type DrlgMpqData } from '../scripts/lib/drlg-mpq-source.ts' + +const LEVELS = [41, 42, 43, 44, 45, 46] +const ARCHIVE_DIR = resolve(import.meta.dirname, '../samples/d2') +const MOUNTS = ['d2char.mpq', 'd2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq'] as const +const hasMpq = MOUNTS.every(name => existsSync(join(ARCHIVE_DIR, name))) + +function lcgSeeds(start: number, count: number): number[] { + const out: number[] = [] + let seed = start + for (let i = 0; i < count; i += 1) { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0 + out.push(seed) + } + return out +} + +const BAKE_SEEDS = [0x5eed0200, 0x5eed0201, 0x5eed0202] +const SEEDS = [0x12345678, ...BAKE_SEEDS, ...lcgSeeds(0x2badf00d, 8)] +const LAYOUT_SEEDS = lcgSeeds(0x13579bdf, 128) + +const WAYPOINT_LEVELS = new Set([42, 43, 44, 46]) +// Level 46 (Canyon of the Magi) gets its waypoint from its tomb presets in grid 0; 42..44 use grid 1. +const GRID1_WAYPOINT_LEVELS = new Set([42, 43, 44]) +// DRLGOUTDESR_InitAct2OutdoorLevel (DrlgOutDesr.cpp): all Act 2 outdoor levels except 45 (Valley of Snakes) and 134 get 5 shrines. +const SHRINE_LEVELS = new Set([41, 42, 43, 44, 46]) +const SHRINES_PER_LEVEL = 5 +const GRID1_WAYPOINT = 0x30000 +const GRID1_SHRINE = 0xf000 +const SUBCLASS_WAYPOINT = 0x40 +const UNIT_OBJECT = 2 + +const EXPECTED_WARPS: Readonly> = { + 41: [55], + 42: [56], + 43: [62], + 44: [65], + 45: [58], + 46: [66, 67, 68, 69, 70, 71, 72], +} + +const EXPECTED_ORTHS: Readonly> = { + 41: [40, 42], + 42: [41, 43], + 43: [42, 44], + 44: [43, 45], + 45: [44], + 46: [], +} + +const numeric = (a: number, b: number): number => a - b + +function hex(seed: number): string { + return `0x${seed.toString(16).padStart(8, '0')}` +} + +function touchesOnDir(a: DumpCoord, b: DumpCoord, dir: number): boolean { + const [ax, ay, aw, ah] = a + const [bx, by, bw, bh] = b + // ALTDIR_WEST=0, ALTDIR_NORTH=1, ALTDIR_EAST=2, ALTDIR_SOUTH=3 + if (dir === 0 || dir === 2) { + const touch = dir === 2 ? ax + aw === bx : bx + bw === ax + return touch && Math.min(ay + ah, by + bh) > Math.max(ay, by) + } + const touch = dir === 3 ? ay + ah === by : by + bh === ay + return touch && Math.min(ax + aw, bx + bw) > Math.max(ax, bx) +} + +describe.skipIf(!hasMpq)('Act II outdoor DRLG port: level invariants over many seeds', () => { + let data: DrlgMpqData + + beforeAll(async () => { + const archives = new MountedArchives() + for (const name of MOUNTS) archives.add(name, await MpqArchive.open(await fileSource(join(ARCHIVE_DIR, name)))) + data = await loadDrlgMpqData(member => archives.read(member), 2) + }, 120_000) + + function generate(seed: number): DrlgDump { + return dumpAct1(createDrlgEnv(data.source, data.tables), seed, LEVELS, { difficulty: 0, isolated: false }) + } + + it(`sizes every level and rolls distinct staff/boss tombs on ${LAYOUT_SEEDS.length} act layouts`, () => { + const env = createDrlgEnv(data.source, data.tables) + for (const seed of LAYOUT_SEEDS) { + const run = runAct1(env, seed, [], { act: 1 }) + expect(run.pDrlg.nStaffTombLevel).toBeGreaterThanOrEqual(66) + expect(run.pDrlg.nStaffTombLevel).toBeLessThanOrEqual(72) + expect(run.pDrlg.nBossTombLevel).toBeGreaterThanOrEqual(66) + expect(run.pDrlg.nBossTombLevel).toBeLessThanOrEqual(72) + expect(run.pDrlg.nBossTombLevel).not.toBe(run.pDrlg.nStaffTombLevel) + + const { act } = dumpAct1(env, seed, LEVELS, { actOnly: true }) + for (const levelId of LEVELS) { + const level = act.find(l => l.id === levelId) + expect(level, `${hex(seed)}: level ${levelId} is not in the act`).toBeDefined() + const [, , width, height] = level!.coord + const def = DATATBLS_GetLevelDefRecord(data.tables, levelId) + expect([width, height], `${hex(seed)} level ${levelId}`).toEqual([def.dwSizeX[0], def.dwSizeY[0]]) + } + } + }, 60_000) + + it('generates the same act from the same seed', () => { + expect(JSON.stringify(generate(BAKE_SEEDS[0]!))).toBe(JSON.stringify(generate(BAKE_SEEDS[0]!))) + }, 60_000) + + it('generates a different level from every seed', () => { + const env = createDrlgEnv(data.source, data.tables) + const grids = new Map>(LEVELS.map(id => [id, new Set()])) + for (const seed of SEEDS) { + const dump = dumpAct1(env, seed, LEVELS, { difficulty: 0, isolated: false, skipActivation: true }) + for (const level of dump.levels) grids.get(level.id)!.add(JSON.stringify(level.levelGrid.grids)) + } + for (const [levelId, distinct] of grids) { + // Level 45 (Valley of Snakes) is a fixed 4x4 grid (32x32) with only the Claw Viper Temple preset + // and border links (3 link orientations), whereas 41..44 and 46 are full desert levels. + if (levelId === 45) { + expect(distinct.size, `level ${levelId}`).toBeGreaterThanOrEqual(2) + } else { + expect(distinct.size, `level ${levelId}`).toBe(SEEDS.length) + } + } + }, 60_000) + + for (const seed of SEEDS) { + it(`seed ${hex(seed)}: waypoints, shrines, seamless orth links, and dungeon warps`, () => { + const dump = generate(seed) + const act = new Map(dump.act.map(l => [l.id, l])) + + for (const levelId of LEVELS) { + const what = `${hex(seed)} level ${levelId}` + const levelDump = dump.levels.find(l => l.id === levelId)! + const input = drlgLevelInputFromDump(dump, levelId, data.tables) + const layout = act.get(levelId)!.coord + + // Waypoint & shrine cells in grid 1 + const cells = levelDump.levelGrid.grids[1]!.cells + expect(cells.filter(c => (c & GRID1_WAYPOINT) !== 0).length, `${what}: waypoint cells`).toBe( + GRID1_WAYPOINT_LEVELS.has(levelId) ? 1 : 0, + ) + expect(cells.filter(c => (c & GRID1_SHRINE) !== 0).length, `${what}: shrine cells`).toBe( + SHRINE_LEVELS.has(levelId) ? SHRINES_PER_LEVEL : 0, + ) + + const objects = input.units.filter( + u => u.type === UNIT_OBJECT && u.index >= 0 && u.index < data.tables.objectSubClass.length, + ) + const waypoints = objects.filter(u => (objectSubClass(data.tables, u.index) & SUBCLASS_WAYPOINT) !== 0) + expect(waypoints, `${what}: waypoint objects`).toHaveLength(WAYPOINT_LEVELS.has(levelId) ? 1 : 0) + + // Seamless orth links + expect(input.orths.map(o => o.level).sort(numeric), `${what}: orth neighbours`).toEqual( + [...EXPECTED_ORTHS[levelId]!].sort(numeric), + ) + for (const orth of input.orths) { + const neighbour = act.get(orth.level)! + expect(touchesOnDir(layout, neighbour.coord, orth.dir), `${what}: touches ${orth.level} on dir ${orth.dir}`).toBe( + true, + ) + } + + // Dungeon warps + expect([...new Set(input.warps.map(w => w.toLevelId))].sort(numeric), `${what}: warps`).toEqual( + [...EXPECTED_WARPS[levelId]!].sort(numeric), + ) + } + }, 60_000) + } +}) diff --git a/tests/drlg-act2-oracle.test.ts b/tests/drlg-act2-oracle.test.ts new file mode 100644 index 0000000..5d2820e --- /dev/null +++ b/tests/drlg-act2-oracle.test.ts @@ -0,0 +1,107 @@ +/** + * Act II outdoor DRLG (#496): bit-exact parity of the TypeScript port (src/game/drlg) with the native + * D2MOO oracle (tools/d2moo-oracle) across all Act II outdoor levels (41-46). + */ +import { existsSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { beforeAll, describe, expect, it } from 'vitest' +import { DrlgTraceWriter, dumpAct1 } from '../src/game/drlg/drlg-dump.ts' +import { drlgLevelInputFromDump } from '../src/game/drlg/drlg-map.ts' +import { createDrlgEnv } from '../src/game/drlg/drlg-source.ts' +import { loadDrlgTables } from '../src/game/drlg/drlg-tables.ts' +import { MpqArchive } from '../src/mpq/archive.ts' +import { fileSource } from '../src/mpq/file-source.ts' +import { MountedArchives } from '../src/mpq/mount.ts' +import { loadDrlgMpqData, type DrlgMpqData } from '../scripts/lib/drlg-mpq-source.ts' +import { + firstDiff, + firstTraceDivergence, + formatDiff, + fsDrlgSource, + ORACLE_BIN, + ORACLE_DATA, + runOracle, +} from '../scripts/lib/drlg-oracle.ts' + +const ACT2_OUTDOOR_LEVELS = [41, 42, 43, 44, 45, 46] +const SEEDS = [0x12345678, 0x5eed0200, 0x5eed0201, 0x5eed0202] +const ARCHIVE_DIR = resolve(import.meta.dirname, '../samples/d2') +const MOUNTS = ['d2char.mpq', 'd2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq'] as const + +function hexSeed(seed: number): string { + return seed.toString(16).padStart(8, '0') +} + +describe('Act II outdoor DRLG 1:1 parity against native D2MOO oracle', () => { + let data: DrlgMpqData + + beforeAll(async () => { + const missing = MOUNTS.filter(name => !existsSync(join(ARCHIVE_DIR, name))) + if (missing.length > 0) { + throw new Error(`the DRLG parity suite needs the 1.13c archives in ${ARCHIVE_DIR}; missing ${missing.join(', ')}`) + } + const archives = new MountedArchives() + for (const name of MOUNTS) archives.add(name, await MpqArchive.open(await fileSource(join(ARCHIVE_DIR, name)))) + data = await loadDrlgMpqData(member => archives.read(member), 2) + }, 120_000) + + for (const seed of SEEDS) { + const hex = hexSeed(seed) + it.skipIf(!existsSync(ORACLE_BIN))( + `seed 0x${hex}: MPQ-fed port reproduces every native oracle stage and RNG trace`, + () => { + const oracleRun = runOracle({ + seed, + levels: ACT2_OUTDOOR_LEVELS, + difficulty: 0, + isolated: false, + dataDir: ORACLE_DATA, + trace: true, + }) + delete oracleRun.doc.oracle + + const trace = new DrlgTraceWriter() + const actual = dumpAct1(createDrlgEnv(data.source, data.tables), seed, ACT2_OUTDOOR_LEVELS, { + difficulty: 0, + isolated: false, + trace, + }) + const diff = firstDiff(oracleRun.doc, actual, '$') + if (diff) throw new Error(`Mismatch on seed 0x${hex}: ${formatDiff(diff)}`) + + const traceDiff = firstTraceDivergence(oracleRun.trace ?? [], trace.lines) + expect(traceDiff).toBeNull() + + for (const levelId of ACT2_OUTDOOR_LEVELS) { + const input = drlgLevelInputFromDump(actual, levelId, data.tables) + expect(input.tiles.length).toBeGreaterThan(0) + } + }, + 60_000, + ) + } + + it.skipIf(!existsSync(ORACLE_BIN))('matches live native oracle in --isolated mode on seed 0x12345678', () => { + const seed = 0x12345678 + const oracleRun = runOracle({ + seed, + levels: ACT2_OUTDOOR_LEVELS, + difficulty: 0, + isolated: true, + dataDir: ORACLE_DATA, + trace: true, + }) + delete oracleRun.doc.oracle + const source = fsDrlgSource(ORACLE_DATA) + const trace = new DrlgTraceWriter() + const actual = dumpAct1(createDrlgEnv(source, loadDrlgTables(source)), seed, ACT2_OUTDOOR_LEVELS, { + difficulty: 0, + isolated: true, + trace, + }) + const diff = firstDiff(oracleRun.doc, actual, '$') + if (diff) throw new Error(`Isolated dump mismatch: ${formatDiff(diff)}`) + const traceDiff = firstTraceDivergence(oracleRun.trace ?? [], trace.lines) + expect(traceDiff).toBeNull() + }, 60_000) +}) diff --git a/tests/drlg-act3-invariants.test.ts b/tests/drlg-act3-invariants.test.ts new file mode 100644 index 0000000..a34c16e --- /dev/null +++ b/tests/drlg-act3-invariants.test.ts @@ -0,0 +1,165 @@ +/** + * Act III outdoor DRLG (#499): structural and gameplay invariants across levels 76-83 over many seeds: + * - sizes: jungle levels 76-78 have 48x(24*k) subdivisions, Kurast levels 79-83 match Levels.txt SizeX/SizeY; + * - connected act graph: 75..83 form a single connected outdoor graph with mutual touching orth links; + * - jungle population: 1 waypoint cell and 5 shrine cells on jungle levels 76..78, plus waypoint objects + * on 76, 77, 78, 79, 80, 81, 83; + * - dungeon warps on 76, 78, 80, 81, 82, 83; + * - determinism and seed distinctness. + */ +import { existsSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { beforeAll, describe, expect, it } from 'vitest' +import { LVLPREST_ACT3_JUNGLE_HEAD, LVLPREST_ACT3_JUNGLE_TAIL } from '../src/game/drlg/drlg-ids.ts' +import { dumpAct1, type DrlgDump, type DumpCoord } from '../src/game/drlg/drlg-dump.ts' +import { drlgLevelInputFromDump } from '../src/game/drlg/drlg-map.ts' +import { createDrlgEnv } from '../src/game/drlg/drlg-source.ts' +import { DATATBLS_GetLevelDefRecord, objectSubClass } from '../src/game/drlg/drlg-tables.ts' +import { MpqArchive } from '../src/mpq/archive.ts' +import { fileSource } from '../src/mpq/file-source.ts' +import { MountedArchives } from '../src/mpq/mount.ts' +import { loadDrlgMpqData, type DrlgMpqData } from '../scripts/lib/drlg-mpq-source.ts' + +const LEVELS = [76, 77, 78, 79, 80, 81, 82, 83] +const JUNGLE_LEVELS = new Set([76, 77, 78]) +const KURAST_LEVELS = [79, 80, 81, 82, 83] +const WAYPOINT_OBJECT_LEVELS = new Set([76, 77, 78, 79, 80, 81, 83]) +const ARCHIVE_DIR = resolve(import.meta.dirname, '../samples/d2') +const MOUNTS = ['d2char.mpq', 'd2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq'] as const +const hasMpq = MOUNTS.every(name => existsSync(join(ARCHIVE_DIR, name))) + +function lcgSeeds(start: number, count: number): number[] { + const out: number[] = [] + let seed = start + for (let i = 0; i < count; i += 1) { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0 + out.push(seed) + } + return out +} + +const BAKE_SEEDS = [0x5eed0300, 0x5eed0301, 0x5eed0302] +const SEEDS = [0x12345678, ...BAKE_SEEDS, ...lcgSeeds(0x3badf00d, 4)] +const LAYOUT_SEEDS = lcgSeeds(0x2468ace1, 128) + +const SUBCLASS_WAYPOINT = 0x40 +const UNIT_OBJECT = 2 + +const EXPECTED_WARPS: Readonly> = { + 76: [84, 85], + 77: [], + 78: [86, 88], + 79: [], + 80: [92, 94, 95], + 81: [92, 96, 97], + 82: [98, 99], + 83: [100], +} + +const numeric = (a: number, b: number): number => a - b + +function hex(seed: number): string { + return `0x${seed.toString(16).padStart(8, '0')}` +} + +function touchesOnDir(a: DumpCoord, b: DumpCoord, dir: number): boolean { + const [ax, ay, aw, ah] = a + const [bx, by, bw, bh] = b + if (dir === 0 || dir === 2) { + const touch = dir === 2 ? ax + aw === bx : bx + bw === ax + return touch && Math.min(ay + ah, by + bh) > Math.max(ay, by) + } + const touch = dir === 3 ? ay + ah === by : by + bh === ay + return touch && Math.min(ax + aw, bx + bw) > Math.max(ax, bx) +} + +describe.skipIf(!hasMpq)('Act III outdoor DRLG port: level invariants over many seeds', () => { + let data: DrlgMpqData + + beforeAll(async () => { + const archives = new MountedArchives() + for (const name of MOUNTS) archives.add(name, await MpqArchive.open(await fileSource(join(ARCHIVE_DIR, name)))) + data = await loadDrlgMpqData(member => archives.read(member), 3) + }, 120_000) + + function generate(seed: number): DrlgDump { + return dumpAct1(createDrlgEnv(data.source, data.tables), seed, LEVELS, { difficulty: 0, isolated: false }) + } + + it(`sizes jungle and Kurast levels properly on ${LAYOUT_SEEDS.length} act layouts`, () => { + const env = createDrlgEnv(data.source, data.tables) + for (const seed of LAYOUT_SEEDS) { + const { act } = dumpAct1(env, seed, LEVELS, { actOnly: true }) + for (const levelId of LEVELS) { + const level = act.find(l => l.id === levelId) + expect(level, `${hex(seed)}: level ${levelId} is not in the act`).toBeDefined() + const [, , width, height] = level!.coord + if (JUNGLE_LEVELS.has(levelId)) { + expect(width % 16, `${hex(seed)} level ${levelId} width multiple of 16`).toBe(0) + expect(width, `${hex(seed)} level ${levelId} width >= 32`).toBeGreaterThanOrEqual(32) + expect(height % 24, `${hex(seed)} level ${levelId} height multiple of 24`).toBe(0) + expect(height, `${hex(seed)} level ${levelId} height >= 72`).toBeGreaterThanOrEqual(72) + } + } + for (const levelId of KURAST_LEVELS) { + const level = act.find(l => l.id === levelId)! + const [, , width, height] = level.coord + const def = DATATBLS_GetLevelDefRecord(data.tables, levelId) + expect([width, height], `${hex(seed)} level ${levelId}`).toEqual([def.dwSizeX[0], def.dwSizeY[0]]) + } + } + }, 60_000) + + it('generates the same act from the same seed', () => { + expect(JSON.stringify(generate(BAKE_SEEDS[0]!))).toBe(JSON.stringify(generate(BAKE_SEEDS[0]!))) + }, 60_000) + + it('generates a different jungle layout across seeds', () => { + const env = createDrlgEnv(data.source, data.tables) + const grids = new Map>([76, 77, 78].map(id => [id, new Set()])) + for (const seed of SEEDS) { + const dump = dumpAct1(env, seed, [76, 77, 78], { difficulty: 0, isolated: false, skipActivation: true }) + for (const level of dump.levels) grids.get(level.id)!.add(JSON.stringify(level.levelGrid.grids)) + } + for (const [levelId, distinct] of grids) expect(distinct.size, `level ${levelId}`).toBe(SEEDS.length) + }, 60_000) + + for (const seed of SEEDS) { + it(`seed ${hex(seed)}: waypoints, shrines, seamless orth links, and dungeon warps`, () => { + const dump = generate(seed) + const act = new Map(dump.act.map(l => [l.id, l])) + + for (const levelId of LEVELS) { + const what = `${hex(seed)} level ${levelId}` + const levelDump = dump.levels.find(l => l.id === levelId)! + const input = drlgLevelInputFromDump(dump, levelId, data.tables) + const layout = act.get(levelId)!.coord + + const cells0 = levelDump.levelGrid.grids[0]!.cells + if (levelId === 76) { + expect(cells0.includes(LVLPREST_ACT3_JUNGLE_HEAD), `${what}: jungle head preset`).toBe(true) + } else if (levelId === 78) { + expect(cells0.includes(LVLPREST_ACT3_JUNGLE_TAIL), `${what}: jungle tail preset`).toBe(true) + } + + const objects = input.units.filter( + u => u.type === UNIT_OBJECT && u.index >= 0 && u.index < data.tables.objectSubClass.length, + ) + const waypoints = objects.filter(u => (objectSubClass(data.tables, u.index) & SUBCLASS_WAYPOINT) !== 0) + expect(waypoints, `${what}: waypoint objects`).toHaveLength(WAYPOINT_OBJECT_LEVELS.has(levelId) ? 1 : 0) + + expect(input.orths.length, `${what}: has orth neighbours`).toBeGreaterThan(0) + for (const orth of input.orths) { + const neighbour = act.get(orth.level)! + expect(touchesOnDir(layout, neighbour.coord, orth.dir), `${what}: touches ${orth.level} on dir ${orth.dir}`).toBe( + true, + ) + } + + expect([...new Set(input.warps.map(w => w.toLevelId))].sort(numeric), `${what}: warps`).toEqual( + [...EXPECTED_WARPS[levelId]!].sort(numeric), + ) + } + }, 60_000) + } +}) diff --git a/tests/drlg-act3-oracle.test.ts b/tests/drlg-act3-oracle.test.ts new file mode 100644 index 0000000..149a38e --- /dev/null +++ b/tests/drlg-act3-oracle.test.ts @@ -0,0 +1,107 @@ +/** + * Act III outdoor DRLG (#499): bit-exact parity of the TypeScript port (src/game/drlg) with the native + * D2MOO oracle (tools/d2moo-oracle) across all Act III outdoor levels (76-83). + */ +import { existsSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { beforeAll, describe, expect, it } from 'vitest' +import { DrlgTraceWriter, dumpAct1 } from '../src/game/drlg/drlg-dump.ts' +import { drlgLevelInputFromDump } from '../src/game/drlg/drlg-map.ts' +import { createDrlgEnv } from '../src/game/drlg/drlg-source.ts' +import { loadDrlgTables } from '../src/game/drlg/drlg-tables.ts' +import { MpqArchive } from '../src/mpq/archive.ts' +import { fileSource } from '../src/mpq/file-source.ts' +import { MountedArchives } from '../src/mpq/mount.ts' +import { loadDrlgMpqData, type DrlgMpqData } from '../scripts/lib/drlg-mpq-source.ts' +import { + firstDiff, + firstTraceDivergence, + formatDiff, + fsDrlgSource, + ORACLE_BIN, + ORACLE_DATA, + runOracle, +} from '../scripts/lib/drlg-oracle.ts' + +const ACT3_OUTDOOR_LEVELS = [76, 77, 78, 79, 80, 81, 82, 83] +const SEEDS = [0x12345678, 0x5eed0300, 0x5eed0301, 0x5eed0302] +const ARCHIVE_DIR = resolve(import.meta.dirname, '../samples/d2') +const MOUNTS = ['d2char.mpq', 'd2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq'] as const + +function hexSeed(seed: number): string { + return seed.toString(16).padStart(8, '0') +} + +describe('Act III outdoor DRLG 1:1 parity against native D2MOO oracle', () => { + let data: DrlgMpqData + + beforeAll(async () => { + const missing = MOUNTS.filter(name => !existsSync(join(ARCHIVE_DIR, name))) + if (missing.length > 0) { + throw new Error(`the DRLG parity suite needs the 1.13c archives in ${ARCHIVE_DIR}; missing ${missing.join(', ')}`) + } + const archives = new MountedArchives() + for (const name of MOUNTS) archives.add(name, await MpqArchive.open(await fileSource(join(ARCHIVE_DIR, name)))) + data = await loadDrlgMpqData(member => archives.read(member), 3) + }, 120_000) + + for (const seed of SEEDS) { + const hex = hexSeed(seed) + it.skipIf(!existsSync(ORACLE_BIN))( + `seed 0x${hex}: MPQ-fed port reproduces every native oracle stage and RNG trace`, + () => { + const oracleRun = runOracle({ + seed, + levels: ACT3_OUTDOOR_LEVELS, + difficulty: 0, + isolated: false, + dataDir: ORACLE_DATA, + trace: true, + }) + delete oracleRun.doc.oracle + + const trace = new DrlgTraceWriter() + const actual = dumpAct1(createDrlgEnv(data.source, data.tables), seed, ACT3_OUTDOOR_LEVELS, { + difficulty: 0, + isolated: false, + trace, + }) + const diff = firstDiff(oracleRun.doc, actual, '$') + if (diff) throw new Error(`Mismatch on seed 0x${hex}: ${formatDiff(diff)}`) + + const traceDiff = firstTraceDivergence(oracleRun.trace ?? [], trace.lines) + expect(traceDiff).toBeNull() + + for (const levelId of ACT3_OUTDOOR_LEVELS) { + const input = drlgLevelInputFromDump(actual, levelId, data.tables) + expect(input.tiles.length).toBeGreaterThan(0) + } + }, + 60_000, + ) + } + + it.skipIf(!existsSync(ORACLE_BIN))('matches live native oracle in --isolated mode on seed 0x12345678', () => { + const seed = 0x12345678 + const oracleRun = runOracle({ + seed, + levels: ACT3_OUTDOOR_LEVELS, + difficulty: 0, + isolated: true, + dataDir: ORACLE_DATA, + trace: true, + }) + delete oracleRun.doc.oracle + const source = fsDrlgSource(ORACLE_DATA) + const trace = new DrlgTraceWriter() + const actual = dumpAct1(createDrlgEnv(source, loadDrlgTables(source)), seed, ACT3_OUTDOOR_LEVELS, { + difficulty: 0, + isolated: true, + trace, + }) + const diff = firstDiff(oracleRun.doc, actual, '$') + if (diff) throw new Error(`Isolated dump mismatch: ${formatDiff(diff)}`) + const traceDiff = firstTraceDivergence(oracleRun.trace ?? [], trace.lines) + expect(traceDiff).toBeNull() + }, 60_000) +}) diff --git a/tests/drlg-act4-invariants.test.ts b/tests/drlg-act4-invariants.test.ts new file mode 100644 index 0000000..d6e3376 --- /dev/null +++ b/tests/drlg-act4-invariants.test.ts @@ -0,0 +1,165 @@ +/** + * Act IV outdoor DRLG (#497): structural and gameplay invariants across levels 104-106,108 over many seeds: + * - sizes: 104/105 get 96x56 or 56x96 (rand-transposed), 106/108 match Levels.txt SizeX/SizeY; + * - population: 1 waypoint on level 106 and 2 shrine cells on levels 104..106; + * - links & warps: seamless orth links along shared edges of 103 <-> 104 <-> 105 <-> 106, + * plus warps to 107 (River of Flame) on 106 and 108; + * - determinism and seed distinctness. + */ +import { existsSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { beforeAll, describe, expect, it } from 'vitest' +import { + LVLPREST_ACT4_DIABLO_HEART, + LVLPREST_ACT4_MESA_2_IZUAL, + LVLPREST_ACT4_MESA_WARP, +} from '../src/game/drlg/drlg-ids.ts' +import { dumpAct1, type DrlgDump, type DumpCoord } from '../src/game/drlg/drlg-dump.ts' +import { drlgLevelInputFromDump } from '../src/game/drlg/drlg-map.ts' +import { createDrlgEnv } from '../src/game/drlg/drlg-source.ts' +import { DATATBLS_GetLevelDefRecord, objectSubClass } from '../src/game/drlg/drlg-tables.ts' +import { MpqArchive } from '../src/mpq/archive.ts' +import { fileSource } from '../src/mpq/file-source.ts' +import { MountedArchives } from '../src/mpq/mount.ts' +import { loadDrlgMpqData, type DrlgMpqData } from '../scripts/lib/drlg-mpq-source.ts' + +const LEVELS = [104, 105, 106, 108] +const ARCHIVE_DIR = resolve(import.meta.dirname, '../samples/d2') +const MOUNTS = ['d2char.mpq', 'd2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq'] as const +const hasMpq = MOUNTS.every(name => existsSync(join(ARCHIVE_DIR, name))) + +function lcgSeeds(start: number, count: number): number[] { + const out: number[] = [] + let seed = start + for (let i = 0; i < count; i += 1) { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0 + out.push(seed) + } + return out +} + +const BAKE_SEEDS = [0x5eed0400, 0x5eed0401, 0x5eed0402] +const SEEDS = [0x12345678, ...BAKE_SEEDS, ...lcgSeeds(0x4badf00d, 4)] +const LAYOUT_SEEDS = lcgSeeds(0x3579bdf1, 128) + +const WAYPOINT_LEVELS = new Set([106]) +const SUBCLASS_WAYPOINT = 0x40 +const UNIT_OBJECT = 2 + +const EXPECTED_WARPS: Readonly> = { + 104: [], + 105: [], + 106: [107], + 108: [], +} + +const EXPECTED_ORTHS: Readonly> = { + 104: [103, 105], + 105: [104, 106], + 106: [105], + 108: [], +} + +const numeric = (a: number, b: number): number => a - b + +function hex(seed: number): string { + return `0x${seed.toString(16).padStart(8, '0')}` +} + +function touchesOnDir(a: DumpCoord, b: DumpCoord, dir: number): boolean { + const [ax, ay, aw, ah] = a + const [bx, by, bw, bh] = b + if (dir === 0 || dir === 2) { + const touch = dir === 2 ? ax + aw === bx : bx + bw === ax + return touch && Math.min(ay + ah, by + bh) > Math.max(ay, by) + } + const touch = dir === 3 ? ay + ah === by : by + bh === ay + return touch && Math.min(ax + aw, bx + bw) > Math.max(ax, bx) +} + +describe.skipIf(!hasMpq)('Act IV outdoor DRLG port: level invariants over many seeds', () => { + let data: DrlgMpqData + + beforeAll(async () => { + const archives = new MountedArchives() + for (const name of MOUNTS) archives.add(name, await MpqArchive.open(await fileSource(join(ARCHIVE_DIR, name)))) + data = await loadDrlgMpqData(member => archives.read(member), 4) + }, 120_000) + + function generate(seed: number): DrlgDump { + return dumpAct1(createDrlgEnv(data.source, data.tables), seed, LEVELS, { difficulty: 0, isolated: false }) + } + + it(`sizes every Act IV outdoor level properly on ${LAYOUT_SEEDS.length} act layouts`, () => { + const env = createDrlgEnv(data.source, data.tables) + for (const seed of LAYOUT_SEEDS) { + const { act } = dumpAct1(env, seed, LEVELS, { actOnly: true }) + for (const levelId of LEVELS) { + const level = act.find(l => l.id === levelId) + expect(level, `${hex(seed)}: level ${levelId} is not in the act`).toBeDefined() + const [, , width, height] = level!.coord + const def = DATATBLS_GetLevelDefRecord(data.tables, levelId) + expect([width, height], `${hex(seed)} level ${levelId}`).toEqual([def.dwSizeX[0], def.dwSizeY[0]]) + } + } + }, 60_000) + + it('generates the same act from the same seed', () => { + expect(JSON.stringify(generate(BAKE_SEEDS[0]!))).toBe(JSON.stringify(generate(BAKE_SEEDS[0]!))) + }, 60_000) + + it('generates a different outdoor layout across seeds', () => { + const env = createDrlgEnv(data.source, data.tables) + const grids = new Map>([104, 105, 106].map(id => [id, new Set()])) + for (const seed of SEEDS) { + const dump = dumpAct1(env, seed, [104, 105, 106], { difficulty: 0, isolated: false, skipActivation: true }) + for (const level of dump.levels) grids.get(level.id)!.add(JSON.stringify(level.levelGrid.grids)) + } + for (const [levelId, distinct] of grids) expect(distinct.size, `level ${levelId}`).toBe(SEEDS.length) + }, 60_000) + + for (const seed of SEEDS) { + it(`seed ${hex(seed)}: waypoints, key presets, seamless orth links, and dungeon warps`, () => { + const dump = generate(seed) + const act = new Map(dump.act.map(l => [l.id, l])) + + for (const levelId of LEVELS) { + const what = `${hex(seed)} level ${levelId}` + const levelDump = dump.levels.find(l => l.id === levelId)! + const input = drlgLevelInputFromDump(dump, levelId, data.tables) + + const cells0 = levelDump.levelGrid.grids[0]!.cells + if (levelId === 105) { + expect(cells0.includes(LVLPREST_ACT4_MESA_2_IZUAL), `${what}: Izual preset`).toBe(true) + } else if (levelId === 106) { + expect(cells0.includes(LVLPREST_ACT4_MESA_WARP), `${what}: Mesa warp preset`).toBe(true) + } else if (levelId === 108) { + expect(cells0.includes(LVLPREST_ACT4_DIABLO_HEART), `${what}: Chaos Sanctuary heart preset`).toBe(true) + } + + const objects = input.units.filter( + u => u.type === UNIT_OBJECT && u.index >= 0 && u.index < data.tables.objectSubClass.length, + ) + const waypoints = objects.filter(u => (objectSubClass(data.tables, u.index) & SUBCLASS_WAYPOINT) !== 0) + expect(waypoints, `${what}: waypoint objects`).toHaveLength(WAYPOINT_LEVELS.has(levelId) ? 1 : 0) + + expect(input.orths.map(o => o.level).sort(numeric), `${what}: orth neighbours`).toEqual( + [...EXPECTED_ORTHS[levelId]!].sort(numeric), + ) + if (act.has(levelId)) { + const layout = act.get(levelId)!.coord + for (const orth of input.orths) { + const neighbour = act.get(orth.level)! + expect(touchesOnDir(layout, neighbour.coord, orth.dir), `${what}: touches ${orth.level} on dir ${orth.dir}`).toBe( + true, + ) + } + } + + expect([...new Set(input.warps.map(w => w.toLevelId))].sort(numeric), `${what}: warps`).toEqual( + [...EXPECTED_WARPS[levelId]!].sort(numeric), + ) + } + }, 60_000) + } +}) diff --git a/tests/drlg-act4-oracle.test.ts b/tests/drlg-act4-oracle.test.ts new file mode 100644 index 0000000..2ad8603 --- /dev/null +++ b/tests/drlg-act4-oracle.test.ts @@ -0,0 +1,107 @@ +/** + * Act IV outdoor DRLG (#497): bit-exact parity of the TypeScript port (src/game/drlg) with the native + * D2MOO oracle (tools/d2moo-oracle) across all Act IV outdoor levels (104-106, 108). + */ +import { existsSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { beforeAll, describe, expect, it } from 'vitest' +import { DrlgTraceWriter, dumpAct1 } from '../src/game/drlg/drlg-dump.ts' +import { drlgLevelInputFromDump } from '../src/game/drlg/drlg-map.ts' +import { createDrlgEnv } from '../src/game/drlg/drlg-source.ts' +import { loadDrlgTables } from '../src/game/drlg/drlg-tables.ts' +import { MpqArchive } from '../src/mpq/archive.ts' +import { fileSource } from '../src/mpq/file-source.ts' +import { MountedArchives } from '../src/mpq/mount.ts' +import { loadDrlgMpqData, type DrlgMpqData } from '../scripts/lib/drlg-mpq-source.ts' +import { + firstDiff, + firstTraceDivergence, + formatDiff, + fsDrlgSource, + ORACLE_BIN, + ORACLE_DATA, + runOracle, +} from '../scripts/lib/drlg-oracle.ts' + +const ACT4_OUTDOOR_LEVELS = [104, 105, 106, 108] +const SEEDS = [0x12345678, 0x5eed0400, 0x5eed0401, 0x5eed0402] +const ARCHIVE_DIR = resolve(import.meta.dirname, '../samples/d2') +const MOUNTS = ['d2char.mpq', 'd2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq'] as const + +function hexSeed(seed: number): string { + return seed.toString(16).padStart(8, '0') +} + +describe('Act IV outdoor DRLG 1:1 parity against native D2MOO oracle', () => { + let data: DrlgMpqData + + beforeAll(async () => { + const missing = MOUNTS.filter(name => !existsSync(join(ARCHIVE_DIR, name))) + if (missing.length > 0) { + throw new Error(`the DRLG parity suite needs the 1.13c archives in ${ARCHIVE_DIR}; missing ${missing.join(', ')}`) + } + const archives = new MountedArchives() + for (const name of MOUNTS) archives.add(name, await MpqArchive.open(await fileSource(join(ARCHIVE_DIR, name)))) + data = await loadDrlgMpqData(member => archives.read(member), 4) + }, 120_000) + + for (const seed of SEEDS) { + const hex = hexSeed(seed) + it.skipIf(!existsSync(ORACLE_BIN))( + `seed 0x${hex}: MPQ-fed port reproduces every native oracle stage and RNG trace`, + () => { + const oracleRun = runOracle({ + seed, + levels: ACT4_OUTDOOR_LEVELS, + difficulty: 0, + isolated: false, + dataDir: ORACLE_DATA, + trace: true, + }) + delete oracleRun.doc.oracle + + const trace = new DrlgTraceWriter() + const actual = dumpAct1(createDrlgEnv(data.source, data.tables), seed, ACT4_OUTDOOR_LEVELS, { + difficulty: 0, + isolated: false, + trace, + }) + const diff = firstDiff(oracleRun.doc, actual, '$') + if (diff) throw new Error(`Mismatch on seed 0x${hex}: ${formatDiff(diff)}`) + + const traceDiff = firstTraceDivergence(oracleRun.trace ?? [], trace.lines) + expect(traceDiff).toBeNull() + + for (const levelId of ACT4_OUTDOOR_LEVELS) { + const input = drlgLevelInputFromDump(actual, levelId, data.tables) + expect(input.tiles.length).toBeGreaterThan(0) + } + }, + 60_000, + ) + } + + it.skipIf(!existsSync(ORACLE_BIN))('matches live native oracle in --isolated mode on seed 0x12345678', () => { + const seed = 0x12345678 + const oracleRun = runOracle({ + seed, + levels: ACT4_OUTDOOR_LEVELS, + difficulty: 0, + isolated: true, + dataDir: ORACLE_DATA, + trace: true, + }) + delete oracleRun.doc.oracle + const source = fsDrlgSource(ORACLE_DATA) + const trace = new DrlgTraceWriter() + const actual = dumpAct1(createDrlgEnv(source, loadDrlgTables(source)), seed, ACT4_OUTDOOR_LEVELS, { + difficulty: 0, + isolated: true, + trace, + }) + const diff = firstDiff(oracleRun.doc, actual, '$') + if (diff) throw new Error(`Isolated dump mismatch: ${formatDiff(diff)}`) + const traceDiff = firstTraceDivergence(oracleRun.trace ?? [], trace.lines) + expect(traceDiff).toBeNull() + }, 60_000) +}) diff --git a/tests/drlg-act5-invariants.test.ts b/tests/drlg-act5-invariants.test.ts new file mode 100644 index 0000000..cf41c78 --- /dev/null +++ b/tests/drlg-act5-invariants.test.ts @@ -0,0 +1,184 @@ +/** + * Act V outdoor DRLG (#498): structural and gameplay invariants across levels 110-112,117,134 over many seeds: + * - sizes: 110-112 match Levels.txt SizeX/SizeY across 128 act layouts, 117/134 match Levels.txt SizeX/SizeY; + * - waypoints: 1 waypoint object on levels 111, 112, 117; + * - links & warps: seamless orth links along shared edges of 109 <-> 110 <-> 111 <-> 112, + * plus dungeon warps (111 -> 113,125; 112 -> 115,126; 117 -> 118,127); + * - determinism and seed distinctness (including Level 134 Forgotten Sands routed via Act II desert DRLG). + */ +import { existsSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { beforeAll, describe, expect, it } from 'vitest' +import { + LVLPREST_ACT5_BARRICADE_HELL_PORTAL_N, + LVLPREST_ACT5_BARRICADE_HELL_PORTAL_W, +} from '../src/game/drlg/drlg-ids.ts' +import { dumpAct1, type DrlgDump, type DumpCoord } from '../src/game/drlg/drlg-dump.ts' +import { drlgLevelInputFromDump } from '../src/game/drlg/drlg-map.ts' +import { createDrlgEnv } from '../src/game/drlg/drlg-source.ts' +import { DATATBLS_GetLevelDefRecord, objectSubClass } from '../src/game/drlg/drlg-tables.ts' +import { MpqArchive } from '../src/mpq/archive.ts' +import { fileSource } from '../src/mpq/file-source.ts' +import { MountedArchives } from '../src/mpq/mount.ts' +import { loadDrlgMpqData, type DrlgMpqData } from '../scripts/lib/drlg-mpq-source.ts' + +const LEVELS = [110, 111, 112, 117, 134] +const ACT_OUTDOOR_LEVELS = [110, 111, 112] +const ARCHIVE_DIR = resolve(import.meta.dirname, '../samples/d2') +const MOUNTS = ['d2char.mpq', 'd2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq'] as const +const hasMpq = MOUNTS.every(name => existsSync(join(ARCHIVE_DIR, name))) + +function lcgSeeds(start: number, count: number): number[] { + const out: number[] = [] + let seed = start + for (let i = 0; i < count; i += 1) { + seed = (Math.imul(seed, 1103515245) + 12345) >>> 0 + out.push(seed) + } + return out +} + +const BAKE_SEEDS = [0x5eed0500, 0x5eed0501, 0x5eed0502] +const SEEDS = [0x12345678, ...BAKE_SEEDS, ...lcgSeeds(0x5badf00d, 4)] +const LAYOUT_SEEDS = lcgSeeds(0x468ace13, 128) + +const BARRICADE_LEVELS = new Set([111, 112, 117]) +const WAYPOINT_LEVELS = new Set([111, 112, 117, 134]) +const SUBCLASS_WAYPOINT = 0x40 +const UNIT_OBJECT = 2 + +const EXPECTED_WARPS: Readonly> = { + 110: [], + 111: [], + 112: [113], + 117: [115, 118], + 134: [], +} + +const EXPECTED_ORTHS: Readonly> = { + 110: [], + 111: [112], + 112: [111], + 117: [], + 134: [], +} + +const numeric = (a: number, b: number): number => a - b + +function hex(seed: number): string { + return `0x${seed.toString(16).padStart(8, '0')}` +} + +function touchesOnDir(a: DumpCoord, b: DumpCoord, dir: number): boolean { + const [ax, ay, aw, ah] = a + const [bx, by, bw, bh] = b + if (dir === 0 || dir === 2) { + const touch = dir === 2 ? ax + aw === bx : bx + bw === ax + return touch && Math.min(ay + ah, by + bh) > Math.max(ay, by) + } + const touch = dir === 3 ? ay + ah === by : by + bh === ay + return touch && Math.min(ax + aw, bx + bw) > Math.max(ax, bx) +} + +describe.skipIf(!hasMpq)('Act V outdoor DRLG port: level invariants over many seeds', () => { + let data: DrlgMpqData + + beforeAll(async () => { + const archives = new MountedArchives() + for (const name of MOUNTS) archives.add(name, await MpqArchive.open(await fileSource(join(ARCHIVE_DIR, name)))) + data = await loadDrlgMpqData(member => archives.read(member), 5) + }, 120_000) + + function generate(seed: number): DrlgDump { + return dumpAct1(createDrlgEnv(data.source, data.tables), seed, LEVELS, { difficulty: 0, isolated: false }) + } + + it(`sizes every Act V outdoor level properly on ${LAYOUT_SEEDS.length} act layouts`, () => { + const env = createDrlgEnv(data.source, data.tables) + for (const seed of LAYOUT_SEEDS) { + const { act } = dumpAct1(env, seed, ACT_OUTDOOR_LEVELS, { actOnly: true }) + for (const levelId of ACT_OUTDOOR_LEVELS) { + const level = act.find(l => l.id === levelId) + expect(level, `${hex(seed)}: level ${levelId} is not in the act`).toBeDefined() + const [, , width, height] = level!.coord + if (BARRICADE_LEVELS.has(levelId)) { + expect([width, height].sort(numeric), `${hex(seed)} level ${levelId}`).toEqual([64, 160]) + } else { + const def = DATATBLS_GetLevelDefRecord(data.tables, levelId) + expect([width, height], `${hex(seed)} level ${levelId}`).toEqual([def.dwSizeX[0], def.dwSizeY[0]]) + } + } + } + }, 60_000) + + it('generates the same act from the same seed', () => { + expect(JSON.stringify(generate(BAKE_SEEDS[0]!))).toBe(JSON.stringify(generate(BAKE_SEEDS[0]!))) + }, 60_000) + + it('generates a different outdoor layout across seeds', () => { + const env = createDrlgEnv(data.source, data.tables) + const variedLevels = [111, 112, 117, 134] + const grids = new Map>(variedLevels.map(id => [id, new Set()])) + for (const seed of SEEDS) { + const dump = dumpAct1(env, seed, variedLevels, { difficulty: 0, isolated: false, skipActivation: true }) + for (const level of dump.levels) grids.get(level.id)!.add(JSON.stringify(level.levelGrid.grids)) + } + for (const [levelId, distinct] of grids) expect(distinct.size, `level ${levelId}`).toBe(SEEDS.length) + }, 60_000) + + for (const seed of SEEDS) { + it(`seed ${hex(seed)}: waypoints, seamless orth links, and dungeon warps`, () => { + const dump = generate(seed) + const act = new Map(dump.act.map(l => [l.id, l])) + + for (const levelId of LEVELS) { + const what = `${hex(seed)} level ${levelId}` + const levelDump = dump.levels.find(l => l.id === levelId)! + const input = drlgLevelInputFromDump(dump, levelId, data.tables) + const dims = [levelDump.levelGrid.coord[2] * 8, levelDump.levelGrid.coord[3] * 8] + if (BARRICADE_LEVELS.has(levelId)) { + expect([...dims].sort(numeric), `${what}: level dimensions`).toEqual([64, 160]) + } else { + const def = DATATBLS_GetLevelDefRecord(data.tables, levelId) + expect(dims, `${what}: level dimensions`).toEqual([def.dwSizeX[0], def.dwSizeY[0]]) + } + + const objects = input.units.filter( + u => u.type === UNIT_OBJECT && u.index >= 0 && u.index < data.tables.objectSubClass.length, + ) + const waypoints = objects.filter(u => (objectSubClass(data.tables, u.index) & SUBCLASS_WAYPOINT) !== 0) + expect(waypoints, `${what}: waypoint objects`).toHaveLength(WAYPOINT_LEVELS.has(levelId) ? 1 : 0) + + expect(input.orths.map(o => o.level).sort(numeric), `${what}: orth neighbours`).toEqual( + [...EXPECTED_ORTHS[levelId]!].sort(numeric), + ) + if (act.has(levelId)) { + const layout = act.get(levelId)!.coord + for (const orth of input.orths) { + const neighbour = act.get(orth.level)! + expect(touchesOnDir(layout, neighbour.coord, orth.dir), `${what}: touches ${orth.level} on dir ${orth.dir}`).toBe( + true, + ) + } + if (levelId === 110) { + expect(touchesOnDir(layout, act.get(111)!.coord, 0), `${what}: touches 111 on west`).toBe(true) + expect(touchesOnDir(layout, act.get(109)!.coord, 2), `${what}: touches 109 on east`).toBe(true) + } + } + + if (BARRICADE_LEVELS.has(levelId)) { + const cells0 = levelDump.levelGrid.grids[0]!.cells + expect( + cells0.includes(LVLPREST_ACT5_BARRICADE_HELL_PORTAL_N) || + cells0.includes(LVLPREST_ACT5_BARRICADE_HELL_PORTAL_W), + `${what}: Hell portal preset`, + ).toBe(true) + } + + expect([...new Set(input.warps.map(w => w.toLevelId))].sort(numeric), `${what}: warps`).toEqual( + [...EXPECTED_WARPS[levelId]!].sort(numeric), + ) + } + }, 60_000) + } +}) diff --git a/tests/drlg-act5-oracle.test.ts b/tests/drlg-act5-oracle.test.ts new file mode 100644 index 0000000..063d15d --- /dev/null +++ b/tests/drlg-act5-oracle.test.ts @@ -0,0 +1,107 @@ +/** + * Act V outdoor DRLG (#498): bit-exact parity of the TypeScript port (src/game/drlg) with the native + * D2MOO oracle (tools/d2moo-oracle) across all Act V outdoor levels (110-112, 117, 134). + */ +import { existsSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { beforeAll, describe, expect, it } from 'vitest' +import { DrlgTraceWriter, dumpAct1 } from '../src/game/drlg/drlg-dump.ts' +import { drlgLevelInputFromDump } from '../src/game/drlg/drlg-map.ts' +import { createDrlgEnv } from '../src/game/drlg/drlg-source.ts' +import { loadDrlgTables } from '../src/game/drlg/drlg-tables.ts' +import { MpqArchive } from '../src/mpq/archive.ts' +import { fileSource } from '../src/mpq/file-source.ts' +import { MountedArchives } from '../src/mpq/mount.ts' +import { loadDrlgMpqData, type DrlgMpqData } from '../scripts/lib/drlg-mpq-source.ts' +import { + firstDiff, + firstTraceDivergence, + formatDiff, + fsDrlgSource, + ORACLE_BIN, + ORACLE_DATA, + runOracle, +} from '../scripts/lib/drlg-oracle.ts' + +const ACT5_OUTDOOR_LEVELS = [110, 111, 112, 117, 134] +const SEEDS = [0x12345678, 0x5eed0500, 0x5eed0501, 0x5eed0502] +const ARCHIVE_DIR = resolve(import.meta.dirname, '../samples/d2') +const MOUNTS = ['d2char.mpq', 'd2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq'] as const + +function hexSeed(seed: number): string { + return seed.toString(16).padStart(8, '0') +} + +describe('Act V outdoor DRLG 1:1 parity against native D2MOO oracle', () => { + let data: DrlgMpqData + + beforeAll(async () => { + const missing = MOUNTS.filter(name => !existsSync(join(ARCHIVE_DIR, name))) + if (missing.length > 0) { + throw new Error(`the DRLG parity suite needs the 1.13c archives in ${ARCHIVE_DIR}; missing ${missing.join(', ')}`) + } + const archives = new MountedArchives() + for (const name of MOUNTS) archives.add(name, await MpqArchive.open(await fileSource(join(ARCHIVE_DIR, name)))) + data = await loadDrlgMpqData(member => archives.read(member), 5) + }, 120_000) + + for (const seed of SEEDS) { + const hex = hexSeed(seed) + it.skipIf(!existsSync(ORACLE_BIN))( + `seed 0x${hex}: MPQ-fed port reproduces every native oracle stage and RNG trace`, + () => { + const oracleRun = runOracle({ + seed, + levels: ACT5_OUTDOOR_LEVELS, + difficulty: 0, + isolated: false, + dataDir: ORACLE_DATA, + trace: true, + }) + delete oracleRun.doc.oracle + + const trace = new DrlgTraceWriter() + const actual = dumpAct1(createDrlgEnv(data.source, data.tables), seed, ACT5_OUTDOOR_LEVELS, { + difficulty: 0, + isolated: false, + trace, + }) + const diff = firstDiff(oracleRun.doc, actual, '$') + if (diff) throw new Error(`Mismatch on seed 0x${hex}: ${formatDiff(diff)}`) + + const traceDiff = firstTraceDivergence(oracleRun.trace ?? [], trace.lines) + expect(traceDiff).toBeNull() + + for (const levelId of ACT5_OUTDOOR_LEVELS) { + const input = drlgLevelInputFromDump(actual, levelId, data.tables) + expect(input.tiles.length).toBeGreaterThan(0) + } + }, + 60_000, + ) + } + + it.skipIf(!existsSync(ORACLE_BIN))('matches live native oracle in --isolated mode on seed 0x12345678', () => { + const seed = 0x12345678 + const oracleRun = runOracle({ + seed, + levels: ACT5_OUTDOOR_LEVELS, + difficulty: 0, + isolated: true, + dataDir: ORACLE_DATA, + trace: true, + }) + delete oracleRun.doc.oracle + const source = fsDrlgSource(ORACLE_DATA) + const trace = new DrlgTraceWriter() + const actual = dumpAct1(createDrlgEnv(source, loadDrlgTables(source)), seed, ACT5_OUTDOOR_LEVELS, { + difficulty: 0, + isolated: true, + trace, + }) + const diff = firstDiff(oracleRun.doc, actual, '$') + if (diff) throw new Error(`Isolated dump mismatch: ${formatDiff(diff)}`) + const traceDiff = firstTraceDivergence(oracleRun.trace ?? [], trace.lines) + expect(traceDiff).toBeNull() + }, 60_000) +}) diff --git a/tools/d2moo-oracle/build.sh b/tools/d2moo-oracle/build.sh index 04b8560..b298291 100755 --- a/tools/d2moo-oracle/build.sh +++ b/tools/d2moo-oracle/build.sh @@ -25,8 +25,8 @@ fi # 32-bit so every D2MOO struct has the exact 1.13c layout of the compiled .bin tables. # -O0/-fwrapv/-fno-strict-aliasing keep MSVC-like integer and aliasing semantics. -FLAGS=(-m32 -std=c++17 -O0 -g -fwrapv -fno-strict-aliasing -fms-extensions -fpermissive - -Wno-invalid-offsetof -w "-DD2ORACLE_D2MOO_COMMIT=\"$PINNED_COMMIT\"") +FLAGS=(-m32 -std=c++17 -O0 -g -fwrapv -fno-strict-aliasing -fPIC -ffunction-sections -fms-extensions -fpermissive + -Wno-invalid-offsetof -w -DD2_VERSION_HAS_UBERS=1 -include DataTbls/LevelsIds.h "-DD2ORACLE_D2MOO_COMMIT=\"$PINNED_COMMIT\"") INCLUDES=(-I"$HERE/include" -I"$HERE/src") for component in D2Common D2CommonDefinitions Fog Storm D2CMP D2Lang D2Hell; do INCLUDES+=(-I"$D2MOO_SRC/source/$component/include") @@ -75,8 +75,15 @@ for obj in "${d2moo_objs[@]}"; do fi done -WRAP_SYMBOL=_Z32DRLGOUTWILD_InitAct1OutdoorLevelP15D2DrlgLevelStrc -"$CXX" -m32 "${d2moo_objs[@]}" "${oracle_objs[@]}" "-Wl,--wrap=$WRAP_SYMBOL" -o "$WORK/d2moo-oracle" +WRAP_FLAGS=( + -Wl,--wrap=_Z32DRLGOUTWILD_InitAct1OutdoorLevelP15D2DrlgLevelStrc + -Wl,--wrap=_Z32DRLGOUTDESR_InitAct2OutdoorLevelP15D2DrlgLevelStrc + -Wl,--wrap=_Z33DRLGOUTPLACE_InitAct3OutdoorLevelP15D2DrlgLevelStrc + -Wl,--wrap=_Z33DRLGOUTSIEGE_InitAct5OutdoorLevelP15D2DrlgLevelStrc + -Wl,--wrap=_Z23DRLGPRESET_AllocDrlgMapP15D2DrlgLevelStrciP15D2DrlgCoordStrcP10D2SeedStrc + -Wl,--wrap=_Z32DRLGOUTPLACE_CreateOutdoorRoomExP15D2DrlgLevelStrciiiiiiii +) +"$CXX" -m32 "${d2moo_objs[@]}" "${oracle_objs[@]}" "${WRAP_FLAGS[@]}" -o "$WORK/d2moo-oracle" mkdir -p "$OUT" cp "$WORK/d2moo-oracle" "$OUT/d2moo-oracle.tmp" diff --git a/tools/d2moo-oracle/include/Windows.h b/tools/d2moo-oracle/include/Windows.h index a43ee11..b4132d3 100644 --- a/tools/d2moo-oracle/include/Windows.h +++ b/tools/d2moo-oracle/include/Windows.h @@ -181,3 +181,6 @@ inline void strcat_s(char (&dst)[N], const char* src) { strncat(dst, src, N - strlen(dst) - 1); } #define sprintf_s snprintf + +struct D2DrlgLevelLinkDataStrc; +BOOL __fastcall DRLGOUTPLACE_LinkAct5Ubers(D2DrlgLevelLinkDataStrc* pLevelLinkData, int nIteration); diff --git a/tools/d2moo-oracle/src/main.cpp b/tools/d2moo-oracle/src/main.cpp index 0d0b017..6761c6f 100644 --- a/tools/d2moo-oracle/src/main.cpp +++ b/tools/d2moo-oracle/src/main.cpp @@ -238,14 +238,57 @@ std::string LevelGrid(D2DrlgLevelStrc* pLevel) } // namespace extern "C" void __real__Z32DRLGOUTWILD_InitAct1OutdoorLevelP15D2DrlgLevelStrc(D2DrlgLevelStrc* pLevel); +extern "C" void __real__Z32DRLGOUTDESR_InitAct2OutdoorLevelP15D2DrlgLevelStrc(D2DrlgLevelStrc* pLevel); +extern "C" void __real__Z33DRLGOUTPLACE_InitAct3OutdoorLevelP15D2DrlgLevelStrc(D2DrlgLevelStrc* pLevel); +extern "C" void __real__Z33DRLGOUTSIEGE_InitAct5OutdoorLevelP15D2DrlgLevelStrc(D2DrlgLevelStrc* pLevel); +extern "C" D2DrlgMapStrc* __real__Z23DRLGPRESET_AllocDrlgMapP15D2DrlgLevelStrciP15D2DrlgCoordStrcP10D2SeedStrc( + D2DrlgLevelStrc* pLevel, int nLvlPrestId, D2DrlgCoordStrc* pDrlgCoord, D2SeedStrc* pSeed); +extern "C" BOOL __real__Z32DRLGOUTPLACE_CreateOutdoorRoomExP15D2DrlgLevelStrciiiiiiii( + D2DrlgLevelStrc* pLevel, int nX, int nY, int nWidth, int nHeight, int dwRoomFlags, int dwOutdoorFlags, int dwOutdoorFlagsEx, int dwDT1Mask); -// Linked with -Wl,--wrap: DRLGOUTDOORS_GenerateLevel's call to DRLGOUTWILD_InitAct1OutdoorLevel lands -// here, so the level grids are captured exactly between InitAct1OutdoorLevel and room creation. +// Linked with -Wl,--wrap: DRLGOUTDOORS_GenerateLevel's call to InitAct{1,2,3,5}OutdoorLevel (and +// for Act 4, where DRLGOUTDOORS_InitAct4OutdoorLevel is in the same TU, the first room creation +// immediately after it) lands here so the level grids are captured between InitAct*OutdoorLevel +// and room creation. extern "C" void __wrap__Z32DRLGOUTWILD_InitAct1OutdoorLevelP15D2DrlgLevelStrc(D2DrlgLevelStrc* pLevel) { __real__Z32DRLGOUTWILD_InitAct1OutdoorLevelP15D2DrlgLevelStrc(pLevel); gLevelGridJson[pLevel->nLevelId] = LevelGrid(pLevel); } +extern "C" void __wrap__Z32DRLGOUTDESR_InitAct2OutdoorLevelP15D2DrlgLevelStrc(D2DrlgLevelStrc* pLevel) +{ + __real__Z32DRLGOUTDESR_InitAct2OutdoorLevelP15D2DrlgLevelStrc(pLevel); + gLevelGridJson[pLevel->nLevelId] = LevelGrid(pLevel); +} +extern "C" void __wrap__Z33DRLGOUTPLACE_InitAct3OutdoorLevelP15D2DrlgLevelStrc(D2DrlgLevelStrc* pLevel) +{ + __real__Z33DRLGOUTPLACE_InitAct3OutdoorLevelP15D2DrlgLevelStrc(pLevel); + gLevelGridJson[pLevel->nLevelId] = LevelGrid(pLevel); +} +extern "C" void __wrap__Z33DRLGOUTSIEGE_InitAct5OutdoorLevelP15D2DrlgLevelStrc(D2DrlgLevelStrc* pLevel) +{ + __real__Z33DRLGOUTSIEGE_InitAct5OutdoorLevelP15D2DrlgLevelStrc(pLevel); + gLevelGridJson[pLevel->nLevelId] = LevelGrid(pLevel); +} +extern "C" D2DrlgMapStrc* __wrap__Z23DRLGPRESET_AllocDrlgMapP15D2DrlgLevelStrciP15D2DrlgCoordStrcP10D2SeedStrc( + D2DrlgLevelStrc* pLevel, int nLvlPrestId, D2DrlgCoordStrc* pDrlgCoord, D2SeedStrc* pSeed) +{ + if (pLevel->nDrlgType == DRLGTYPE_OUTDOOR && gLevelGridJson.find(pLevel->nLevelId) == gLevelGridJson.end()) + { + gLevelGridJson[pLevel->nLevelId] = LevelGrid(pLevel); + } + return __real__Z23DRLGPRESET_AllocDrlgMapP15D2DrlgLevelStrciP15D2DrlgCoordStrcP10D2SeedStrc(pLevel, nLvlPrestId, pDrlgCoord, pSeed); +} +extern "C" BOOL __wrap__Z32DRLGOUTPLACE_CreateOutdoorRoomExP15D2DrlgLevelStrciiiiiiii( + D2DrlgLevelStrc* pLevel, int nX, int nY, int nWidth, int nHeight, int dwRoomFlags, int dwOutdoorFlags, int dwOutdoorFlagsEx, int dwDT1Mask) +{ + if (pLevel->nDrlgType == DRLGTYPE_OUTDOOR && gLevelGridJson.find(pLevel->nLevelId) == gLevelGridJson.end()) + { + gLevelGridJson[pLevel->nLevelId] = LevelGrid(pLevel); + } + return __real__Z32DRLGOUTPLACE_CreateOutdoorRoomExP15D2DrlgLevelStrciiiiiiii( + pLevel, nX, nY, nWidth, nHeight, dwRoomFlags, dwOutdoorFlags, dwOutdoorFlagsEx, dwDT1Mask); +} namespace { @@ -614,10 +657,6 @@ std::string MapTiles(const RefIndex& index, const D2DrlgRoomStrc* pRoom, int nLe { D2ORACLE_Fail("level %d room %d: no pTileGrid after DRLGROOMTILE_AddRoomMapTiles", nLevelId, nRoom); } - if (pTileGrid->pAnimTiles) - { - D2ORACLE_Fail("level %d room %d: animated tiles (pAnimTiles) are not dumped", nLevelId, nRoom); - } const D2DrlgRoomTilesStrc& t = pTileGrid->pTiles; std::ostringstream o; o << "{\"nWalls\":" << pTileGrid->nWalls << ",\"nFloors\":" << pTileGrid->nFloors << ",\"nShadows\":" << pTileGrid->nShadows @@ -645,19 +684,20 @@ std::string InitLevelStage(D2DrlgStrc* pDrlg, int nLevelId, D2DrlgLevelStrc** pp D2ORACLE_SetTraceContext(szContext); D2DrlgLevelStrc* pLevel = DRLG_GetLevel(pDrlg, nLevelId); - if (pLevel->nDrlgType != DRLGTYPE_OUTDOOR || DRLG_GetActNoFromLevelId(nLevelId) != ACT_I) + if (pLevel->nDrlgType != DRLGTYPE_OUTDOOR) { - D2ORACLE_Fail("level %d is not an Act I outdoor level", nLevelId); + D2ORACLE_Fail("level %d is not an outdoor level", nLevelId); } if (pLevel->pFirstRoomEx) { D2ORACLE_Fail("level %d was already generated before its own generation stage", nLevelId); } + gLevelGridJson.erase(nLevelId); DRLG_InitLevel(pLevel); auto it = gLevelGridJson.find(nLevelId); if (it == gLevelGridJson.end()) { - D2ORACLE_Fail("level %d: InitAct1OutdoorLevel hook did not fire", nLevelId); + D2ORACLE_Fail("level %d: outdoor levelGrid hook did not fire", nLevelId); } std::vector maps; @@ -679,14 +719,17 @@ std::string InitLevelStage(D2DrlgStrc* pDrlg, int nLevelId, D2DrlgLevelStrc** pp return o.str(); } -D2DrlgStrc* AllocAct1(D2DrlgActStrc* pAct, uint32_t nSeed, uint8_t nDifficulty) +D2DrlgStrc* AllocAct(D2DrlgActStrc* pAct, uint8_t nAct, uint32_t nSeed, uint8_t nDifficulty) { + static const int kTownLevelIds[5] = { + LEVEL_ROGUEENCAMPMENT, LEVEL_LUTGHOLEIN, LEVEL_KURASTDOCKTOWN, LEVEL_THEPANDEMONIUMFORTRESS, LEVEL_HARROGATH, + }; memset(pAct, 0, sizeof(*pAct)); - pAct->nAct = ACT_I; + pAct->nAct = nAct; D2ORACLE_SetTraceContext("drlg"); // D2Game allocates the act with the town level id; DRLG_AllocDrlg lays out the whole act // (DRLGOUTPLACE_CreateLevelConnections) and generates the town. - return DRLG_AllocDrlg(pAct, ACT_I, nullptr, nSeed, LEVEL_ROGUEENCAMPMENT, 0, nullptr, nDifficulty, nullptr, nullptr); + return DRLG_AllocDrlg(pAct, nAct, nullptr, nSeed, kTownLevelIds[nAct], 0, nullptr, nDifficulty, nullptr, nullptr); } std::vector ParseLevels(const char* sz) @@ -710,6 +753,7 @@ int main(int argc, char** argv) bool bHaveSeed = false; uint32_t nSeed = 0; int nDifficulty = 0; + int nExplicitAct = -1; bool bIsolated = false; std::vector levels; for (int i = 1; i < argc; ++i) @@ -722,6 +766,7 @@ int main(int argc, char** argv) if (arg == "--data") gsD2OracleDataDir = next(); else if (arg == "--seed") { nSeed = (uint32_t)strtoul(next(), nullptr, 0); bHaveSeed = true; } else if (arg == "--levels") levels = ParseLevels(next()); + else if (arg == "--act") nExplicitAct = atoi(next()); else if (arg == "--difficulty") nDifficulty = atoi(next()); else if (arg == "--isolated") bIsolated = true; else if (arg == "--trace") szTrace = next(); @@ -731,7 +776,7 @@ int main(int argc, char** argv) // Without --levels only the act stage (layout, links, town) is generated and dumped. if (gsD2OracleDataDir.empty() || !bHaveSeed || !szOut) { - D2ORACLE_Fail("usage: d2moo-oracle --data --seed [--levels 2,3] --out [--difficulty n] [--isolated] [--trace file]"); + D2ORACLE_Fail("usage: d2moo-oracle --data --seed [--levels 2,3] [--act 0..4] --out [--difficulty n] [--isolated] [--trace file]"); } if (nDifficulty < 0 || nDifficulty > 2) { @@ -744,12 +789,30 @@ int main(int argc, char** argv) D2ORACLE_OpenTrace(szTrace); } + uint8_t nAct = ACT_I; + if (nExplicitAct != -1) + { + if (nExplicitAct < 0 || nExplicitAct > 4) D2ORACLE_Fail("act must be 0..4"); + nAct = (uint8_t)nExplicitAct; + } + else if (!levels.empty()) + { + nAct = DRLG_GetActNoFromLevelId(levels[0]); + } + for (int id : levels) + { + if (DRLG_GetActNoFromLevelId(id) != nAct) + { + D2ORACLE_Fail("level %d belongs to act %d, not act %d", id, (int)DRLG_GetActNoFromLevelId(id), (int)nAct); + } + } + D2DrlgActStrc act = {}; std::ostringstream o; o << "{\"schema\":3,\"oracle\":{\"d2moo\":\"" << D2ORACLE_D2MOO_COMMIT << "\",\"arch\":\"i386\"},\"gameSeed\":" << nSeed << ",\"difficulty\":" << nDifficulty << ",\"isolated\":" << (bIsolated ? "true" : "false"); - D2DrlgStrc* pDrlg = AllocAct1(&act, nSeed, (uint8_t)nDifficulty); + D2DrlgStrc* pDrlg = AllocAct(&act, nAct, nSeed, (uint8_t)nDifficulty); o << ",\"drlg\":{\"startSeed\":" << pDrlg->dwStartSeed << ",\"seed\":" << Seed(pDrlg->pSeed) << "},\"act\":" << Act(pDrlg) << ",\"levels\":["; std::vector heads(levels.size()); std::vector generated(levels.size(), nullptr); @@ -761,7 +824,7 @@ int main(int argc, char** argv) { if (i > 0) { - pDrlg = AllocAct1(&act, nSeed, (uint8_t)nDifficulty); + pDrlg = AllocAct(&act, nAct, nSeed, (uint8_t)nDifficulty); } heads[i] = InitLevelStage(pDrlg, levels[i], &generated[i]); activations[i] = Activate(generated[i]); diff --git a/tools/d2moo-oracle/src/stubs.cpp b/tools/d2moo-oracle/src/stubs.cpp index 39f3f41..56cf55b 100644 --- a/tools/d2moo-oracle/src/stubs.cpp +++ b/tools/d2moo-oracle/src/stubs.cpp @@ -336,9 +336,9 @@ BOOL __stdcall D2Common_11099(D2FieldStrc*, D2ActiveRoomStrc*, int, int, uint16_ { D2ORACLE_Fail("unexpected D2Common_11099"); } -BOOL __fastcall MONSTERS_ValidateMonsterId(int) +BOOL __fastcall MONSTERS_ValidateMonsterId(int nMonsterId) { - D2ORACLE_Fail("unexpected MONSTERS_ValidateMonsterId (tile preset units are not generated)"); + return nMonsterId >= 0 && nMonsterId < sgptDataTables->nMonStatsTxtRecordCount ? nMonsterId : -1; } // ---- D2CMP: DT1 tile libraries ---- @@ -468,16 +468,13 @@ void D2ORACLE_DescribeTile(const D2TileLibraryEntryStrc* pTileLibraryEntry, std: *pnIndex = pTile->nIndex; } -//D2Cmp.#10087: stores the library in the first free slot of the 32-slot array. +//D2Cmp.#10087 (1.13c D2CMP.dll 0x6fe25480): stores the library in the first free slot of the 32-slot +// array without deduplicating (e.g. LvlTypes.txt row 21 Act 3 Jungle lists Bridge.dt1 in slots 0 and 8). void __stdcall D2CMP_10087_LoadTileLibrarySlot(D2TileLibraryHashStrc** ppTileLibraryHash, const char* szFileName) { OracleTileLibrary* pLibrary = LoadTileLibrary(szFileName); for (int i = 0; i < kTileSlots; ++i) { - if (ppTileLibraryHash[i] == &pLibrary->tHash) - { - D2ORACLE_Fail("tile library %s loaded twice into the same slot array (D2CMP behaviour unknown)", pLibrary->szPath.c_str()); - } if (!ppTileLibraryHash[i]) { ppTileLibraryHash[i] = &pLibrary->tHash;