diablo2-web/scripts/render-new-ts-blood-moor.ts

277 lines
10 KiB
TypeScript

import { readFileSync, writeFileSync, mkdirSync } from 'node:fs'
import { join } from 'node:path'
import { MpqArchive } from '../src/mpq/archive.ts'
import { fileSource } from '../src/mpq/file-source.ts'
import { MountedArchives } from '../src/mpq/mount.ts'
import { decodeDs1, type Ds1 } from '../src/formats/ds1.ts'
import { decodeDt1, type Dt1 } from '../src/formats/dt1.ts'
import { decodePal } from '../src/formats/pal.ts'
import { loadActTables, parseTable, cell, tileMemberPath, type D2Table } from '../src/game/acts.ts'
import { buildIsoMapScene, COLLIDE_WALL, COLLIDE_DOOR } from '../src/game/d2map.ts'
import { encodeIndexedPng } from './png.ts'
import {
generateWilderness,
type WildernessPiece,
type WildernessSubstitution,
type WildernessResult,
} from '../src/game/wilderness.ts'
const d2DataDir = '/usr/local/google/home/taodao/d2-data'
const artifactDir = '/usr/local/google/home/taodao/.gemini/jetski/brain/a4f9163c-a011-4161-b169-7f361b1ed57e'
mkdirSync(artifactDir, { recursive: true })
async function main() {
const mpqNames = ['Patch_D2.mpq', 'd2exp.mpq', 'd2data.mpq']
const archives = new MountedArchives()
const rawMpqs: MpqArchive[] = []
for (const name of mpqNames) {
const archive = await MpqArchive.open(await fileSource(join(d2DataDir, name)))
archives.add(name, archive)
rawMpqs.push(archive)
}
async function readMpqFile(mpqPath: string): Promise<Uint8Array | null> {
const norm = mpqPath.replace(/\//g, '\\')
for (const mpq of rawMpqs) {
const entry = mpq.find(norm)
if (entry !== undefined) {
return await mpq.read(entry)
}
}
return null
}
const actTables = await loadActTables(archives)
const lvlsubTable = parseTable(await archives.read('data\\global\\excel\\LvlSub.txt'))
const ds1Cache = new Map<string, Ds1>()
async function loadDs1(relative: string): Promise<Ds1> {
const member = tileMemberPath(relative)
const cached = ds1Cache.get(member)
if (cached !== undefined) return cached
const decoded = decodeDs1(await archives.read(member))
ds1Cache.set(member, decoded)
return decoded
}
async function rowDs1s(table: D2Table, row: readonly string[]): Promise<Ds1[]> {
const levels: Ds1[] = []
for (let slot = 1; slot <= 6; slot += 1) {
const val = cell(table, row, `File${String(slot)}`)
if (val === '' || val === '0') continue
levels.push(await loadDs1(val))
}
return levels
}
const WILDERNESS_PIECE_FAMILIES = [
'Act 1 - Wild', 'Act 1 - Town 1 Transition', 'Act 1 - Cave Entrance',
'Act 1 - DOE Entrance', 'Act 1 - Corral Fill', 'Act 1 - Fence Fill',
'Act 1 - River', 'Act 1 - Bridge', 'Act 1 - Bivouac', 'Act 1 - Pond',
'Act 1 - Swamp Fill', 'Act 1 - Stone Fill', 'Act 1 - Cottages',
'Act 1 - Fallen Camp', 'Act 1 - Camp', 'Act 1 - Cairn Stones',
'Act 1 - Inifus', 'Act 1 - Tower', 'Act 1 - Ruin', 'Act 1 - Tree Fill',
'Act 1 - Graveyard',
]
const act1Pieces: WildernessPiece[] = []
for (const row of actTables.lvlprest.rows) {
const name = cell(actTables.lvlprest, row, 'Name')
if (!WILDERNESS_PIECE_FAMILIES.some(fam => name.startsWith(fam))) continue
const levels = await rowDs1s(actTables.lvlprest, row)
if (levels.length === 0) continue
act1Pieces.push({ name, levels, border: /\bBorder\b/i.test(name) })
}
const act1Subs: WildernessSubstitution[] = []
for (const row of lvlsubTable.rows) {
const subType = Number(cell(lvlsubTable, row, 'Type'))
if (subType !== 0 && subType !== 1 && subType !== 6) continue
const file = cell(lvlsubTable, row, 'File')
if (file === '' || file === '0') continue
const levels = [await loadDs1(file)]
act1Subs.push({
name: cell(lvlsubTable, row, 'Name'),
type: subType,
gridSize: Number(cell(lvlsubTable, row, 'GridSize')) || 1,
bordType: Number(cell(lvlsubTable, row, 'BordType')),
dt1Mask: Number(cell(lvlsubTable, row, 'Dt1Mask')) || 0,
prob: [0, 1, 2, 3, 4].map(i => Number(cell(lvlsubTable, row, `Prob${String(i)}`)) || 0),
trials: [0, 1, 2, 3, 4].map(i => Number(cell(lvlsubTable, row, `Trials${String(i)}`)) || 0),
max: [0, 1, 2, 3, 4].map(i => Number(cell(lvlsubTable, row, `Max${String(i)}`)) || 0),
levels,
})
}
// Load DT1 libraries for Act 1 Wilderness (LevelType = 2) + 3 universal DT1s
const dt1Libraries: Dt1[] = []
const universalFiles = [
'data\\global\\tiles\\act1\\outdoors\\blank.dt1',
'data\\global\\tiles\\act1\\outdoors\\inviswal.dt1',
'data\\global\\tiles\\act1\\outdoors\\warp.dt1',
]
for (const uf of universalFiles) {
const bytes = await readMpqFile(uf)
if (bytes) dt1Libraries.push(decodeDt1(bytes))
}
const lvlTypesBytes = await readMpqFile('data\\global\\excel\\LvlTypes.txt')
const lvlTypesLines = new TextDecoder().decode(lvlTypesBytes!).split(/\r?\n/)
const lvlTypesHeader = lvlTypesLines[0]!.split('\t')
const lvlTypeIdIdx = lvlTypesHeader.indexOf('Id')
const fileCols = Array.from({ length: 32 }, (_, i) => lvlTypesHeader.indexOf(`File ${i + 1}`))
const typeRow = lvlTypesLines.slice(1).map(l => l.split('\t')).find(c => Number(c[lvlTypeIdIdx]) === 2)
if (typeRow) {
for (let i = 0; i < 32; i++) {
const colIdx = fileCols[i]!
const fName = (typeRow[colIdx] ?? '').trim()
if (!fName || fName === '0') continue
const bytes = await readMpqFile(`data\\global\\tiles\\${fName}`)
if (bytes) dt1Libraries.push(decodeDt1(bytes))
}
}
const seed = 0x12345678
console.log(`Generating Blood Moor with new TypeScript wilderness generator (Seed: 0x${seed.toString(16)})...`)
const result: WildernessResult = generateWilderness({
levelId: 2,
levelName: 'Blood Moor',
levelTypeName: 'Act 1 - Wilderness',
sizeX: 80,
sizeY: 80,
subType: 0,
subTheme: 0,
seed,
pieces: act1Pieces,
substitutions: act1Subs,
dt1Libraries,
})
const ds1 = result.level
console.log(`Generated Level DS1: ${ds1.width}x${ds1.height} tiles, ${ds1.objects.length} objects`)
const scene = buildIsoMapScene(ds1, dt1Libraries, seed)
console.log(`Scene built: ${scene.gridWidth}x${scene.gridHeight} subtiles, ${scene.widthPx}x${scene.heightPx} px, ${scene.frames.length} frames`)
const palBytes = await readMpqFile('data\\global\\palette\\act1\\pal.dat')
const palette = decodePal(palBytes!)
// 1) Render High-Res Blueprint
const scale = 3
const mapW = scene.gridWidth * scale
const mapH = scene.gridHeight * scale
const mapPixels = new Uint8Array(mapW * mapH)
const mapPal = new Uint8Array(256 * 3)
const setColor = (idx: number, r: number, g: number, b: number) => {
mapPal[idx * 3] = r
mapPal[idx * 3 + 1] = g
mapPal[idx * 3 + 2] = b
}
setColor(1, 12, 16, 22) // Void
setColor(2, 42, 68, 46) // Outdoor 8x8 Grid Line
setColor(3, 36, 78, 44) // Walkable Grass Moorland
setColor(4, 198, 158, 86) // Solid Wall / Cliff / Fence / River Bank
setColor(5, 176, 132, 78) // Dirt Road Path
setColor(6, 240, 84, 84) // Preset Landmark Outline
setColor(7, 88, 166, 255) // River Boundary
setColor(8, 255, 230, 100) // DS1 Object
mapPixels.fill(1)
for (let sy = 0; sy < scene.gridHeight; sy++) {
for (let sx = 0; sx < scene.gridWidth; sx++) {
const cellX = Math.floor(sx / 5)
const cellY = Math.floor(sy / 5)
const c = ds1.cells[cellY]?.[cellX]
const hasFloor = c && c.floors.some(f => f.prop1 !== 0)
const hasWall = c && c.walls.some(w => w.prop1 !== 0)
if (!hasFloor && !hasWall) continue
const mask = scene.collisionMasks[sy * scene.gridWidth + sx]!
let color = 3
const isRoad = c?.floors.some(f => f.style === 1 || f.style === 30 || (f.style === 0 && f.sequence > 0))
if (isRoad) color = 5
if ((mask & COLLIDE_WALL) !== 0) color = 4
for (let dy = 0; dy < scale; dy++) {
for (let dx = 0; dx < scale; dx++) {
mapPixels[(sy * scale + dy) * mapW + (sx * scale + dx)] = color
}
}
}
}
// Draw 8x8 Room Grid Lines
for (let by = 0; by <= ds1.height; by += 8) {
const py = by * 5 * scale
if (py >= 0 && py < mapH) {
for (let px = 0; px < mapW; px++) {
mapPixels[py * mapW + px] = 2
}
}
}
for (let bx = 0; bx <= ds1.width; bx += 8) {
const px = bx * 5 * scale
if (px >= 0 && px < mapW) {
for (let py = 0; py < mapH; py++) {
mapPixels[py * mapW + px] = 2
}
}
}
// Draw Objects
for (const obj of ds1.objects) {
const ox = obj.x * scale
const oy = obj.y * scale
for (let dy = -2; dy <= 2; dy++) {
for (let dx = -2; dx <= 2; dx++) {
const px = ox + dx
const py = oy + dy
if (px >= 0 && px < mapW && py >= 0 && py < mapH) {
mapPixels[py * mapW + px] = 8
}
}
}
}
const blueprintOut = join(artifactDir, 'new_ts_blood_moor_blueprint.png')
writeFileSync(blueprintOut, encodeIndexedPng({ width: mapW, height: mapH, pixels: mapPixels, palette: mapPal }))
console.log(`Saved Blueprint: ${blueprintOut}`)
// 2) Render Full Isometric DT1 Scene
const isoScale = scene.widthPx > 4000 ? 2 : 1
const isoW = Math.ceil(scene.widthPx / isoScale)
const isoH = Math.ceil(scene.heightPx / isoScale)
const isoPixels = new Uint8Array(isoW * isoH)
const drawPass = (draws: typeof scene.floors) => {
for (const d of draws) {
const frame = scene.frames[d.frameIndex]
if (!frame) continue
for (let fy = 0; fy < frame.height; fy += isoScale) {
const py = Math.floor((d.y + fy) / isoScale)
if (py < 0 || py >= isoH) continue
for (let fx = 0; fx < frame.width; fx += isoScale) {
const px = Math.floor((d.x + fx) / isoScale)
if (px < 0 || px >= isoW) continue
const idx = frame.indices[fy * frame.width + fx]!
if (idx !== 0) isoPixels[py * isoW + px] = idx
}
}
}
}
drawPass(scene.floors)
drawPass(scene.shadows)
drawPass(scene.walls)
drawPass(scene.roofs)
const isoOut = join(artifactDir, 'new_ts_blood_moor_iso.png')
writeFileSync(isoOut, encodeIndexedPng({ width: isoW, height: isoH, pixels: isoPixels, palette: palette.rgb }))
console.log(`Saved Isometric Render: ${isoOut}`)
console.log(`Dimensions: ${isoW}x${isoH} px (scale 1/${isoScale})`)
}
void main()