diablo2-web/src/game/drlg/drlg-room.ts

538 lines
17 KiB
TypeScript

/**
* D2Common DRLG room helpers: 1:1 port of the parts of D2MOO
* source/D2Common/src/Drlg/DrlgDrlgRoom.cpp (commit 5596f5c, MIT License, Copyright (c) 2020-2025
* The Phrozen Keep community) used by the outdoor generator.
*/
import { newD2Seed, SEED_InitLowSeed, SEED_RollRandomNumber } from '../d2-rng.ts'
import { DRLG_GetLevel, DRLG_InitLevel, DRLG_IsTownLevel } from './drlg-drlg.ts'
import { DATATBLS_GetLevelDefRecord } from './drlg-tables.ts'
import {
DRLGLEVELFLAG_AUTOMAP_REVEAL,
DRLGROOMFLAG_AUTOMAP_REVEAL,
DRLGROOMFLAG_HAS_WARP_0,
DRLGROOMFLAG_HAS_WARP_MASK,
DRLGROOMFLAG_POPULATION_ZERO,
DRLGTYPE_MAZE,
DRLGTYPE_PRESET,
newCoord,
newOrth,
newOutdoorRoom,
newPresetUnit,
type D2DrlgCoordStrc,
type D2DrlgLevelStrc,
type D2DrlgOrthStrc,
type D2DrlgRoomStrc,
type D2DrlgStrc,
type D2PresetUnitStrc,
type D2RoomTileStrc,
} from './drlg-types.ts'
import { DRLGPRESET_AllocPresetRoomData } from './drlg-preset.ts'
import { DRLGWARP_GetLvlWarpTxtRecordFromWarpIdAndDirection, DRLGWARP_GetWarpDestinationFromArray } from './drlg-warp.ts'
//D2Common.0x6FD771C0
// fRoomStatus = 4 is not modelled (room status lists only matter to the activation scheduler).
export function DRLGROOM_AllocRoomEx(pLevel: D2DrlgLevelStrc, nType: number): D2DrlgRoomStrc {
const pDrlgRoom: D2DrlgRoomStrc = {
pLevel,
pDrlgCoord: newCoord(),
dwFlags: 0,
dwOtherFlags: 0,
nType,
pMaze: null,
pOutdoor: null,
dwDT1Mask: 0,
pTiles: [],
pTileGrid: null,
nRoomsNear: 0,
ppRoomsNear: [],
pRoomTiles: null,
pPresetUnits: null,
pDrlgOrth: null,
pSeed: newD2Seed(),
dwInitSeed: 0,
pLogicalRoomInfo: null,
pRoom: null,
pDrlgRoomNext: null,
}
SEED_InitLowSeed(pDrlgRoom.pSeed, SEED_RollRandomNumber(pLevel.pSeed))
pDrlgRoom.dwInitSeed = SEED_RollRandomNumber(pDrlgRoom.pSeed) >>> 0
if (pLevel.dwFlags & DRLGLEVELFLAG_AUTOMAP_REVEAL) {
pDrlgRoom.dwFlags = (pDrlgRoom.dwFlags | DRLGROOMFLAG_AUTOMAP_REVEAL) >>> 0
}
if (nType === DRLGTYPE_MAZE) {
DRLGOUTROOM_AllocDrlgOutdoorRoom(pDrlgRoom)
} else if (nType === DRLGTYPE_PRESET) {
DRLGPRESET_AllocPresetRoomData(pDrlgRoom)
}
return pDrlgRoom
}
//D2Common.0x6FD83DE0 (DrlgOutRoom.cpp)
export function DRLGOUTROOM_AllocDrlgOutdoorRoom(pDrlgRoom: D2DrlgRoomStrc): void {
pDrlgRoom.pOutdoor = newOutdoorRoom()
}
//D2Common.0x6FD780E0
export function DRLGROOM_AllocPresetUnit(
pDrlgRoom: D2DrlgRoomStrc | null,
nUnitType: number,
nIndex: number,
nMode: number,
nX: number,
nY: number,
): D2PresetUnitStrc {
const pPresetUnit = newPresetUnit()
pPresetUnit.nUnitType = nUnitType
pPresetUnit.nMode = nMode
pPresetUnit.nIndex = nIndex
pPresetUnit.nYpos = nY
pPresetUnit.nXpos = nX
if (pDrlgRoom) {
pPresetUnit.pNext = pDrlgRoom.pPresetUnits
pDrlgRoom.pPresetUnits = pPresetUnit
} else {
pPresetUnit.pNext = null
}
return pPresetUnit
}
//D2Common.0x6FD77600
// C: DRLGROOM_AddOrth(D2DrlgOrthStrc** ppDrlgOrth, ...). Takes *ppDrlgOrth, returns its new value.
export function DRLGROOM_AddOrth(
pHead: D2DrlgOrthStrc | null,
pLevel: D2DrlgLevelStrc,
nDirection: number,
bIsPreset: boolean,
): D2DrlgOrthStrc {
const pNew = newOrth()
pNew.pLevel = pLevel
pNew.nDirection = nDirection & 0xff
pNew.bPreset = bIsPreset ? 1 : 0
pNew.bInit = 0
pNew.pBox = pLevel.pLevelCoords
if (!pHead) return pNew
let pNext = pHead.pNext
let pPrevious = pHead
if (pNext) {
do {
if (sub_6FD776B0(pNext, pNew)) break
pPrevious = pNext
pNext = pNext.pNext
} while (pNext)
} else if (sub_6FD776B0(pHead, pNew)) {
pNew.pNext = pHead
return pNew
}
pNew.pNext = pNext
pPrevious.pNext = pNew
return pHead
}
//D2Common.0x6FD776B0
export function sub_6FD776B0(pDrlgOrth1: D2DrlgOrthStrc, pDrlgOrth2: D2DrlgOrthStrc): boolean {
if (pDrlgOrth1.nDirection <= pDrlgOrth2.nDirection) {
if (pDrlgOrth1.nDirection === pDrlgOrth2.nDirection) {
switch (pDrlgOrth2.nDirection) {
case 0:
if (pDrlgOrth1.pBox!.nPosY <= pDrlgOrth2.pBox!.nPosY) return false
break
case 1:
if (pDrlgOrth1.pBox!.nPosX >= pDrlgOrth2.pBox!.nPosX) return false
break
case 2:
if (pDrlgOrth1.pBox!.nPosY >= pDrlgOrth2.pBox!.nPosY) return false
break
case 3:
if (pDrlgOrth1.pBox!.nPosX <= pDrlgOrth2.pBox!.nPosX) return false
break
default:
return false
}
} else {
return false
}
}
return true
}
// Helper function (DrlgDrlgRoom.cpp:365)
function DRLG_ComputeManhattanDistance(c1: D2DrlgCoordStrc, c2: D2DrlgCoordStrc): { nX: number; nY: number } {
const nX = c1.nPosX >= c2.nPosX ? c1.nPosX - c2.nWidth - c2.nPosX : c2.nPosX - c1.nWidth - c1.nPosX
const nY = c1.nPosY >= c2.nPosY ? c1.nPosY - c2.nHeight - c2.nPosY : c2.nPosY - c1.nHeight - c1.nPosY
return { nX, nY }
}
//D2Common.0x6FD77740
export function DRLG_GetRectanglesManhattanDistanceAndCheckNotOverlapping(
c1: D2DrlgCoordStrc,
c2: D2DrlgCoordStrc,
nMaxDistance: number,
): { result: boolean; nDistanceX: number; nDistanceY: number } {
const d = DRLG_ComputeManhattanDistance(c1, c2)
return { result: d.nX >= nMaxDistance || d.nY >= nMaxDistance, nDistanceX: d.nX, nDistanceY: d.nY }
}
//D2Common.0x6FD777B0
export function DRLG_CheckNotOverlappingUsingManhattanDistance(c1: D2DrlgCoordStrc, c2: D2DrlgCoordStrc, nMaxDistanceToAssumeCollision: number): boolean {
const d = DRLG_ComputeManhattanDistance(c1, c2)
return d.nX >= nMaxDistanceToAssumeCollision || d.nY >= nMaxDistanceToAssumeCollision
}
//D2Common.0x6FD77800
export function DRLG_CheckOverlappingWithOrthogonalMargin(c1: D2DrlgCoordStrc, c2: D2DrlgCoordStrc, nOrthogonalDistanceMax: number): boolean {
const d = DRLG_ComputeManhattanDistance(c1, c2)
if (nOrthogonalDistanceMax) {
if (d.nX === 0 && d.nY <= nOrthogonalDistanceMax) return true
if (d.nY === 0 && d.nX <= nOrthogonalDistanceMax) return true
} else if (d.nX <= 0 && d.nY <= 0) {
return true
}
return false
}
//D2Common.0x6FD773B0
export function DRLGROOM_FreeRoomEx(pDrlgRoom: D2DrlgRoomStrc): void {
pDrlgRoom.nRoomsNear = 0
pDrlgRoom.ppRoomsNear = []
pDrlgRoom.pOutdoor = null
pDrlgRoom.pMaze = null
pDrlgRoom.pPresetUnits = null
let pNextDrlgOrth: D2DrlgOrthStrc | null = null
for (let pDrlgOrth = pDrlgRoom.pDrlgOrth; pDrlgOrth; pDrlgOrth = pNextDrlgOrth) {
pNextDrlgOrth = pDrlgOrth.pNext
if (pDrlgOrth.bInit === 1) {
const pCurrentRoomEx = pDrlgOrth.pDrlgRoom!
let pOrth = pDrlgRoom.pDrlgOrth!
let pNextOrth = pOrth.pNext
if (pOrth.bInit === 1 && pOrth.pDrlgRoom === pCurrentRoomEx) {
pDrlgRoom.pDrlgOrth = pNextOrth
} else {
while (pNextOrth) {
if (pNextOrth.bInit === 1 && pNextOrth.pDrlgRoom === pCurrentRoomEx) {
pOrth.pNext = pNextOrth.pNext
break
}
pOrth = pNextOrth
pNextOrth = pNextOrth.pNext
}
}
if (pCurrentRoomEx.pDrlgOrth) {
pOrth = pCurrentRoomEx.pDrlgOrth
pNextOrth = pOrth.pNext
if (pOrth.bInit === 1 && pOrth.pDrlgRoom === pDrlgRoom) {
pCurrentRoomEx.pDrlgOrth = pNextOrth
} else {
while (pNextOrth) {
if (pNextOrth.bInit === 1 && pNextOrth.pDrlgRoom === pDrlgRoom) {
pOrth.pNext = pNextOrth.pNext
break
}
pOrth = pNextOrth
pNextOrth = pNextOrth.pNext
}
}
}
}
}
pDrlgRoom.pDrlgOrth = null
let pCurrentRoomEx = pDrlgRoom.pLevel.pFirstRoomEx
if (pCurrentRoomEx === pDrlgRoom) {
pDrlgRoom.pLevel.pFirstRoomEx = pDrlgRoom.pDrlgRoomNext
pDrlgRoom.pLevel.nRooms -= 1
} else if (pCurrentRoomEx) {
let pNextRoomEx = pCurrentRoomEx.pDrlgRoomNext
while (pNextRoomEx) {
if (pNextRoomEx === pDrlgRoom) {
pCurrentRoomEx.pDrlgRoomNext = pDrlgRoom.pDrlgRoomNext
pDrlgRoom.pLevel.nRooms -= 1
break
}
pCurrentRoomEx = pNextRoomEx
pNextRoomEx = pCurrentRoomEx.pDrlgRoomNext
}
}
}
//D2Common.0x6FD77520
export function DRLGROOM_AllocDrlgOrthsForRooms(pDrlgRoom1: D2DrlgRoomStrc, pDrlgRoom2: D2DrlgRoomStrc, nDirection: number): void {
let pDrlgOrth = pDrlgRoom1.pDrlgOrth
while (pDrlgOrth) {
if (pDrlgOrth.pDrlgRoom === pDrlgRoom2) break
pDrlgOrth = pDrlgOrth.pNext
}
if (!pDrlgOrth) {
const pNew = newOrth()
pNew.pNext = pDrlgRoom1.pDrlgOrth
pDrlgRoom1.pDrlgOrth = pNew
pNew.pDrlgRoom = pDrlgRoom2
pNew.nDirection = nDirection & 0xff
pNew.bInit = 1
pNew.pBox = pDrlgRoom2.pDrlgCoord
}
pDrlgOrth = pDrlgRoom2.pDrlgOrth
while (pDrlgOrth) {
if (pDrlgOrth.pDrlgRoom === pDrlgRoom1) break
pDrlgOrth = pDrlgOrth.pNext
}
if (!pDrlgOrth) {
const pNew = newOrth()
pNew.pNext = pDrlgRoom2.pDrlgOrth
pDrlgRoom2.pDrlgOrth = pNew
pNew.pDrlgRoom = pDrlgRoom1
pNew.nDirection = ((nDirection & 0xff) - 2) & 3
pNew.bInit = 1
pNew.pBox = pDrlgRoom1.pDrlgCoord
}
}
//D2Common.0x6FD77890
export function DRLGMAZE_CheckRoomNotOverlaping(
pLevel: D2DrlgLevelStrc,
pDrlgRoom1: D2DrlgRoomStrc,
pIgnoredRoom: D2DrlgRoomStrc | null,
nMargin: number,
): boolean {
for (let pCurrentRoomEx = pLevel.pFirstRoomEx; pCurrentRoomEx; pCurrentRoomEx = pCurrentRoomEx.pDrlgRoomNext) {
if (pCurrentRoomEx !== pDrlgRoom1 && pCurrentRoomEx !== pIgnoredRoom) {
if (!DRLG_CheckNotOverlappingUsingManhattanDistance(pDrlgRoom1.pDrlgCoord, pCurrentRoomEx.pDrlgCoord, nMargin)) {
return false
}
}
}
return true
}
//D2Common.0x6FD77910
export function DRLGROOM_AddRoomExToLevel(pLevel: D2DrlgLevelStrc, pDrlgRoom: D2DrlgRoomStrc): void {
pDrlgRoom.pDrlgRoomNext = pLevel.pFirstRoomEx
pLevel.pFirstRoomEx = pDrlgRoom
pLevel.nRooms += 1
}
//D2Common.0x6FD77930
export function DRLGROOM_AreXYInsideCoordinates(c: D2DrlgCoordStrc, nX: number, nY: number): boolean {
return nX >= c.nPosX && nY >= c.nPosY && nX < c.nPosX + c.nWidth && nY < c.nPosY + c.nHeight
}
//D2Common.0x6FD77980
export function DRLGROOM_AreXYInsideCoordinatesOrOnBorder(c: D2DrlgCoordStrc, nX: number, nY: number): boolean {
return nX >= c.nPosX && nY >= c.nPosY && nX <= c.nPosX + c.nWidth && nY <= c.nPosY + c.nHeight
}
//D2Common.0x6FD781E0
export function DRLGROOM_GetVisArrayFromLevelId(pDrlg: D2DrlgStrc, nLevelId: number): readonly number[] {
for (let pDrlgWarp = pDrlg.pWarp; pDrlgWarp; pDrlgWarp = pDrlgWarp.pNext) {
if (!pDrlgWarp.nLevel) throw new Error('ptVisInfo->eLevelId != LEVEL_ID_NONE')
if (nLevelId === pDrlgWarp.nLevel) return pDrlgWarp.nVis
}
return DATATBLS_GetLevelDefRecord(pDrlg.env.tables, nLevelId).dwVis
}
//D2Common.0x6FD77EB0
export function DRLGROOM_SortRoomListByPosition(ppRoomList: D2DrlgRoomStrc[], nListSize: number): void {
for (let j = nListSize - 1; j > 0; j -= 1) {
for (let i = 0; i < nListSize - 1; i += 1) {
const pDrlgRoom1 = ppRoomList[i + 1]!
const pDrlgRoom2 = ppRoomList[i]!
if (
pDrlgRoom2.pDrlgCoord.nPosX >= pDrlgRoom1.pDrlgCoord.nPosX + pDrlgRoom1.pDrlgCoord.nWidth ||
pDrlgRoom2.pDrlgCoord.nPosY >= pDrlgRoom1.pDrlgCoord.nPosY + pDrlgRoom1.pDrlgCoord.nHeight
) {
ppRoomList[i] = pDrlgRoom1
ppRoomList[i + 1] = pDrlgRoom2
}
}
}
}
//D2Common.0x6FD77F00
export function sub_6FD77F00(
pDrlgRoom1: D2DrlgRoomStrc,
nWarpId: number,
pDrlgRoom2: D2DrlgRoomStrc | null,
nWarpFlag: number,
nDirection: number,
): boolean {
let bResult = false
while (pDrlgRoom2) {
if (((1 << (nWarpFlag + 4)) & pDrlgRoom2.dwFlags) !== 0) {
if (nDirection === -1) {
let nX = pDrlgRoom1.pDrlgCoord.nPosX - pDrlgRoom2.pDrlgCoord.nWidth - pDrlgRoom2.pDrlgCoord.nPosX
if (nX < 0) {
nX = pDrlgRoom2.pDrlgCoord.nPosX - (pDrlgRoom1.pDrlgCoord.nPosX + pDrlgRoom1.pDrlgCoord.nWidth)
}
let nY = pDrlgRoom1.pDrlgCoord.nPosY - pDrlgRoom2.pDrlgCoord.nHeight - pDrlgRoom2.pDrlgCoord.nPosY
if (nY < 0) {
nY = pDrlgRoom2.pDrlgCoord.nPosY - (pDrlgRoom1.pDrlgCoord.nPosY + pDrlgRoom1.pDrlgCoord.nHeight)
}
if (nX < 0) nX = 0
if (nY < 0) nY = 0
if (nX < 6 && nY < 6) {
pDrlgRoom1.ppRoomsNear.push(pDrlgRoom2)
pDrlgRoom1.nRoomsNear += 1
DRLGROOM_SortRoomListByPosition(pDrlgRoom1.ppRoomsNear, pDrlgRoom1.nRoomsNear)
bResult = true
}
} else {
pDrlgRoom1.ppRoomsNear.push(pDrlgRoom2)
pDrlgRoom1.nRoomsNear += 1
DRLGROOM_SortRoomListByPosition(pDrlgRoom1.ppRoomsNear, pDrlgRoom1.nRoomsNear)
// D2_CALLOC_STRC_POOL(D2RoomTileStrc)
const pRoomTile: D2RoomTileStrc = {
pDrlgRoom: pDrlgRoom2,
pLvlWarpTxtRecord: DRLGWARP_GetLvlWarpTxtRecordFromWarpIdAndDirection(pDrlgRoom1.pLevel, nWarpId, 'b'),
bEnabled: 1,
unk0x0C: null,
unk0x10: null,
pNext: pDrlgRoom1.pRoomTiles,
}
pDrlgRoom1.pRoomTiles = pRoomTile
return true
}
}
pDrlgRoom2 = pDrlgRoom2.pDrlgRoomNext
}
return bResult
}
//D2Common.0x6FD77BB0
export function sub_6FD77BB0(pDrlgRoom: D2DrlgRoomStrc): void {
let bSkip = false
const pDrlg = pDrlgRoom.pLevel.pDrlg
let nNearRooms = 0
for (let pNearRoom = pDrlgRoom.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) {
nNearRooms += 1
}
}
const ppRoomList: D2DrlgRoomStrc[] = []
let nNearRoomsCounter = 0
for (let pNearRoom = pDrlgRoom.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) {
ppRoomList[nNearRoomsCounter] = pNearRoom
nNearRoomsCounter += 1
}
}
DRLGROOM_SortRoomListByPosition(ppRoomList, nNearRooms)
pDrlgRoom.ppRoomsNear = ppRoomList
pDrlgRoom.nRoomsNear = nNearRooms
if (pDrlgRoom.dwFlags & DRLGROOMFLAG_HAS_WARP_MASK) {
let nFlags = DRLGROOMFLAG_HAS_WARP_0
let nWarpId = 0
do {
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]!)
const nWarpDestination = DRLGWARP_GetWarpDestinationFromArray(pDrlgRoom.pLevel, nWarpId)
if (!pLevel.pFirstRoomEx) {
DRLG_InitLevel(pLevel)
}
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
if (sub_6FD77F00(pDrlgRoom, nWarpId, pNearRoom, i, nWarpDestination)) {
bSkip = true
break
}
}
}
}
if (!bSkip) {
for (let i = 0; i < 8; i += 1) {
if (pDestinationVisArray[i] === pDrlgRoom.pLevel.nLevelId) {
const pNearRoom = pLevel.pFirstRoomEx
if (sub_6FD77F00(pDrlgRoom, nWarpId, pNearRoom, i, nWarpDestination)) {
break
}
}
}
}
}
nFlags <<= 1
nWarpId += 1
} while (nFlags & DRLGROOMFLAG_HAS_WARP_MASK)
}
if (!DRLG_IsTownLevel(pDrlgRoom.pLevel.nLevelId)) {
for (let i = 0; i < pDrlgRoom.nRoomsNear; i += 1) {
if (DRLG_IsTownLevel(pDrlgRoom.ppRoomsNear[i]!.pLevel.nLevelId)) {
pDrlgRoom.dwFlags = (pDrlgRoom.dwFlags | DRLGROOMFLAG_POPULATION_ZERO) >>> 0
return
}
}
}
}