This commit is contained in:
parent
298c6f6b39
commit
d887b39485
|
|
@ -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<number, number[]> = {
|
||||
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 })
|
||||
|
|
|
|||
|
|
@ -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<number>()
|
||||
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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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<D2DrlgTileDataStrc | null>(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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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}`)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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, [])
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]!
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Record<number, readonly number[]>> = {
|
||||
41: [55],
|
||||
42: [56],
|
||||
43: [62],
|
||||
44: [65],
|
||||
45: [58],
|
||||
46: [66, 67, 68, 69, 70, 71, 72],
|
||||
}
|
||||
|
||||
const EXPECTED_ORTHS: Readonly<Record<number, readonly number[]>> = {
|
||||
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<number, Set<string>>(LEVELS.map(id => [id, new Set<string>()]))
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
|
@ -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)
|
||||
})
|
||||
|
|
@ -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<Record<number, readonly number[]>> = {
|
||||
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<number, Set<string>>([76, 77, 78].map(id => [id, new Set<string>()]))
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
|
@ -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)
|
||||
})
|
||||
|
|
@ -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<Record<number, readonly number[]>> = {
|
||||
104: [],
|
||||
105: [],
|
||||
106: [107],
|
||||
108: [],
|
||||
}
|
||||
|
||||
const EXPECTED_ORTHS: Readonly<Record<number, readonly number[]>> = {
|
||||
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<number, Set<string>>([104, 105, 106].map(id => [id, new Set<string>()]))
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
|
@ -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)
|
||||
})
|
||||
|
|
@ -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<Record<number, readonly number[]>> = {
|
||||
110: [],
|
||||
111: [],
|
||||
112: [113],
|
||||
117: [115, 118],
|
||||
134: [],
|
||||
}
|
||||
|
||||
const EXPECTED_ORTHS: Readonly<Record<number, readonly number[]>> = {
|
||||
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<number, Set<string>>(variedLevels.map(id => [id, new Set<string>()]))
|
||||
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)
|
||||
}
|
||||
})
|
||||
|
|
@ -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)
|
||||
})
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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<D2DrlgMapStrc*> 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<int> 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<int> 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 <dir> --seed <n> [--levels 2,3] --out <file> [--difficulty n] [--isolated] [--trace file]");
|
||||
D2ORACLE_Fail("usage: d2moo-oracle --data <dir> --seed <n> [--levels 2,3] [--act 0..4] --out <file> [--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<std::string> heads(levels.size());
|
||||
std::vector<D2DrlgLevelStrc*> 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]);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue