277 lines
10 KiB
TypeScript
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()
|