678 lines
22 KiB
TypeScript
678 lines
22 KiB
TypeScript
/**
|
|
* Offline Missile Asset & Data Baking Script.
|
|
*
|
|
* Extracts authentic Diablo II v1.13c missile DCC sprites from MPQ archives:
|
|
* - Fire: Firebolt, Fireball, FireArrowExplode2
|
|
* - Cold: Icebolt, IceBlast, GlacialSpike, IceArrowExplode, FreezeExplodeCenter
|
|
* - Lightning: ChargedBolt
|
|
* - Necromancer: teethMissile, teethexplode, BoneSpear, BoneSpirit, BoneSpiritExplode, poisonNova
|
|
* - Paladin: HolyBoltMissile, blessedhammer
|
|
* - Amazon: SafeArrow, FireArrow, IceArrow, Arrow, LightningJavelin
|
|
*
|
|
* Bakes them into indexed-color PNG atlases and JSON metadata under:
|
|
* - public/missiles/
|
|
* - samples/d2-packs/missiles/
|
|
*
|
|
* And generates typed metadata in src/render/missiles-meta.ts.
|
|
* Enforces Zero Runtime MPQ/DLL Architectural Invariant.
|
|
*/
|
|
|
|
import { mkdirSync, writeFileSync, readFileSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { MpqArchive } from '../src/mpq/archive.ts'
|
|
import { fileSource } from '../src/mpq/file-source.ts'
|
|
import { decodeDcc } from '../src/formats/dcc.ts'
|
|
import { decodePl2 } from '../src/formats/pl2.ts'
|
|
import { encodeRgbaPng } from './png.ts'
|
|
import type { SpriteFrame, SpriteSheet } from '../src/formats/sprite.ts'
|
|
|
|
export interface MissileFrameMeta {
|
|
readonly x: number
|
|
readonly y: number
|
|
readonly w: number
|
|
readonly h: number
|
|
readonly anchorX: number
|
|
readonly anchorY: number
|
|
}
|
|
|
|
export type MissileFrameTuple = readonly [
|
|
x: number,
|
|
y: number,
|
|
width: number,
|
|
height: number,
|
|
anchorX: number,
|
|
anchorY: number,
|
|
]
|
|
|
|
export interface MissileMeta {
|
|
readonly name: string
|
|
readonly celFile: string
|
|
readonly width: number
|
|
readonly height: number
|
|
readonly directions: number
|
|
readonly framesPerDirection: number
|
|
readonly animSpeed: number
|
|
readonly groups: readonly (readonly MissileFrameTuple[])[]
|
|
}
|
|
|
|
const MISSILE_TARGETS = [
|
|
// Fire spells & explosions
|
|
{ name: 'firebolt', celFile: 'Firebolt', animSpeed: 16 },
|
|
{ name: 'fireball', celFile: 'Fireball', animSpeed: 16 },
|
|
{ name: 'fireexplode', celFile: 'FireArrowExplode2', animSpeed: 16 },
|
|
|
|
// Ice spells & explosions
|
|
{ name: 'icebolt', celFile: 'Icebolt', animSpeed: 16 },
|
|
{ name: 'iceblast', celFile: 'IceBlast', animSpeed: 16 },
|
|
{ name: 'glacialspike', celFile: 'GlacialSpike', animSpeed: 16 },
|
|
{ name: 'iceexplode', celFile: 'IceArrowExplode', animSpeed: 16 },
|
|
{ name: 'freezeexplode', celFile: 'FreezeExplodeCenter', animSpeed: 16 },
|
|
{ name: 'frozenorb', celFile: 'IceOrb', animSpeed: 16 },
|
|
{ name: 'frozenorbexplode', celFile: 'IceOrbExplode', animSpeed: 16 },
|
|
{ name: 'blizzard1', celFile: 'icestormfallvar01', animSpeed: 16 },
|
|
{ name: 'blizzard2', celFile: 'icestormfallvar02', animSpeed: 16 },
|
|
{ name: 'blizzard3', celFile: 'Blizzard', animSpeed: 16 },
|
|
{ name: 'blizzardexplode1', celFile: 'icestormimpactvar01', animSpeed: 16 },
|
|
{ name: 'blizzardexplode2', celFile: 'icestormimpactvar02', animSpeed: 16 },
|
|
|
|
// Lightning
|
|
{ name: 'chargedbolt', celFile: 'ChargedBolt', animSpeed: 16 },
|
|
|
|
// Necromancer
|
|
{ name: 'teeth', celFile: 'teethMissile', animSpeed: 16 },
|
|
{ name: 'teethexplode', celFile: 'teethexplode', animSpeed: 16 },
|
|
{ name: 'bonespear', celFile: 'BoneSpear', animSpeed: 16 },
|
|
{ name: 'bonespirit', celFile: 'BoneSpirit', animSpeed: 16 },
|
|
{ name: 'bonespiritexplode', celFile: 'BoneSpiritExplode', animSpeed: 16 },
|
|
{ name: 'poisonnova', celFile: 'poisonNova', animSpeed: 16 },
|
|
|
|
// Paladin
|
|
{ name: 'holybolt', celFile: 'HolyBoltMissile', animSpeed: 16 },
|
|
{ name: 'blessedhammer', celFile: 'blessedhammer', animSpeed: 16 },
|
|
|
|
// Amazon
|
|
{ name: 'magicarrow', celFile: 'SafeArrow', animSpeed: 16 },
|
|
{ name: 'firearrow', celFile: 'FireArrow', animSpeed: 16 },
|
|
{ name: 'icearrow', celFile: 'IceArrow', animSpeed: 16 },
|
|
{ name: 'arrow', celFile: 'Arrow', animSpeed: 16, trans: 0 },
|
|
{ name: 'lightningjavelin', celFile: 'LightningJavelin', animSpeed: 16 },
|
|
|
|
// Meteor (Skill 56)
|
|
{ name: 'meteor', celFile: 'Meteor', animSpeed: 16 },
|
|
{ name: 'meteortail', celFile: 'MeteorTail', animSpeed: 16 },
|
|
{ name: 'meteorcenter', celFile: 'MeteorTargetIndicator', animSpeed: 16 },
|
|
{ name: 'meteorexplode', celFile: 'ExpArrowExplode', animSpeed: 16 },
|
|
{ name: 'meteorfire', celFile: 'groundFireMedium', animSpeed: 16 },
|
|
{ name: 'firesmall', celFile: 'groundFireSmall', animSpeed: 16 },
|
|
|
|
// 18 Sorceress Skills Missiles (v1.13c Parity)
|
|
{ name: 'nova', celFile: 'ElectricNova', animSpeed: 16, trans: 1 },
|
|
{ name: 'lightningstrike', celFile: 'LightningStrike', animSpeed: 16, trans: 1 },
|
|
{ name: 'thunderstorm1', celFile: 'LightningboltBig', animSpeed: 16, trans: 1 },
|
|
{ name: 'sparkle', celFile: 'Gleam', animSpeed: 16, trans: 1 },
|
|
{ name: 'freezingarrowexp2', celFile: 'FreezeExplodeEjecta', animSpeed: 16, trans: 1 },
|
|
{ name: 'firewall', celFile: 'groundFireBig', animSpeed: 16, trans: 1 },
|
|
{ name: 'infernoflame1', celFile: 'Flamethrower', animSpeed: 16, trans: 1 },
|
|
{ name: 'infernoflame2', celFile: 'Flamethrower2', animSpeed: 16, trans: 1 },
|
|
{ name: 'frostnova', celFile: 'FrostNova', animSpeed: 16, trans: 1 },
|
|
] as const
|
|
|
|
const PAGE_SIZE = 4096
|
|
|
|
/**
|
|
* Shelf-pack all frames of a missile SpriteSheet into a 32-bit RGBA PNG buffer.
|
|
* Enforces Diablo II 1.13c Trans=1 transparency:
|
|
* - Pure black / background antialiasing fringe pixels (max(r,g,b) <= 4 or idx 172) are strictly transparent (alpha=0).
|
|
* - Semi-dark edge pixels ramp smoothly so no black borders or dark smudge boxes appear over the ground.
|
|
* - Core pixels remain solid (alpha=255) for vibrant spell rendering.
|
|
*/
|
|
export function packMissileSheetToRgbaPng(
|
|
sheet: SpriteSheet,
|
|
palette: Uint8Array,
|
|
trans: number = 1,
|
|
): {
|
|
png: Uint8Array
|
|
width: number
|
|
height: number
|
|
groups: [number, number, number, number, number, number][][]
|
|
} {
|
|
const frames = sheet.groups.flatMap(g => g.frames)
|
|
const widest = Math.max(1, ...frames.map(f => f.width))
|
|
const area = frames.reduce((sum, f) => sum + (f.width + 1) * (f.height + 1), 0)
|
|
let pageWidth = Math.min(PAGE_SIZE, Math.max(widest + 2, 512))
|
|
while (pageWidth < PAGE_SIZE && pageWidth * pageWidth < area * 1.15) {
|
|
pageWidth *= 2
|
|
}
|
|
pageWidth = Math.min(PAGE_SIZE, pageWidth)
|
|
|
|
let cursorX = 0
|
|
let shelfY = 0
|
|
let shelfHeight = 0
|
|
const placements: { x: number; y: number; width: number; height: number }[] = []
|
|
|
|
for (const frame of frames) {
|
|
const w = Math.max(1, Math.min(frame.width, pageWidth))
|
|
const h = Math.max(1, Math.min(frame.height, PAGE_SIZE))
|
|
if (cursorX + w > pageWidth) {
|
|
shelfY += shelfHeight + 1
|
|
shelfHeight = 0
|
|
cursorX = 0
|
|
}
|
|
placements.push({ x: cursorX, y: shelfY, width: w, height: h })
|
|
cursorX += w + 1
|
|
shelfHeight = Math.max(shelfHeight, h)
|
|
}
|
|
|
|
const pageHeight = Math.max(1, shelfY + shelfHeight)
|
|
const rgba = new Uint8Array(pageWidth * pageHeight * 4)
|
|
|
|
let idx = 0
|
|
for (const frame of frames) {
|
|
const place = placements[idx]!
|
|
idx += 1
|
|
for (let row = 0; row < place.height; row += 1) {
|
|
const fromRow = row * frame.width
|
|
const toRow = (place.y + row) * pageWidth + place.x
|
|
for (let col = 0; col < place.width; col += 1) {
|
|
const srcAt = fromRow + col
|
|
const dst = (toRow + col) * 4
|
|
if (frame.mask[srcAt] === 0) {
|
|
rgba[dst] = 0
|
|
rgba[dst + 1] = 0
|
|
rgba[dst + 2] = 0
|
|
rgba[dst + 3] = 0
|
|
} else {
|
|
const val = frame.indices[srcAt]!
|
|
if (val === 0) {
|
|
rgba[dst] = 0
|
|
rgba[dst + 1] = 0
|
|
rgba[dst + 2] = 0
|
|
rgba[dst + 3] = 0
|
|
} else {
|
|
const r = palette[val * 3]!
|
|
const g = palette[val * 3 + 1]!
|
|
const b = palette[val * 3 + 2]!
|
|
if (trans === 1) {
|
|
const m = Math.max(r, g, b)
|
|
if (m <= 4) {
|
|
rgba[dst] = 0
|
|
rgba[dst + 1] = 0
|
|
rgba[dst + 2] = 0
|
|
rgba[dst + 3] = 0
|
|
} else {
|
|
const alpha = Math.min(255, Math.round(((m - 4) / (60 - 4)) * 255))
|
|
rgba[dst] = r
|
|
rgba[dst + 1] = g
|
|
rgba[dst + 2] = b
|
|
rgba[dst + 3] = alpha
|
|
}
|
|
} else {
|
|
rgba[dst] = r
|
|
rgba[dst + 1] = g
|
|
rgba[dst + 2] = b
|
|
rgba[dst + 3] = 255
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const groups: [number, number, number, number, number, number][][] = []
|
|
let consumed = 0
|
|
for (const group of sheet.groups) {
|
|
const gList: [number, number, number, number, number, number][] = []
|
|
for (let i = 0; i < group.frames.length; i += 1) {
|
|
const p = placements[consumed]!
|
|
const f = group.frames[i]!
|
|
consumed += 1
|
|
gList.push([p.x, p.y, p.width, p.height, f.anchorX ?? 0, f.anchorY ?? 0])
|
|
}
|
|
groups.push(gList)
|
|
}
|
|
|
|
const png = encodeRgbaPng({ width: pageWidth, height: pageHeight, pixels: rgba })
|
|
return { png, width: pageWidth, height: pageHeight, groups }
|
|
}
|
|
|
|
function formatMetaTs(meta: MissileMeta): string {
|
|
const groupsStr = meta.groups
|
|
.map(
|
|
group =>
|
|
` [\n${group.map(f => ` [${f.join(', ')}]`).join(',\n')}\n ]`,
|
|
)
|
|
.join(',\n')
|
|
|
|
return `{\n name: ${JSON.stringify(meta.name)},\n celFile: ${JSON.stringify(meta.celFile)},\n width: ${meta.width},\n height: ${meta.height},\n directions: ${meta.directions},\n framesPerDirection: ${meta.framesPerDirection},\n animSpeed: ${meta.animSpeed},\n groups: [\n${groupsStr}\n ],\n}`
|
|
}
|
|
|
|
export async function bakeMissiles(projectRoot: string = process.cwd()): Promise<Record<string, MissileMeta>> {
|
|
const archivePaths = [
|
|
join(projectRoot, 'samples', 'd2', 'Patch_D2.mpq'),
|
|
join(projectRoot, 'samples', 'd2', 'd2exp.mpq'),
|
|
join(projectRoot, 'samples', 'd2', 'd2data.mpq'),
|
|
'/usr/local/google/home/taodao/d2-data/Patch_D2.mpq',
|
|
'/usr/local/google/home/taodao/d2-data/d2exp.mpq',
|
|
'/usr/local/google/home/taodao/d2-data/d2data.mpq',
|
|
]
|
|
const archives: MpqArchive[] = []
|
|
for (const p of archivePaths) {
|
|
try {
|
|
archives.push(await MpqArchive.open(await fileSource(p)))
|
|
} catch {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
async function readMpqFile(path: string): Promise<Uint8Array | null> {
|
|
for (const a of archives) {
|
|
const f = a.find(path)
|
|
if (f) return a.read(f)
|
|
}
|
|
return null
|
|
}
|
|
|
|
const palBytes = await readMpqFile('data/global/palette/ACT1/pal.pl2')
|
|
if (!palBytes) throw new Error('data/global/palette/ACT1/pal.pl2 not found in MPQ archives')
|
|
const pl2 = decodePl2(palBytes)
|
|
|
|
const publicOutDir = join(projectRoot, 'public', 'missiles')
|
|
const packOutDir = join(projectRoot, 'samples', 'd2-packs', 'missiles')
|
|
mkdirSync(publicOutDir, { recursive: true })
|
|
mkdirSync(packOutDir, { recursive: true })
|
|
|
|
const metaRecord: Record<string, MissileMeta> = {}
|
|
|
|
for (const target of MISSILE_TARGETS) {
|
|
const raw = await readMpqFile(`data/global/missiles/${target.celFile}.dcc`)
|
|
if (!raw) throw new Error(`Missile DCC not found: data/global/missiles/${target.celFile}.dcc`)
|
|
|
|
const dcc = decodeDcc(raw)
|
|
const groups = dcc.directions.map(dir => ({
|
|
frames: dir.frames.map(
|
|
(frame): SpriteFrame => ({
|
|
width: frame.frame.width,
|
|
height: frame.frame.height,
|
|
indices: frame.frame.indices,
|
|
mask: frame.frame.mask,
|
|
anchorX: dir.box.left,
|
|
anchorY: dir.box.top,
|
|
}),
|
|
),
|
|
}))
|
|
|
|
const sheet: SpriteSheet = { groups, width: null }
|
|
const trans = 'trans' in target ? (target as any).trans : 1
|
|
const packed = packMissileSheetToRgbaPng(sheet, pl2.rgb, trans)
|
|
|
|
const meta: MissileMeta = {
|
|
name: target.name,
|
|
celFile: target.celFile,
|
|
width: packed.width,
|
|
height: packed.height,
|
|
directions: dcc.directions.length,
|
|
framesPerDirection: dcc.directions[0]?.frames.length ?? 0,
|
|
animSpeed: target.animSpeed,
|
|
groups: packed.groups,
|
|
}
|
|
|
|
metaRecord[target.name] = meta
|
|
|
|
// Write PNG and JSON to public/missiles/ and samples/d2-packs/missiles/
|
|
const jsonStr = JSON.stringify(meta, null, 2)
|
|
writeFileSync(join(publicOutDir, `${target.name}.png`), packed.png)
|
|
writeFileSync(join(publicOutDir, `${target.name}.json`), jsonStr, 'utf-8')
|
|
writeFileSync(join(packOutDir, `${target.name}.png`), packed.png)
|
|
writeFileSync(join(packOutDir, `${target.name}.json`), jsonStr, 'utf-8')
|
|
|
|
console.log(`[Missile Packer] Baked ${target.name} (${target.celFile}.dcc): ${packed.width}x${packed.height}, ${packed.png.byteLength} bytes PNG`)
|
|
}
|
|
|
|
// Aliases for blizzard4 (from blizzard3) and blizzardexplode3 (from blizzardexplode2)
|
|
const copyAlias = (aliasName: string, sourceName: string) => {
|
|
const srcMeta = metaRecord[sourceName]
|
|
if (!srcMeta) return
|
|
const aliasMeta: MissileMeta = { ...srcMeta, name: aliasName }
|
|
const jsonStr = JSON.stringify(aliasMeta, null, 2)
|
|
writeFileSync(join(publicOutDir, `${aliasName}.json`), jsonStr, 'utf-8')
|
|
writeFileSync(join(packOutDir, `${aliasName}.json`), jsonStr, 'utf-8')
|
|
const srcPngBytes = readFileSync(join(publicOutDir, `${sourceName}.png`))
|
|
writeFileSync(join(publicOutDir, `${aliasName}.png`), srcPngBytes)
|
|
writeFileSync(join(packOutDir, `${aliasName}.png`), srcPngBytes)
|
|
console.log(`[Missile Packer] Aliased ${aliasName} -> ${sourceName}`)
|
|
}
|
|
copyAlias('blizzard4', 'blizzard3')
|
|
copyAlias('blizzardexplode3', 'blizzardexplode2')
|
|
copyAlias('meteortargetindicator', 'meteorcenter')
|
|
copyAlias('firemedium', 'meteorfire')
|
|
|
|
// 18 Sorceress Skills Missiles aliases
|
|
copyAlias('electricnova', 'nova')
|
|
copyAlias('chainlightning', 'lightningstrike')
|
|
copyAlias('lightninghit', 'lightningstrike')
|
|
copyAlias('thunderstorm', 'thunderstorm1')
|
|
copyAlias('lightningboltbig', 'thunderstorm1')
|
|
copyAlias('gleam', 'sparkle')
|
|
copyAlias('freezeexplodeejecta', 'freezingarrowexp2')
|
|
copyAlias('glacialspikeejecta', 'freezingarrowexp2')
|
|
copyAlias('blaze', 'firewall')
|
|
copyAlias('firewallmaker', 'firewall')
|
|
copyAlias('groundfirebig', 'firewall')
|
|
copyAlias('flamethrower', 'infernoflame1')
|
|
copyAlias('flamethrower2', 'infernoflame2')
|
|
copyAlias('coldunique', 'frostnova')
|
|
copyAlias('mephfrostnova', 'frostnova')
|
|
|
|
// Generate src/render/missiles-meta.ts
|
|
const metaExports = MISSILE_TARGETS.map(t => {
|
|
const upper = t.name.toUpperCase()
|
|
return `export const ${upper}_META: MissileMeta = ${formatMetaTs(metaRecord[t.name]!)}`
|
|
}).join('\n\n')
|
|
|
|
const mapEntries = MISSILE_TARGETS.map(t => {
|
|
return ` '${t.name}': ${t.name.toUpperCase()}_META,`
|
|
}).join('\n')
|
|
|
|
const metaTsContent = `/**
|
|
* Authentic Diablo II v1.13c Missile Atlas Metadata.
|
|
* Auto-generated by scripts/pack-missiles.ts.
|
|
* Enforces Zero Runtime MPQ/DLL Architectural Invariant.
|
|
*/
|
|
|
|
export interface MissileFrameMeta {
|
|
readonly x: number
|
|
readonly y: number
|
|
readonly w: number
|
|
readonly h: number
|
|
readonly anchorX: number
|
|
readonly anchorY: number
|
|
}
|
|
|
|
export type MissileFrameTuple = readonly [
|
|
x: number,
|
|
y: number,
|
|
width: number,
|
|
height: number,
|
|
anchorX: number,
|
|
anchorY: number,
|
|
]
|
|
|
|
export interface MissileMeta {
|
|
readonly name: string
|
|
readonly celFile: string
|
|
readonly width: number
|
|
readonly height: number
|
|
readonly directions: number
|
|
readonly framesPerDirection: number
|
|
readonly animSpeed: number
|
|
readonly groups: readonly (readonly MissileFrameTuple[])[]
|
|
}
|
|
|
|
/**
|
|
* The engine's 64-direction space -> DCC direction tables.
|
|
* Ported from OpenDiablo2 d2fileformats/d2dcc/dcc_dir_lookup.go (Dir64ToDcc).
|
|
*/
|
|
const DIR64_TO_DCC: Readonly<Record<number, readonly number[]>> = {
|
|
4: [
|
|
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1,
|
|
1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2,
|
|
2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,
|
|
3, 3, 3, 3, 3, 3, 3, 3, 0, 0, 0, 0, 0, 0, 0, 0,
|
|
],
|
|
8: [
|
|
4, 4, 4, 4, 0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5,
|
|
5, 5, 5, 5, 1, 1, 1, 1, 1, 1, 1, 1, 6, 6, 6, 6,
|
|
6, 6, 6, 6, 2, 2, 2, 2, 2, 2, 2, 2, 7, 7, 7, 7,
|
|
7, 7, 7, 7, 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4,
|
|
],
|
|
16: [
|
|
4, 4, 8, 8, 8, 8, 0, 0, 0, 0, 9, 9, 9, 9, 5, 5,
|
|
5, 5, 10, 10, 10, 10, 1, 1, 1, 1, 11, 11, 11, 11, 6, 6,
|
|
6, 6, 12, 12, 12, 12, 2, 2, 2, 2, 13, 13, 13, 13, 7, 7,
|
|
7, 7, 14, 14, 14, 14, 3, 3, 3, 3, 15, 15, 15, 15, 4, 4,
|
|
],
|
|
32: [
|
|
4, 16, 16, 8, 8, 17, 17, 0, 0, 18, 18, 9, 9, 19, 19, 5,
|
|
5, 20, 20, 10, 10, 21, 21, 1, 1, 22, 22, 11, 11, 23, 23, 6,
|
|
6, 24, 24, 12, 12, 25, 25, 2, 2, 26, 26, 13, 13, 27, 27, 7,
|
|
7, 28, 28, 14, 14, 29, 29, 3, 3, 30, 30, 15, 15, 31, 31, 4,
|
|
],
|
|
}
|
|
|
|
/**
|
|
* Calculate DCC direction index for a missile based on 2D velocity vector.
|
|
*
|
|
* In Diablo II's 64-direction space:
|
|
* - South (0, +y) is dir 0
|
|
* - West (-x, 0) is dir 16
|
|
* - North (0, -y) is dir 32
|
|
* - East (+x, 0) is dir 48
|
|
*/
|
|
export function velocityToDccDirection(vx: number, vy: number, directions = 16): number {
|
|
if (directions <= 1) return 0
|
|
const angle = Math.atan2(vy, vx) // -PI to PI
|
|
// Map angle so that South (PI/2) is 0, turning clockwise
|
|
const fraction = (angle - Math.PI / 2) / (2 * Math.PI)
|
|
const dir64 = Math.round((((fraction % 1) + 1) % 1) * 64) % 64
|
|
const table = DIR64_TO_DCC[directions]
|
|
if (table !== undefined) {
|
|
return table[dir64] ?? 0
|
|
}
|
|
return 0
|
|
}
|
|
|
|
${metaExports}
|
|
|
|
export const BLIZZARD4_META: MissileMeta = {
|
|
...BLIZZARD3_META,
|
|
name: 'blizzard4',
|
|
}
|
|
|
|
export const BLIZZARDEXPLODE3_META: MissileMeta = {
|
|
...BLIZZARDEXPLODE2_META,
|
|
name: 'blizzardexplode3',
|
|
}
|
|
|
|
export const HYDRA_META: MissileMeta = {
|
|
...FIREBOLT_META,
|
|
name: 'hydra',
|
|
}
|
|
|
|
export const HYDRAFIRE_META: MissileMeta = {
|
|
...FIREBOLT_META,
|
|
name: 'hydrafire',
|
|
}
|
|
|
|
export const METEORTARGETINDICATOR_META: MissileMeta = {
|
|
...METEORCENTER_META,
|
|
name: 'meteortargetindicator',
|
|
}
|
|
|
|
export const FIREMEDIUM_META: MissileMeta = {
|
|
...METEORFIRE_META,
|
|
name: 'firemedium',
|
|
}
|
|
|
|
export const ELECTRICNOVA_META: MissileMeta = {
|
|
...NOVA_META,
|
|
name: 'electricnova',
|
|
}
|
|
|
|
export const CHAINLIGHTNING_META: MissileMeta = {
|
|
...LIGHTNINGSTRIKE_META,
|
|
name: 'chainlightning',
|
|
}
|
|
|
|
export const LIGHTNINGHIT_META: MissileMeta = {
|
|
...LIGHTNINGSTRIKE_META,
|
|
name: 'lightninghit',
|
|
}
|
|
|
|
export const THUNDERSTORM_META: MissileMeta = {
|
|
...THUNDERSTORM1_META,
|
|
name: 'thunderstorm',
|
|
}
|
|
|
|
export const LIGHTNINGBOLTBIG_META: MissileMeta = {
|
|
...THUNDERSTORM1_META,
|
|
name: 'lightningboltbig',
|
|
}
|
|
|
|
export const GLEAM_META: MissileMeta = {
|
|
...SPARKLE_META,
|
|
name: 'gleam',
|
|
}
|
|
|
|
export const FREEZEEXPLODEEJECTA_META: MissileMeta = {
|
|
...FREEZINGARROWEXP2_META,
|
|
name: 'freezeexplodeejecta',
|
|
}
|
|
|
|
export const GLACIALSPIKEEJECTA_META: MissileMeta = {
|
|
...FREEZINGARROWEXP2_META,
|
|
name: 'glacialspikeejecta',
|
|
}
|
|
|
|
export const BLAZE_META: MissileMeta = {
|
|
...FIREWALL_META,
|
|
name: 'blaze',
|
|
}
|
|
|
|
export const FIREWALLMAKER_META: MissileMeta = {
|
|
...FIREWALL_META,
|
|
name: 'firewallmaker',
|
|
}
|
|
|
|
export const GROUNDFIREBIG_META: MissileMeta = {
|
|
...FIREWALL_META,
|
|
name: 'groundfirebig',
|
|
}
|
|
|
|
export const FLAMETHROWER_META: MissileMeta = {
|
|
...INFERNOFLAME1_META,
|
|
name: 'flamethrower',
|
|
}
|
|
|
|
export const FLAMETHROWER2_META: MissileMeta = {
|
|
...INFERNOFLAME2_META,
|
|
name: 'flamethrower2',
|
|
}
|
|
|
|
export const COLDUNIQUE_META: MissileMeta = {
|
|
...FROSTNOVA_META,
|
|
name: 'coldunique',
|
|
}
|
|
|
|
export const MEPHFROSTNOVA_META: MissileMeta = {
|
|
...FROSTNOVA_META,
|
|
name: 'mephfrostnova',
|
|
}
|
|
|
|
export const MISSILE_METAS: Readonly<Record<string, MissileMeta>> = Object.freeze({
|
|
${mapEntries}
|
|
'blizzard4': BLIZZARD4_META,
|
|
'blizzardexplode3': BLIZZARDEXPLODE3_META,
|
|
'hydra': HYDRA_META,
|
|
'hydrafire': HYDRAFIRE_META,
|
|
'meteortargetindicator': METEORTARGETINDICATOR_META,
|
|
'firemedium': FIREMEDIUM_META,
|
|
'electricnova': ELECTRICNOVA_META,
|
|
'chainlightning': CHAINLIGHTNING_META,
|
|
'lightninghit': LIGHTNINGHIT_META,
|
|
'thunderstorm': THUNDERSTORM_META,
|
|
'lightningboltbig': LIGHTNINGBOLTBIG_META,
|
|
'gleam': GLEAM_META,
|
|
'freezeexplodeejecta': FREEZEEXPLODEEJECTA_META,
|
|
'glacialspikeejecta': GLACIALSPIKEEJECTA_META,
|
|
'blaze': BLAZE_META,
|
|
'firewallmaker': FIREWALLMAKER_META,
|
|
'groundfirebig': GROUNDFIREBIG_META,
|
|
'flamethrower': FLAMETHROWER_META,
|
|
'flamethrower2': FLAMETHROWER2_META,
|
|
'coldunique': COLDUNIQUE_META,
|
|
'mephfrostnova': MEPHFROSTNOVA_META,
|
|
|
|
// Exact CelFile aliases for 18 Sorceress Skills Missiles & Ground Truth CelFiles
|
|
'ElectricNova': ELECTRICNOVA_META,
|
|
'LightningStrike': LIGHTNINGSTRIKE_META,
|
|
'ChainLightning': CHAINLIGHTNING_META,
|
|
'LightningHit': LIGHTNINGHIT_META,
|
|
'Thunderstorm': THUNDERSTORM_META,
|
|
'LightningboltBig': LIGHTNINGBOLTBIG_META,
|
|
'Gleam': GLEAM_META,
|
|
'FreezeExplodeEjecta': FREEZEEXPLODEEJECTA_META,
|
|
'GlacialSpikeEjecta': GLACIALSPIKEEJECTA_META,
|
|
'Blaze': BLAZE_META,
|
|
'FirewallMaker': FIREWALLMAKER_META,
|
|
'groundFireBig': GROUNDFIREBIG_META,
|
|
'Flamethrower': FLAMETHROWER_META,
|
|
'Flamethrower2': FLAMETHROWER2_META,
|
|
'ColdUnique': COLDUNIQUE_META,
|
|
'MephFrostNova': MEPHFROSTNOVA_META,
|
|
'FrostNova': FROSTNOVA_META,
|
|
|
|
// Base missiles CelFiles
|
|
'Firebolt': FIREBOLT_META,
|
|
'Fireball': FIREBALL_META,
|
|
'FireArrowExplode2': FIREEXPLODE_META,
|
|
'firearrowexplode2': FIREEXPLODE_META,
|
|
'Icebolt': ICEBOLT_META,
|
|
'IceBlast': ICEBLAST_META,
|
|
'GlacialSpike': GLACIALSPIKE_META,
|
|
'IceArrowExplode': ICEEXPLODE_META,
|
|
'icearrowexplode': ICEEXPLODE_META,
|
|
'FreezeExplodeCenter': FREEZEEXPLODE_META,
|
|
'freezeexplodecenter': FREEZEEXPLODE_META,
|
|
'IceOrb': FROZENORB_META,
|
|
'iceorb': FROZENORB_META,
|
|
'IceOrbExplode': FROZENORBEXPLODE_META,
|
|
'iceorbexplode': FROZENORBEXPLODE_META,
|
|
'Blizzard': BLIZZARD3_META,
|
|
'blizzard': BLIZZARD3_META,
|
|
'ChargedBolt': CHARGEDBOLT_META,
|
|
'teethMissile': TEETH_META,
|
|
'teethmissile': TEETH_META,
|
|
'BoneSpear': BONESPEAR_META,
|
|
'BoneSpirit': BONESPIRIT_META,
|
|
'BoneSpiritExplode': BONESPIRITEXPLODE_META,
|
|
'poisonNova': POISONNOVA_META,
|
|
'HolyBoltMissile': HOLYBOLT_META,
|
|
'holyboltmissile': HOLYBOLT_META,
|
|
'SafeArrow': MAGICARROW_META,
|
|
'safearrow': MAGICARROW_META,
|
|
'FireArrow': FIREARROW_META,
|
|
'IceArrow': ICEARROW_META,
|
|
'Arrow': ARROW_META,
|
|
'LightningJavelin': LIGHTNINGJAVELIN_META,
|
|
'Meteor': METEOR_META,
|
|
'MeteorTail': METEORTAIL_META,
|
|
'MeteorTargetIndicator': METEORTARGETINDICATOR_META,
|
|
'ExpArrowExplode': METEOREXPLODE_META,
|
|
'exparrowexplode': METEOREXPLODE_META,
|
|
'groundFireMedium': FIREMEDIUM_META,
|
|
'groundfiremedium': FIREMEDIUM_META,
|
|
'groundFireSmall': FIRESMALL_META,
|
|
'groundfiresmall': FIRESMALL_META,
|
|
})
|
|
`
|
|
|
|
const outTsPath = join(projectRoot, 'src', 'render', 'missiles-meta.ts')
|
|
writeFileSync(outTsPath, metaTsContent, 'utf-8')
|
|
console.log(`[Missile Packer] Wrote ${outTsPath}`)
|
|
|
|
return metaRecord
|
|
}
|
|
|
|
// Direct invocation via tsx
|
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
bakeMissiles()
|
|
.then(() => {
|
|
console.log('[Missile Packer] All missile assets baked successfully.')
|
|
process.exit(0)
|
|
})
|
|
.catch(err => {
|
|
console.error('[Missile Packer] Failed:', err)
|
|
process.exit(1)
|
|
})
|
|
}
|