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 { 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() async function loadDs1(relative: string): Promise { 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 { 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()