fix(map-gen): fix Act 4 Chaos Sanctuary, Act 3 Jungles/Travincal/Kurast, Act 2 Deserts & Act 5 Frozen Tundra layouts
This commit is contained in:
parent
25ae7289ee
commit
602565030f
|
|
@ -530,6 +530,8 @@ function themeValues(table: D2Table, row: readonly string[], prefix: string): nu
|
|||
const WILDERNESS_PIECE_FAMILIES: Readonly<Record<string, readonly string[]>> = {
|
||||
'Act 1 - Wilderness': [
|
||||
'Act 1 - Wild',
|
||||
'Act 1 - DOE Entrance',
|
||||
'Act 1 - Cave Entrance',
|
||||
'Act 1 - Corral Fill',
|
||||
'Act 1 - Fence Fill',
|
||||
'Act 1 - River',
|
||||
|
|
@ -549,10 +551,10 @@ const WILDERNESS_PIECE_FAMILIES: Readonly<Record<string, readonly string[]>> = {
|
|||
'Act 1 - Graveyard',
|
||||
],
|
||||
'Act 2 - Desert': ['Act 2 - Desert'],
|
||||
'Act 3 - Jungle': ['Act 3 - Jungle'],
|
||||
'Act 3 - Jungle': ['Act 3 - Jungle', 'Act 3 - Clearing'],
|
||||
'Act 3 - Kurast': ['Act 3 - Burst', 'Act 3 - Burbs', 'Act 3 - Clearing', 'Act 3 - Slums', 'Act 3 - Metro', 'Act 3 - Travincal', 'Act 3 - Bridge'],
|
||||
'Act 4 - Mesa': ['Act 4 - Mesa', 'Act 4 - Fortress', 'Act 4 - Pits', 'Act 4 - Bridge'],
|
||||
'Act 4 - Lava': ['Act 4 - Lava', 'Act 4 - Diablo'],
|
||||
'Act 4 - Lava': ['Act 4 - Lava', 'Act 4 - Diablo', 'Act 4 - Bridge'],
|
||||
'Act 5 - Siege': ['Act 5 - Siege'],
|
||||
'Act 5 - Barricade': ['Act 5 - Barricade'],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1189,6 +1189,70 @@ export const SPECIAL_PRESETS_BY_LEVEL: Readonly<Record<number, readonly string[]
|
|||
'Act 1 - Stone Fill 1',
|
||||
'Act 1 - Stone Fill 2',
|
||||
],
|
||||
41: [
|
||||
'Act 2 - Desert Tomb 1',
|
||||
'Act 2 - Desert Fill Wagon 1',
|
||||
'Act 2 - Desert Fill Bone 1',
|
||||
'Act 2 - Desert Fill Bone 2',
|
||||
'Act 2 - Desert Fill Berms 1',
|
||||
'Act 2 - Desert Fill Berms 2',
|
||||
],
|
||||
42: [
|
||||
'Act 2 - Desert Tomb 1',
|
||||
'Act 2 - Desert Ruins 16X16',
|
||||
'Act 2 - Desert Fill Mesa 1',
|
||||
'Act 2 - Desert Fill Head 1',
|
||||
'Act 2 - Desert Fill Head 2',
|
||||
],
|
||||
43: [
|
||||
'Act 2 - Desert Oasis 2',
|
||||
'Act 2 - Desert Oasis 3',
|
||||
'Act 2 - Desert Oasis 1',
|
||||
'Act 2 - Desert Lair 1',
|
||||
'Act 2 - Desert Fill Bone 1',
|
||||
'Act 2 - Desert Fill Berms 3',
|
||||
],
|
||||
44: [
|
||||
'Act 2 - Desert Ruins Sewer',
|
||||
'Act 2 - Desert Ruins Elder',
|
||||
'Act 2 - Desert Ruins 16X16',
|
||||
'Act 2 - Desert Ruins 16X08',
|
||||
'Act 2 - Desert Ruins 08X16',
|
||||
],
|
||||
45: [
|
||||
'Act 2 - Desert Tomb 2',
|
||||
'Act 2 - Desert Valley Ruin 1',
|
||||
'Act 2 - Desert Valley Ruin 2',
|
||||
],
|
||||
46: [
|
||||
'Act 2 - Desert Valley Warp',
|
||||
'Act 2 - Desert Cliff Top King Tomb',
|
||||
'Act 2 - Desert Cliff Left King Tomb',
|
||||
'Act 2 - Desert Cliff Right King Tomb',
|
||||
'Act 2 - Desert Fill Mesa 1',
|
||||
],
|
||||
79: [
|
||||
'Act 3 - Slums 16x16',
|
||||
'Act 3 - Slums 16x16',
|
||||
'Act 3 - Slums 16x08',
|
||||
'Act 3 - Slums 08x16',
|
||||
'Act 3 - Slums 08x08',
|
||||
],
|
||||
80: [
|
||||
'Act 3 - Burbs Temple',
|
||||
'Act 3 - Burbs Temple',
|
||||
'Act 3 - Burbs Sewer',
|
||||
'Act 3 - Burbs Waypoint',
|
||||
'Act 3 - Burbs 16x16',
|
||||
'Act 3 - Burbs 16x08',
|
||||
],
|
||||
81: [
|
||||
'Act 3 - MetroTemple',
|
||||
'Act 3 - MetroTemple',
|
||||
'Act 3 - Metro Sewer',
|
||||
'Act 3 - Metro 16x16',
|
||||
'Act 3 - Metro 16x08',
|
||||
],
|
||||
111: [
|
||||
'Act 5 - Barricade Waypoint Dirt',
|
||||
'Act 5 - Barricade Hell Portal N',
|
||||
|
|
@ -1217,6 +1281,15 @@ export const SPECIAL_PRESETS_BY_LEVEL: Readonly<Record<number, readonly string[]
|
|||
'Act 5 - Barricade Ruins N Treasure',
|
||||
'Act 5 - Barricade Ruins W Treasure',
|
||||
],
|
||||
117: [
|
||||
'Act 5 - Barricade To Cave 32x16 Snow',
|
||||
'Act 5 - Barricade Hell Portal N',
|
||||
'Act 5 - Barricade Waypoint Snow',
|
||||
'Act 5 - Barricade 1 Snow',
|
||||
'Act 5 - Barricade 2 Snow',
|
||||
'Act 5 - Barricade 3 Snow',
|
||||
'Act 5 - Barricade Snow Lake 1',
|
||||
],
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -1420,6 +1493,139 @@ function stampAct5SiegeStrips(
|
|||
return placed
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble the 7 cross-wing presets for Act 4 Chaos Sanctuary (LevelId 108, 120x120).
|
||||
*/
|
||||
function stampAct4ChaosSanctuary(
|
||||
canvas: Canvas,
|
||||
pieces: readonly WildernessPiece[],
|
||||
stats: WildernessStats,
|
||||
): Set<string> {
|
||||
const placements: Array<{ name: string; x: number; y: number }> = [
|
||||
{ name: 'Act 4 - Diablo Arm N', x: 48, y: 0 },
|
||||
{ name: 'Act 4 - Diablo Arm W', x: 24, y: 24 },
|
||||
{ name: 'Act 4 - Diablo Heart', x: 48, y: 24 },
|
||||
{ name: 'Act 4 - Diablo Arm E', x: 72, y: 24 },
|
||||
{ name: 'Act 4 - Diablo Arm S', x: 48, y: 48 },
|
||||
{ name: 'Act 4 - Diablo Entry', x: 48, y: 72 },
|
||||
{ name: 'Act 4 - Bridge 2', x: 48, y: 96 },
|
||||
]
|
||||
const placed = new Set<string>()
|
||||
for (const item of placements) {
|
||||
const piece = pieces.find(p => p.name.trim() === item.name.trim() || p.name.trim().startsWith(item.name.trim()))
|
||||
const ds1 = piece?.levels[0]
|
||||
if (!ds1) continue
|
||||
stampDs1(canvas, ds1, item.x, item.y)
|
||||
placed.add(item.name)
|
||||
stats.substitutions.push({ name: item.name, role: 'object', enabled: true, clusters: 1 })
|
||||
}
|
||||
return placed
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble the 6 presets for Act 3 Travincal (LevelId 83, 64x64).
|
||||
*/
|
||||
function stampAct3Travincal(
|
||||
canvas: Canvas,
|
||||
pieces: readonly WildernessPiece[],
|
||||
stats: WildernessStats,
|
||||
): Set<string> {
|
||||
const placements: Array<{ name: string; x: number; y: number }> = [
|
||||
{ name: 'Act 3 - Travincal NW', x: 0, y: 0 },
|
||||
{ name: 'Act 3 - Travincal N', x: 16, y: 0 },
|
||||
{ name: 'Act 3 - Travincal NE', x: 48, y: 0 },
|
||||
{ name: 'Act 3 - Travincal SW', x: 0, y: 32 },
|
||||
{ name: 'Act 3 - Travincal S', x: 16, y: 32 },
|
||||
{ name: 'Act 3 - Travincal SE', x: 48, y: 32 },
|
||||
]
|
||||
const placed = new Set<string>()
|
||||
for (const item of placements) {
|
||||
const piece = pieces.find(p => p.name.trim() === item.name.trim())
|
||||
const ds1 = piece?.levels[0]
|
||||
if (!ds1) continue
|
||||
stampDs1(canvas, ds1, item.x, item.y)
|
||||
placed.add(item.name)
|
||||
stats.substitutions.push({ name: item.name, role: 'object', enabled: true, clusters: 1 })
|
||||
}
|
||||
return placed
|
||||
}
|
||||
|
||||
/**
|
||||
* Stamp the 48x16 bridge preset for Act 3 Kurast Causeway (LevelId 82).
|
||||
*/
|
||||
function stampAct3KurastCauseway(
|
||||
canvas: Canvas,
|
||||
pieces: readonly WildernessPiece[],
|
||||
stats: WildernessStats,
|
||||
): Set<string> {
|
||||
const piece = pieces.find(p => p.name === 'Act 3 - Bridge')
|
||||
const ds1 = piece?.levels[0]
|
||||
const placed = new Set<string>()
|
||||
if (ds1) {
|
||||
stampDs1(canvas, ds1, 0, 0)
|
||||
placed.add('Act 3 - Bridge')
|
||||
stats.substitutions.push({ name: 'Act 3 - Bridge', role: 'object', enabled: true, clusters: 1 })
|
||||
}
|
||||
return placed
|
||||
}
|
||||
|
||||
/**
|
||||
* Assemble the 64x192 (2x6 blocks of 32x32) river and clearing layout for Act 3 Jungles (LevelId 76, 77, 78).
|
||||
*/
|
||||
function stampAct3Jungle(
|
||||
canvas: Canvas,
|
||||
levelId: number,
|
||||
pieces: readonly WildernessPiece[],
|
||||
rng: Rng,
|
||||
stats: WildernessStats,
|
||||
): Set<string> {
|
||||
const placed = new Set<string>()
|
||||
const tailPiece = pieces.find(p => p.name === 'Act 3 - Jungle Tail')
|
||||
const headPiece = pieces.find(p => p.name === 'Act 3 - Jungle Head')
|
||||
if (tailPiece?.levels[0]) {
|
||||
stampDs1(canvas, tailPiece.levels[0], 0, 0)
|
||||
placed.add(tailPiece.name)
|
||||
stats.substitutions.push({ name: tailPiece.name, role: 'object', enabled: true, clusters: 1 })
|
||||
}
|
||||
if (headPiece?.levels[0]) {
|
||||
stampDs1(canvas, headPiece.levels[0], 0, 160)
|
||||
placed.add(headPiece.name)
|
||||
stats.substitutions.push({ name: headPiece.name, role: 'object', enabled: true, clusters: 1 })
|
||||
}
|
||||
|
||||
const clearingPrefix =
|
||||
levelId === 76
|
||||
? 'Act 3 - Clearing Webby'
|
||||
: levelId === 77
|
||||
? 'Act 3 - Clearing Boggy'
|
||||
: 'Act 3 - Clearing Pygmy'
|
||||
const clearingPieces = pieces.filter(p => p.name.startsWith(clearingPrefix) && p.levels.length > 0)
|
||||
const riverPieces = pieces.filter(
|
||||
p =>
|
||||
p.name.startsWith('Act 3 - Jungle ') &&
|
||||
p.name !== 'Act 3 - Jungle Head' &&
|
||||
p.name !== 'Act 3 - Jungle Tail' &&
|
||||
p.levels.length > 0,
|
||||
)
|
||||
|
||||
for (let row = 0; row < 4; row += 1) {
|
||||
const y = 32 + row * 32
|
||||
const riverOnLeft = (row + rng.int(0, 1)) % 2 === 0
|
||||
for (let col = 0; col < 2; col += 1) {
|
||||
const x = col * 32
|
||||
const isRiverSlot = (col === 0 && riverOnLeft) || (col === 1 && !riverOnLeft)
|
||||
const pool = isRiverSlot || clearingPieces.length === 0 ? riverPieces : clearingPieces
|
||||
if (pool.length === 0) continue
|
||||
const chosen = pool[rng.int(0, pool.length - 1)]!
|
||||
const ds1 = chosen.levels[rng.int(0, chosen.levels.length - 1)]!
|
||||
stampDs1(canvas, ds1, x, y)
|
||||
placed.add(chosen.name)
|
||||
stats.substitutions.push({ name: chosen.name, role: 'object', enabled: true, clusters: 1 })
|
||||
}
|
||||
}
|
||||
return placed
|
||||
}
|
||||
|
||||
/**
|
||||
* Lay a 2-tile-wide meandering dirt road network connecting the South entrance, East exit,
|
||||
* and interior landmarks in Act 1 wilderness levels (LevelId 2..7).
|
||||
|
|
@ -1696,10 +1902,26 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
|||
}
|
||||
stats.groundCells = fillGround(canvas, groundTile, 0, 0, canvas.width, canvas.height)
|
||||
|
||||
// Special case: Act 5 Bloody Foothills (LevelId 110 / Act 5 - Siege 1) is assembled
|
||||
// from 15 sequential 16x48 siege strips rather than perimeter borders + random scatter.
|
||||
if (request.levelId === 110 || request.levelTypeName === 'Act 5 - Siege') {
|
||||
const placedSiege = stampAct5SiegeStrips(canvas, request.pieces, stats)
|
||||
// Special case: Act 5 Bloody Foothills (110), Act 4 Chaos Sanctuary (108),
|
||||
// Act 3 Travincal (83), and Act 3 Jungles (76, 77, 78) are assembled from
|
||||
// deterministic multi-preset layouts rather than rectangular perimeter borders.
|
||||
if (
|
||||
request.levelId === 110 ||
|
||||
request.levelTypeName === 'Act 5 - Siege' ||
|
||||
request.levelId === 108 ||
|
||||
request.levelId === 83 ||
|
||||
(request.levelId >= 76 && request.levelId <= 78)
|
||||
) {
|
||||
let placedSpecial: Set<string>
|
||||
if (request.levelId === 108) {
|
||||
placedSpecial = stampAct4ChaosSanctuary(canvas, request.pieces, stats)
|
||||
} else if (request.levelId === 83) {
|
||||
placedSpecial = stampAct3Travincal(canvas, request.pieces, stats)
|
||||
} else if (request.levelId >= 76 && request.levelId <= 78) {
|
||||
placedSpecial = stampAct3Jungle(canvas, request.levelId, request.pieces, rng, stats)
|
||||
} else {
|
||||
placedSpecial = stampAct5SiegeStrips(canvas, request.pieces, stats)
|
||||
}
|
||||
let finalGroundCells = 0
|
||||
for (let y = 0; y < canvas.height; y += 1) {
|
||||
const row = canvas.cells[y]
|
||||
|
|
@ -1742,7 +1964,7 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
|||
borderBlocks: 0,
|
||||
borderUsage: {},
|
||||
substitutions: stats.substitutions,
|
||||
substitutedClusters: placedSiege.size,
|
||||
substitutedClusters: placedSpecial.size,
|
||||
objects: canvas.objects.length,
|
||||
cells: canvas.width * canvas.height,
|
||||
unresolved: stats.unresolved,
|
||||
|
|
@ -1752,11 +1974,20 @@ export function generateWilderness(request: WildernessRequest): WildernessResult
|
|||
}
|
||||
}
|
||||
|
||||
// Filter pieces for Act 5 Barricade dirt (111) vs snow (112)
|
||||
// Filter pieces for Act 3 Kurast districts (79 Slums, 80 Burbs, 81 Metro) and Act 5 dirt (111) vs snow (112, 117)
|
||||
let levelPieces = request.pieces
|
||||
if (request.levelId === 111) {
|
||||
if (request.levelId === 79) {
|
||||
const slumsOnly = request.pieces.filter(p => /slums/i.test(p.name))
|
||||
if (slumsOnly.length > 0) levelPieces = slumsOnly
|
||||
} else if (request.levelId === 80) {
|
||||
const burbsOnly = request.pieces.filter(p => /burbs/i.test(p.name))
|
||||
if (burbsOnly.length > 0) levelPieces = burbsOnly
|
||||
} else if (request.levelId === 81) {
|
||||
const metroOnly = request.pieces.filter(p => /metro/i.test(p.name))
|
||||
if (metroOnly.length > 0) levelPieces = metroOnly
|
||||
} else if (request.levelId === 111) {
|
||||
levelPieces = request.pieces.filter(p => !/snow/i.test(p.name))
|
||||
} else if (request.levelId === 112) {
|
||||
} else if (request.levelId === 112 || request.levelId === 117) {
|
||||
const snowOnly = request.pieces.filter(p => /snow/i.test(p.name) || /ruins/i.test(p.name))
|
||||
if (snowOnly.length > 0) levelPieces = snowOnly
|
||||
}
|
||||
|
|
|
|||
|
|
@ -250,4 +250,79 @@ describe("Wilderness Features & Border Rules", () => {
|
|||
expect(result.level.cells[0][0].floors[0].sequence).toBe(10)
|
||||
expect(result.level.cells[0][224].floors[0].sequence).toBe(24)
|
||||
})
|
||||
|
||||
test("Act 4 Chaos Sanctuary (Level 108) stamps 7 cross-wing presets on 120x120 canvas", () => {
|
||||
const names = [
|
||||
"Act 4 - Diablo Arm N",
|
||||
"Act 4 - Diablo Arm W",
|
||||
"Act 4 - Diablo Heart",
|
||||
"Act 4 - Diablo Arm E",
|
||||
"Act 4 - Diablo Arm S",
|
||||
"Act 4 - Diablo Entry",
|
||||
"Act 4 - Bridge 2",
|
||||
]
|
||||
const pieces: WildernessPiece[] = names.map((name, idx) => {
|
||||
const baseDs1 = makeMockDs1(25, 25, false)
|
||||
const ds1: Ds1 = {
|
||||
...baseDs1,
|
||||
objects: [{ type: 2, id: 200 + idx, x: 5, y: 5, flags: 0 }],
|
||||
}
|
||||
return { name, border: false, levels: [ds1] }
|
||||
})
|
||||
|
||||
const result = generateWilderness({
|
||||
levelId: 108,
|
||||
levelName: "Chaos Sanctuary",
|
||||
levelTypeName: "Act 4 - Lava",
|
||||
sizeX: 120,
|
||||
sizeY: 120,
|
||||
subType: -1,
|
||||
subTheme: -1,
|
||||
seed: 108,
|
||||
pieces,
|
||||
substitutions: [],
|
||||
})
|
||||
|
||||
expect(result.level.width).toBe(120)
|
||||
expect(result.level.height).toBe(120)
|
||||
expect(result.stats.substitutedClusters).toBe(7)
|
||||
expect(result.level.objects.length).toBe(7)
|
||||
})
|
||||
|
||||
test("Act 3 Travincal (Level 83) stamps all 6 temple complex pieces even with trailing spaces", () => {
|
||||
const names = [
|
||||
"Act 3 - Travincal NW",
|
||||
"Act 3 - Travincal N ", // trailing space as in LvlPrest.txt
|
||||
"Act 3 - Travincal NE",
|
||||
"Act 3 - Travincal SW",
|
||||
"Act 3 - Travincal S ", // trailing space as in LvlPrest.txt
|
||||
"Act 3 - Travincal SE",
|
||||
]
|
||||
const pieces: WildernessPiece[] = names.map((name, idx) => {
|
||||
const baseDs1 = makeMockDs1(name.includes(" N ") || name.includes(" S ") ? 33 : 17, 33, false)
|
||||
const ds1: Ds1 = {
|
||||
...baseDs1,
|
||||
objects: [{ type: 2, id: 300 + idx, x: 5, y: 5, flags: 0 }],
|
||||
}
|
||||
return { name, border: false, levels: [ds1] }
|
||||
})
|
||||
|
||||
const result = generateWilderness({
|
||||
levelId: 83,
|
||||
levelName: "Travincal",
|
||||
levelTypeName: "Act 3 - Kurast",
|
||||
sizeX: 64,
|
||||
sizeY: 64,
|
||||
subType: -1,
|
||||
subTheme: -1,
|
||||
seed: 83,
|
||||
pieces,
|
||||
substitutions: [],
|
||||
})
|
||||
|
||||
expect(result.level.width).toBe(64)
|
||||
expect(result.level.height).toBe(64)
|
||||
expect(result.stats.substitutedClusters).toBe(6)
|
||||
expect(result.level.objects.length).toBe(6)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue