diff --git a/src/client/render/missiles-meta.ts b/src/client/render/missiles-meta.ts index f05d5d2..0b942df 100644 --- a/src/client/render/missiles-meta.ts +++ b/src/client/render/missiles-meta.ts @@ -33,36 +33,8 @@ export interface MissileMeta { 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> = { - 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, - ], -} +import { dir64ToDcc } from '../../common/units/character.ts' +import { dir64FromPoints } from '../../common/units/direction.ts' /** * Calculate DCC direction index for a missile based on 2D velocity vector. @@ -75,15 +47,29 @@ const DIR64_TO_DCC: Readonly> = { */ export function velocityToDccDirection(vx: number, vy: number, directions = 16): number { if (directions <= 1) return 0 + if (vx === 0 && vy === 0) 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 + return dir64ToDcc(dir64, directions) +} + +/** + * Calculate DCC direction index from start to target coordinates using + * Diablo II 1.13c integer path direction calculation (0x6FD5CF40). + */ +export function pointsToDccDirection( + startX: number, + startY: number, + targetX: number, + targetY: number, + directions = 16, + fallback = 0, +): number { + if (directions <= 1) return 0 + const dir64 = dir64FromPoints(startX, startY, targetX, targetY, fallback) + return dir64ToDcc(dir64, directions) } export const FIREBOLT_META: MissileMeta = { diff --git a/src/client/render/monster-art.ts b/src/client/render/monster-art.ts index f901343..5af8fc6 100644 --- a/src/client/render/monster-art.ts +++ b/src/client/render/monster-art.ts @@ -107,7 +107,7 @@ export async function loadMonsterAtlas( } } - const atlas = buildIndexedAtlas(combined, 172, { width: 1024 }) + const atlas = buildIndexedAtlas(combined, undefined, { width: 1024 }) const handle = renderer.addIndexedAtlas(atlas.r8, atlas.width, atlas.height) const groups = atlas.groups.slice(0, baseCount) diff --git a/src/client/render/renderer.ts b/src/client/render/renderer.ts index 102a7fa..bf41549 100644 --- a/src/client/render/renderer.ts +++ b/src/client/render/renderer.ts @@ -33,6 +33,8 @@ * a flush per colour. */ import type { AtlasFrame, SpriteAtlas } from './atlas.ts' +import type { Pl2, Pl2Transform } from '../../common/formats/pl2.ts' +import { decodePl2, pl2ExpectedSize } from '../../common/formats/pl2.ts' /** Camera state for one frame. */ export interface Camera { @@ -94,6 +96,20 @@ export interface DrawOptions { readonly elevationY?: number } +/** + * Optional transform tables or full PL2 file for authentic 1.13c hardware palette rows. + */ +export type PaletteTransformInput = + | Pl2 + | readonly (Pl2Transform | undefined | null)[] + | { + readonly cold?: Pl2Transform | undefined + readonly poison?: Pl2Transform | undefined + readonly unique?: Pl2Transform | undefined + readonly champion?: Pl2Transform | undefined + readonly hueVariations?: readonly Pl2Transform[] | undefined + } + /** Dynamic scene lighting configuration for Glide-style illumination. */ export interface LightingConfig { /** Ambient light color in range 0..1, e.g. [0.03, 0.03, 0.04]. */ @@ -233,7 +249,7 @@ ${cases.join('\n')} ${paletteLookup}${discardAlpha} if (u_lightMode == 1 && u_blendMode == 0) { vec4 lightSample = texture(u_lightmap, v_lightUv); - vec3 dynamicLight = lightSample.rgb * (0.22 + 0.78 * lightSample.a); + vec3 dynamicLight = lightSample.rgb; vec3 totalLight = clamp(u_ambient.rgb + dynamicLight, 0.0, 1.0); outColor = vec4(texel.rgb * totalLight * v_tint.rgb, texel.a * v_tint.a); } else { @@ -809,22 +825,68 @@ export class SpriteRenderer { /** * Upload the 256-colour Act palette and precompute hardware status tint rows: * - Row 0: Normal Act Palette - * - Row 1: Cold / Chilled Blue Tint - * - Row 2: Poisoned Green Tint - * - Row 3: Unique Gold Boss Tint - * - Row 4: Champion / Elite Blue Tint + * - Row 1: Cold / Chilled Blue Tint (authentic blueTones transform when available) + * - Row 2: Poisoned Green Tint (authentic greenTones transform when available) + * - Row 3: Unique Gold Boss Tint (authentic Utrans / hueVariations transform when available) + * - Row 4: Champion / Elite Blue Tint (authentic hueVariations transform when available) * - * @param rgb - 768-byte array of 256 RGB triplets (`[r, g, b, ...]`). + * @param rgb - 768-byte array of 256 RGB triplets (`[r, g, b, ...]`) or full decoded PL2 buffer. + * @param transforms - optional PL2 object or transform array for authentic 1.13c colormaps. */ - setPalette(rgb: Uint8Array): void { + setPalette(rgb: Uint8Array, transforms?: PaletteTransformInput): void { if (this.disposed) return this.flush() const gl = this.gl + + let baseRgb = rgb + let coldXform: Pl2Transform | undefined + let poisonXform: Pl2Transform | undefined + let uniqueXform: Pl2Transform | undefined + let champXform: Pl2Transform | undefined + + if (rgb.byteLength >= pl2ExpectedSize()) { + try { + const pl2 = decodePl2(rgb) + baseRgb = pl2.rgb + coldXform = pl2.blueTones + poisonXform = pl2.greenTones + uniqueXform = pl2.hueVariations[2] ?? pl2.selectedUnitShift + champXform = pl2.hueVariations[4] ?? pl2.blueTones + } catch { + // Fall back to treating as raw RGB + } + } else if (transforms) { + if (Array.isArray(transforms)) { + coldXform = transforms[0] ?? undefined + poisonXform = transforms[1] ?? undefined + uniqueXform = transforms[2] ?? undefined + champXform = transforms[3] ?? undefined + } else if ('blueTones' in transforms && 'greenTones' in transforms) { + const pl2 = transforms as Pl2 + coldXform = pl2.blueTones + poisonXform = pl2.greenTones + uniqueXform = pl2.hueVariations?.[2] ?? pl2.selectedUnitShift + champXform = pl2.hueVariations?.[4] ?? pl2.blueTones + } else { + const tObj = transforms as { + cold?: Pl2Transform + poison?: Pl2Transform + unique?: Pl2Transform + champion?: Pl2Transform + hueVariations?: readonly Pl2Transform[] + } + coldXform = tObj.cold ?? tObj.hueVariations?.[1] + poisonXform = tObj.poison ?? tObj.hueVariations?.[3] + uniqueXform = tObj.unique ?? tObj.hueVariations?.[2] + champXform = tObj.champion ?? tObj.hueVariations?.[4] + } + } + const rgba = new Uint8Array(256 * PALETTE_ROWS * 4) for (let i = 1; i < 256; i += 1) { - const r = rgb[i * 3] ?? 0 - const g = rgb[i * 3 + 1] ?? 0 - const b = rgb[i * 3 + 2] ?? 0 + const r = baseRgb[i * 3] ?? 0 + const g = baseRgb[i * 3 + 1] ?? 0 + const b = baseRgb[i * 3 + 2] ?? 0 const lum = Math.round(0.299 * r + 0.587 * g + 0.114 * b) // Row 0: Normal Act Palette @@ -836,30 +898,58 @@ export class SpriteRenderer { // Row 1: Cold / Chilled Icy Blue const r1 = (256 + i) * 4 - rgba[r1] = Math.min(255, Math.round(r * 0.35)) - rgba[r1 + 1] = Math.min(255, Math.round(g * 0.65 + 40)) - rgba[r1 + 2] = Math.min(255, Math.round(b * 0.9 + 100)) + if (coldXform) { + const targetIdx = (coldXform[i] ?? i) * 3 + rgba[r1] = baseRgb[targetIdx] ?? 0 + rgba[r1 + 1] = baseRgb[targetIdx + 1] ?? 0 + rgba[r1 + 2] = baseRgb[targetIdx + 2] ?? 0 + } else { + rgba[r1] = Math.min(255, Math.round(r * 0.35)) + rgba[r1 + 1] = Math.min(255, Math.round(g * 0.65 + 40)) + rgba[r1 + 2] = Math.min(255, Math.round(b * 0.9 + 100)) + } rgba[r1 + 3] = 255 // Row 2: Poisoned Venom Green const r2 = (256 * 2 + i) * 4 - rgba[r2] = Math.min(255, Math.round(r * 0.35)) - rgba[r2 + 1] = Math.min(255, Math.round(g * 0.85 + 85)) - rgba[r2 + 2] = Math.min(255, Math.round(b * 0.35)) + if (poisonXform) { + const targetIdx = (poisonXform[i] ?? i) * 3 + rgba[r2] = baseRgb[targetIdx] ?? 0 + rgba[r2 + 1] = baseRgb[targetIdx + 1] ?? 0 + rgba[r2 + 2] = baseRgb[targetIdx + 2] ?? 0 + } else { + rgba[r2] = Math.min(255, Math.round(r * 0.35)) + rgba[r2 + 1] = Math.min(255, Math.round(g * 0.85 + 85)) + rgba[r2 + 2] = Math.min(255, Math.round(b * 0.35)) + } rgba[r2 + 3] = 255 // Row 3: Unique Boss Golden Aura const r3 = (256 * 3 + i) * 4 - rgba[r3] = Math.min(255, Math.round(lum * 1.25 + 55)) - rgba[r3 + 1] = Math.min(255, Math.round(lum * 0.95 + 25)) - rgba[r3 + 2] = Math.min(255, Math.round(lum * 0.35)) + if (uniqueXform) { + const targetIdx = (uniqueXform[i] ?? i) * 3 + rgba[r3] = baseRgb[targetIdx] ?? 0 + rgba[r3 + 1] = baseRgb[targetIdx + 1] ?? 0 + rgba[r3 + 2] = baseRgb[targetIdx + 2] ?? 0 + } else { + rgba[r3] = Math.min(255, Math.round(lum * 1.25 + 55)) + rgba[r3 + 1] = Math.min(255, Math.round(lum * 0.95 + 25)) + rgba[r3 + 2] = Math.min(255, Math.round(lum * 0.35)) + } rgba[r3 + 3] = 255 // Row 4: Champion / Elite Royal Blue const r4 = (256 * 4 + i) * 4 - rgba[r4] = Math.min(255, Math.round(lum * 0.45 + 30)) - rgba[r4 + 1] = Math.min(255, Math.round(lum * 0.6 + 45)) - rgba[r4 + 2] = Math.min(255, Math.round(lum * 1.3 + 75)) + if (champXform) { + const targetIdx = (champXform[i] ?? i) * 3 + rgba[r4] = baseRgb[targetIdx] ?? 0 + rgba[r4 + 1] = baseRgb[targetIdx + 1] ?? 0 + rgba[r4 + 2] = baseRgb[targetIdx + 2] ?? 0 + } else { + rgba[r4] = Math.min(255, Math.round(lum * 0.45 + 30)) + rgba[r4 + 1] = Math.min(255, Math.round(lum * 0.6 + 45)) + rgba[r4 + 2] = Math.min(255, Math.round(lum * 1.3 + 75)) + } rgba[r4 + 3] = 255 } gl.bindTexture(gl.TEXTURE_2D, this.paletteTexture) @@ -1065,6 +1155,8 @@ export class SpriteRenderer { const tint = options.tint const flipped = options.flipX === true const paletteRow = page.indexed === true ? (options.paletteRow ?? 0) : -1 + const elevationY = options.elevationY !== undefined && options.elevationY > 0 ? Math.round(options.elevationY) : 0 + const topPalette = elevationY > 0 ? paletteRow + elevationY * 16 : paletteRow const drawW = options.width ?? frame.width const drawH = options.height ?? frame.height this.quad( @@ -1080,6 +1172,7 @@ export class SpriteRenderer { tint === undefined ? 1 : tint[3], unit, paletteRow, + topPalette, ) } diff --git a/src/client/scene/lighting-presets.ts b/src/client/scene/lighting-presets.ts index 8e69b79..1cde410 100644 --- a/src/client/scene/lighting-presets.ts +++ b/src/client/scene/lighting-presets.ts @@ -187,6 +187,7 @@ export interface PointLightInput { readonly r: number readonly g: number readonly b: number + readonly flicker?: number | undefined } /** @@ -242,18 +243,16 @@ export function applyLightingInputs(inputs: LightingInputs, targets: LightingPas const overrideAmb = presetCfg.overrideAmbient const envIntensity = inputs.environmentIntensity ?? environment?.currentIntensity ?? 255 const rawIntensity = overrideAmb !== undefined ? overrideAmb.intensity : envIntensity - const isDarkCave = rawIntensity <= 14 || inputs.preset === 'cave' || inputs.preset === 'torch' - const flicker = (Math.sin(inputs.tick * 0.35) * 1.5) | 0 - const basePlayerRadius = (isDarkCave ? 20 : 22) + (inputs.playerLightRadiusBonus ?? 0) + const basePlayerRadius = 11 + (inputs.playerLightRadiusBonus ?? 0) lightGrid.addLight({ subTileX: pSub.x, subTileY: pSub.y, - radius: Math.max(8, basePlayerRadius + flicker), - intensity: 242, + radius: Math.max(1, basePlayerRadius), + intensity: 255, red: 255, - green: 234, - blue: 196, + green: 255, + blue: 255, }) for (const pl of inputs.pointLights) { @@ -265,19 +264,19 @@ export function applyLightingInputs(inputs: LightingInputs, targets: LightingPas red: pl.r, green: pl.g, blue: pl.b, + ...(pl.flicker !== undefined ? { flicker: pl.flicker } : {}), }) } - const effectiveAmbIntensity = rawIntensity > 0 ? rawIntensity : 5 const envColor = inputs.environmentColor ?? [ environment?.currentRed ?? 255, environment?.currentGreen ?? 255, environment?.currentBlue ?? 255, ] - const effR = overrideAmb !== undefined ? overrideAmb.red : (rawIntensity > 0 ? envColor[0] : 185) - const effG = overrideAmb !== undefined ? overrideAmb.green : (rawIntensity > 0 ? envColor[1] : 175) - const effB = overrideAmb !== undefined ? overrideAmb.blue : (rawIntensity > 0 ? envColor[2] : 165) - const ambNorm = effectiveAmbIntensity / 255 + const effR = overrideAmb !== undefined ? overrideAmb.red : envColor[0] + const effG = overrideAmb !== undefined ? overrideAmb.green : envColor[1] + const effB = overrideAmb !== undefined ? overrideAmb.blue : envColor[2] + const ambNorm = rawIntensity / 255 const ambR = (effR / 255) * ambNorm const ambG = (effG / 255) * ambNorm const ambB = (effB / 255) * ambNorm @@ -291,3 +290,822 @@ export function applyLightingInputs(inputs: LightingInputs, targets: LightingPas lightmap: lightGrid.exportTextureBuffer(), }) } + +/** Canonical 1.13c Monster point light metadata from MonStats2.txt [Light, light-r, light-g, light-b] */ +export const CANONICAL_MONSTER_LIGHTS: Readonly> = Object.freeze({ + "fallenshaman1": [5,230,168,255], + "fallenshaman2": [5,230,168,255], + "fallenshaman3": [5,230,168,255], + "fallenshaman4": [5,230,168,255], + "fallenshaman5": [5,230,168,255], + "willowisp1": [6,255,255,255], + "willowisp2": [6,255,255,255], + "willowisp3": [6,255,255,255], + "willowisp4": [6,255,255,255], + "vampire1": [3,255,138,255], + "vampire2": [3,255,138,255], + "vampire3": [3,255,138,255], + "vampire4": [5,255,255,255], + "vampire5": [3,255,138,255], + "andariel": [8,255,255,255], + "duriel": [9,255,255,255], + "firebeast": [3,255,138,255], + "iceglobe": [3,255,60,255], + "lightningbeast": [3,255,255,255], + "poisonorb": [3,102,255,255], + "flyingscimitar": [3,100,190,255], + "cantor1": [3,105,190,255], + "cantor2": [3,105,190,255], + "cantor3": [3,105,190,255], + "cantor4": [3,105,190,255], + "mephisto": [9,255,255,255], + "summoner": [9,174,255,255], + "tyrael1": [11,255,255,255], + "izual": [9,255,255,255], + "roguehire": [5,255,255,255], + "skmage_pois1": [5,102,255,255], + "skmage_pois2": [5,102,255,255], + "skmage_pois3": [5,102,255,255], + "skmage_pois4": [5,102,255,255], + "fetishshaman1": [5,255,255,255], + "fetishshaman2": [5,255,255,255], + "fetishshaman3": [5,255,255,255], + "fetishshaman4": [5,255,255,255], + "fetishshaman5": [5,255,255,255], + "claygolem": [5,255,255,255], + "bloodgolem": [5,255,255,255], + "irongolem": [5,255,255,255], + "firegolem": [5,255,255,255], + "familiar": [5,255,255,255], + "fingermage1": [7,255,90,255], + "fingermage2": [7,255,228,132], + "fingermage3": [7,255,255,255], + "doomknight1": [4,255,255,255], + "doomknight2": [4,255,255,255], + "doomknight3": [4,255,255,255], + "act2hire": [5,255,255,255], + "hydra1": [4,255,138,255], + "hydra2": [4,255,138,255], + "hydra3": [4,255,138,255], + "dopplezon": [5,255,255,255], + "valkyrie": [5,255,255,255], + "act3hire": [5,255,255,255], + "necromage": [5,255,255,255], + "tyrael2": [11,255,255,255], + "skmage_cold1": [5,255,60,255], + "skmage_cold2": [5,255,60,255], + "skmage_cold3": [5,255,60,255], + "skmage_cold4": [5,255,60,255], + "skmage_fire1": [5,255,138,255], + "skmage_fire2": [5,255,138,255], + "skmage_fire3": [5,255,138,255], + "skmage_fire4": [5,255,138,255], + "skmage_ltng1": [5,255,255,255], + "skmage_ltng2": [5,255,255,255], + "skmage_ltng3": [5,255,255,255], + "skmage_ltng4": [5,255,255,255], + "mephistospirit": [3,255,255,255], + "izualghost": [5,255,255,255], + "malachai": [5,255,255,255], + "hephasto": [5,255,255,255], + "spiritofbarbs": [4,255,255,255], + "heartofwolverine": [4,255,50,50], + "oaksage": [4,255,255,255], + "suicideminion1": [5,230,168,255], + "suicideminion2": [5,230,168,255], + "suicideminion3": [5,230,168,255], + "suicideminion4": [5,230,168,255], + "suicideminion5": [5,230,168,255], + "suicideminion6": [5,230,168,255], + "suicideminion7": [5,230,168,255], + "suicideminion8": [5,230,168,255], + "tyrael3": [11,255,255,255], + "evilhut": [7,255,255,150], + "ancientstatue1": [6,255,255,255], + "ancientstatue2": [6,255,255,255], + "ancientstatue3": [6,255,255,255], + "baalthrone": [6,255,255,255], + "baalcrabstairs": [6,255,255,255], + "act5hire1": [5,255,255,255], + "act5hire2": [5,255,255,255], + "worldstoneeffect": [15,255,178,64], + "ubermephisto": [9,255,255,255], + "uberandariel": [8,255,255,255], + "uberizual": [9,255,255,255], + "uberduriel": [9,255,255,255], +}) + +/** Canonical 1.13c Object point light metadata from Objects.txt [Lit0..Lit7, Red, Green, Blue, Flicker] */ +export const CANONICAL_OBJECT_LIGHTS: Readonly> = Object.freeze({ + 2: [[0,17,0,0,0,0,0,0],255,255,255,0], + 11: [[0,11,0,0,0,0,0,0],255,255,255,0], + 17: [[0,0,17,0,0,0,0,0],255,255,255,0], + 18: [[0,0,17,0,0,0,0,0],255,255,255,0], + 19: [[0,0,17,0,0,0,0,0],255,255,255,0], + 20: [[0,0,17,0,0,0,0,0],255,255,255,0], + 21: [[0,0,17,0,0,0,0,0],255,255,255,0], + 22: [[0,0,17,0,0,0,0,0],255,255,255,0], + 26: [[5,10,5,0,0,0,0,0],255,255,255,0], + 29: [[0,19,18,0,0,0,0,0],255,236,176,1], + 33: [[18,18,18,0,0,0,0,0],255,255,213,1], + 34: [[18,18,18,0,0,0,0,0],255,255,213,1], + 37: [[0,19,19,0,0,0,0,0],255,236,176,0], + 38: [[17,19,19,0,0,0,0,0],255,236,176,1], + 39: [[0,19,19,0,0,0,0,0],255,236,176,1], + 48: [[19,19,19,17,17,17,17,17],255,236,176,1], + 49: [[19,19,19,17,17,17,17,17],255,236,176,1], + 59: [[0,18,19,0,0,0,0,0],120,120,255,1], + 60: [[0,18,19,0,0,0,0,0],255,120,120,1], + 77: [[0,17,0,0,0,0,0,0],255,255,255,0], + 80: [[0,17,0,0,0,0,0,0],255,255,255,0], + 81: [[12,17,7,0,0,0,0,0],255,255,255,0], + 83: [[0,17,0,0,0,0,0,0],255,255,255,0], + 84: [[0,17,0,0,0,0,0,0],255,255,255,0], + 85: [[0,17,0,0,0,0,0,0],255,255,255,0], + 86: [[0,19,0,0,0,0,0,0],255,255,180,0], + 90: [[0,17,0,0,0,0,0,0],255,255,255,0], + 97: [[0,17,0,0,0,0,0,0],255,255,255,0], + 100: [[0,10,10,0,0,0,0,0],255,255,255,0], + 101: [[0,17,18,0,0,0,0,0],255,236,176,0], + 102: [[0,17,18,0,0,0,0,0],255,236,176,0], + 108: [[16,0,0,0,0,0,0,0],255,255,255,0], + 109: [[0,17,0,0,0,0,0,0],255,255,255,0], + 116: [[10,17,0,0,0,0,0,0],255,190,255,0], + 117: [[0,19,18,0,0,0,0,0],255,236,176,0], + 119: [[0,18,19,0,0,0,0,0],100,100,255,1], + 120: [[0,17,0,0,0,0,0,0],255,255,255,0], + 123: [[0,17,0,0,0,0,0,0],255,255,255,0], + 124: [[0,17,0,0,0,0,0,0],255,255,255,0], + 131: [[17,0,0,0,0,0,0,0],255,100,100,0], + 133: [[0,17,0,0,0,0,0,0],255,255,255,0], + 134: [[0,17,0,0,0,0,0,0],255,255,255,0], + 135: [[0,17,0,0,0,0,0,0],255,255,255,0], + 136: [[0,17,0,0,0,0,0,0],255,255,255,0], + 145: [[0,18,19,0,0,0,0,0],100,100,255,1], + 149: [[0,17,17,0,0,0,0,0],255,255,255,0], + 150: [[0,17,0,0,0,0,0,0],255,255,255,0], + 151: [[0,17,0,0,0,0,0,0],255,255,255,0], + 152: [[25,25,25,0,0,0,0,0],255,255,255,0], + 153: [[0,18,18,0,0,0,0,0],255,190,255,0], + 156: [[0,18,19,0,0,0,0,0],100,100,255,1], + 157: [[0,18,19,0,0,0,0,0],100,100,255,1], + 160: [[0,16,17,0,0,0,0,0],255,255,255,0], + 161: [[0,17,18,0,0,0,0,0],255,255,255,0], + 162: [[0,18,19,0,0,0,0,0],255,255,255,0], + 164: [[0,17,0,0,0,0,0,0],255,255,255,0], + 165: [[0,17,0,0,0,0,0,0],255,255,255,0], + 166: [[0,17,0,0,0,0,0,0],255,255,255,0], + 167: [[0,17,0,0,0,0,0,0],255,255,255,0], + 168: [[0,17,0,0,0,0,0,0],255,255,255,0], + 170: [[0,17,0,0,0,0,0,0],255,255,255,0], + 172: [[0,17,0,0,0,0,0,0],255,255,255,0], + 173: [[0,17,0,0,0,0,0,0],255,255,255,0], + 184: [[0,17,0,0,0,0,0,0],255,255,255,0], + 189: [[0,18,19,0,0,0,0,0],100,100,255,0], + 190: [[0,17,0,0,0,0,0,0],255,255,255,0], + 191: [[0,17,0,0,0,0,0,0],255,255,255,0], + 192: [[17,0,0,0,0,0,0,0],255,255,255,0], + 197: [[0,17,0,0,0,0,0,0],255,255,255,0], + 199: [[0,17,0,0,0,0,0,0],255,255,255,0], + 200: [[0,17,0,0,0,0,0,0],255,255,255,0], + 201: [[0,17,0,0,0,0,0,0],255,255,255,0], + 202: [[0,17,0,0,0,0,0,0],255,255,255,0], + 206: [[0,17,0,0,0,0,0,0],255,255,255,0], + 226: [[0,17,0,0,0,0,0,0],255,255,255,0], + 231: [[0,17,0,0,0,0,0,0],255,255,255,0], + 232: [[0,17,0,0,0,0,0,0],255,255,255,0], + 237: [[0,18,19,0,0,0,0,0],100,100,255,1], + 238: [[0,18,19,0,0,0,0,0],100,100,255,1], + 239: [[0,0,17,0,0,0,0,0],255,255,255,0], + 245: [[17,0,0,0,0,0,0,0],255,255,255,0], + 259: [[0,3,0,0,0,0,0,0],255,125,125,1], + 262: [[0,17,0,0,0,0,0,0],255,255,255,0], + 263: [[0,17,0,0,0,0,0,0],255,255,255,0], + 264: [[0,17,0,0,0,0,0,0],255,255,255,0], + 265: [[0,17,0,0,0,0,0,0],255,255,255,0], + 273: [[18,0,0,0,0,0,0,0],255,255,255,0], + 275: [[0,17,0,0,0,0,0,0],255,255,255,0], + 276: [[0,17,0,0,0,0,0,0],255,255,255,0], + 277: [[0,17,0,0,0,0,0,0],255,255,255,0], + 278: [[0,17,0,0,0,0,0,0],255,255,255,0], + 279: [[0,17,0,0,0,0,0,0],255,255,255,0], + 280: [[0,17,0,0,0,0,0,0],255,255,255,0], + 281: [[0,17,0,0,0,0,0,0],255,255,255,0], + 282: [[0,17,0,0,0,0,0,0],255,255,255,0], + 283: [[17,0,0,0,0,0,0,0],255,255,255,0], + 285: [[17,0,0,0,0,0,0,0],255,255,255,0], + 286: [[17,0,0,0,0,0,0,0],255,255,255,0], + 287: [[17,0,0,0,0,0,0,0],255,255,255,0], + 288: [[0,18,19,0,0,0,0,0],100,100,255,1], + 296: [[18,0,0,0,0,0,0,0],255,255,255,0], + 297: [[18,0,0,0,0,0,0,0],255,255,255,0], + 298: [[0,17,19,0,0,0,0,0],120,120,255,1], + 299: [[0,17,0,0,0,0,0,0],255,255,255,0], + 300: [[0,17,0,0,0,0,0,0],255,255,255,0], + 303: [[0,17,0,0,0,0,0,0],255,255,255,0], + 304: [[17,0,0,0,0,0,0,0],255,255,255,0], + 305: [[17,0,0,0,0,0,0,0],255,255,255,0], + 306: [[17,0,0,0,0,0,0,0],255,255,255,0], + 307: [[17,0,0,0,0,0,0,0],125,125,255,0], + 308: [[17,0,0,0,0,0,0,0],125,125,255,0], + 309: [[17,0,0,0,0,0,0,0],125,125,255,0], + 310: [[17,0,0,0,0,0,0,0],125,125,255,0], + 311: [[17,0,0,0,0,0,0,0],125,125,255,0], + 312: [[17,0,0,0,0,0,0,0],125,125,255,0], + 313: [[17,0,0,0,0,0,0,0],125,125,255,0], + 319: [[0,17,0,0,0,0,0,0],255,255,255,0], + 320: [[0,17,0,0,0,0,0,0],255,255,255,0], + 323: [[0,18,19,0,0,0,0,0],100,100,255,1], + 324: [[0,18,19,0,0,0,0,0],100,100,255,1], + 325: [[0,17,0,0,0,0,0,0],255,255,255,0], + 327: [[10,19,19,0,0,0,0,0],255,236,176,0], + 328: [[10,19,19,0,0,0,0,0],255,236,176,0], + 342: [[15,17,19,0,0,0,0,0],255,100,100,1], + 343: [[0,17,0,0,0,0,0,0],255,255,255,0], + 344: [[0,17,0,0,0,0,0,0],255,255,255,0], + 345: [[6,0,0,0,0,0,0,0],255,100,100,1], + 346: [[8,0,0,0,0,0,0,0],255,100,100,1], + 347: [[12,0,0,0,0,0,0,0],255,100,100,1], + 351: [[6,0,0,0,0,0,0,0],255,100,100,0], + 352: [[8,0,0,0,0,0,0,0],255,100,100,0], + 353: [[10,0,0,0,0,0,0,0],255,100,100,0], + 354: [[12,0,0,0,0,0,0,0],100,100,255,1], + 355: [[12,0,0,0,0,0,0,0],100,100,255,1], + 356: [[12,0,0,0,0,0,0,0],100,100,255,1], + 358: [[19,19,0,0,0,0,0,0],255,236,176,1], + 359: [[19,19,0,0,0,0,0,0],255,236,176,1], + 361: [[0,17,0,0,0,0,0,0],255,255,255,0], + 370: [[19,0,0,0,0,0,0,0],255,255,255,1], + 373: [[0,3,0,0,0,0,0,0],255,125,125,1], + 376: [[13,17,19,15,13,13,0,0],255,236,176,0], + 377: [[19,18,19,0,0,0,0,0],255,120,120,1], + 380: [[17,15,17,0,0,0,0,0],255,200,200,0], + 381: [[17,15,17,0,0,0,0,0],255,200,200,0], + 392: [[0,15,17,0,0,0,0,0],255,128,128,0], + 393: [[0,15,17,0,0,0,0,0],255,128,128,0], + 394: [[0,15,17,0,0,0,0,0],255,128,128,0], + 395: [[0,15,17,0,0,0,0,0],255,128,128,0], + 396: [[0,15,17,0,0,0,0,0],255,128,128,0], + 397: [[12,0,0,0,0,0,0,0],100,100,255,1], + 398: [[0,18,19,0,0,0,0,0],100,100,255,1], + 400: [[17,0,0,0,0,0,0,0],255,255,255,0], + 401: [[18,0,0,0,0,0,0,0],180,55,55,0], + 402: [[0,18,19,0,0,0,0,0],100,100,255,1], + 403: [[17,0,0,0,0,0,0,0],255,255,255,0], + 404: [[17,0,0,0,0,0,0,0],255,255,255,0], + 405: [[12,0,0,0,0,0,0,0],100,100,255,1], + 406: [[12,0,0,0,0,0,0,0],100,100,255,1], + 407: [[12,0,0,0,0,0,0,0],100,100,255,1], + 408: [[17,0,0,0,0,0,0,0],255,255,255,0], + 409: [[19,0,0,0,0,0,0,0],255,255,255,0], + 410: [[19,0,0,0,0,0,0,0],255,255,255,0], + 414: [[0,17,0,0,0,0,0,0],255,255,255,0], + 415: [[0,17,0,0,0,0,0,0],255,255,255,0], + 419: [[0,11,0,0,0,0,0,0],255,255,255,0], + 421: [[0,17,0,0,0,0,0,0],255,255,255,0], + 422: [[0,17,0,0,0,0,0,0],255,255,255,0], + 423: [[0,17,0,0,0,0,0,0],255,255,255,0], + 427: [[0,17,0,0,0,0,0,0],255,255,255,0], + 428: [[0,17,0,0,0,0,0,0],255,255,255,0], + 429: [[0,18,19,0,0,0,0,0],100,100,255,1], + 434: [[19,0,0,0,0,0,0,0],255,255,255,0], + 435: [[19,0,0,0,0,0,0,0],255,255,255,0], + 436: [[19,0,0,0,0,0,0,0],255,255,255,0], + 437: [[19,0,0,0,0,0,0,0],255,255,255,0], + 438: [[19,0,0,0,0,0,0,0],255,255,255,0], + 439: [[19,0,0,0,0,0,0,0],255,255,255,0], + 442: [[17,0,0,0,0,0,0,0],255,255,255,0], + 454: [[0,11,0,0,0,0,0,0],255,255,255,0], + 464: [[0,17,0,0,0,0,0,0],255,255,255,0], + 465: [[0,17,0,0,0,0,0,0],255,255,255,0], + 472: [[0,17,0,0,0,0,0,0],255,255,255,0], + 473: [[0,10,5,0,0,0,0,0],255,255,255,0], + 474: [[8,0,9,0,0,0,0,0],255,255,255,0], + 475: [[8,0,9,0,0,0,0,0],255,255,255,0], + 476: [[8,0,9,0,0,0,0,0],255,255,255,0], + 479: [[0,17,0,0,0,0,0,0],255,255,255,0], + 480: [[19,0,0,0,0,0,0,0],255,255,255,0], + 481: [[19,0,0,0,0,0,0,0],255,255,255,0], + 482: [[19,0,0,0,0,0,0,0],255,255,255,0], + 483: [[0,17,0,0,0,0,0,0],255,255,255,0], + 484: [[0,17,0,0,0,0,0,0],255,255,255,0], + 488: [[0,17,0,0,0,0,0,0],255,255,255,0], + 489: [[19,0,0,0,0,0,0,0],255,255,255,0], + 490: [[19,0,0,0,0,0,0,0],255,255,255,0], + 491: [[0,17,0,0,0,0,0,0],255,255,255,0], + 492: [[0,17,0,0,0,0,0,0],255,255,255,0], + 494: [[0,18,19,0,0,0,0,0],100,100,255,1], + 495: [[0,17,0,0,0,0,0,0],255,255,255,0], + 496: [[0,18,19,0,0,0,0,0],100,100,255,1], + 497: [[0,17,0,0,0,0,0,0],255,255,255,0], + 499: [[0,17,0,0,0,0,0,0],255,255,255,0], + 503: [[0,17,0,0,0,0,0,0],255,255,255,0], + 509: [[0,17,0,0,0,0,0,0],255,255,255,0], + 511: [[0,18,19,0,0,0,0,0],100,100,255,1], + 512: [[0,17,0,0,0,0,0,0],255,255,255,0], + 514: [[19,0,0,0,0,0,0,0],255,255,255,0], + 515: [[19,0,0,0,0,0,0,0],255,255,255,0], + 520: [[0,17,0,0,0,0,0,0],255,255,255,0], + 521: [[0,17,0,0,0,0,0,0],255,255,255,0], + 522: [[0,17,0,0,0,0,0,0],255,255,255,0], + 523: [[18,0,0,0,0,0,0,0],255,160,55,1], + 536: [[19,0,0,0,0,0,0,0],255,255,255,0], + 537: [[19,0,0,0,0,0,0,0],255,255,255,0], + 538: [[19,0,0,0,0,0,0,0],255,255,255,0], + 539: [[0,18,19,0,0,0,0,0],100,100,255,1], + 546: [[0,15,19,0,0,0,0,0],255,255,255,0], + 552: [[19,0,0,0,0,0,0,0],255,255,255,0], + 553: [[19,0,0,0,0,0,0,0],255,255,255,0], + 554: [[19,0,0,0,0,0,0,0],255,255,255,0], + 558: [[19,0,0,0,0,0,0,0],255,255,255,0], + 559: [[19,0,0,0,0,0,0,0],255,255,255,0], + 560: [[19,0,0,0,0,0,0,0],255,255,255,0], + 564: [[0,15,17,0,0,0,0,0],255,255,255,0], +}) + +/** Canonical 1.13c Missile point light metadata from Missiles.txt [Id, Light, Red, Green, Blue, Flicker] */ +export const CANONICAL_MISSILE_LIGHTS: Readonly> = Object.freeze({ + "bighead1": [2,5,143,143,255,0], + "bighead2": [3,5,143,143,255,0], + "bighead3": [4,5,143,143,255,0], + "bighead4": [5,5,143,143,255,0], + "bighead5": [6,5,143,143,255,0], + "firearrow": [12,5,255,178,64,0], + "shafire1": [22,5,255,178,64,0], + "shafire2": [23,5,255,178,64,0], + "shafire3": [24,5,255,178,64,0], + "shafire4": [25,5,255,178,64,0], + "shafire5": [26,5,255,178,64,0], + "magicarrow": [27,3,255,255,255,0], + "icearrow": [28,5,81,81,255,0], + "fireexplode": [29,13,255,178,64,0], + "iceexplode": [30,11,81,81,255,0], + "bigheadexp": [33,8,143,143,255,0], + "shamanexp": [34,8,255,178,64,0], + "coldarrow": [40,5,81,81,255,0], + "explodingarrow": [41,5,255,178,64,0], + "explodingarrowexp": [42,15,255,178,64,0], + "plaguejavelin": [43,4,128,255,128,0], + "explosivepotionexp": [51,13,255,178,64,0], + "holybolt": [55,7,222,222,255,0], + "chargedbolt": [56,3,255,255,255,4], + "firebolt": [58,7,255,178,64,0], + "icebolt": [59,7,81,81,255,0], + "infernoflame1": [60,5,255,178,64,0], + "infernoflame2": [61,5,255,178,64,0], + "fireball": [62,7,255,178,64,0], + "blaze": [67,5,255,178,64,4], + "firewallmaker": [68,8,255,178,64,0], + "firewall": [69,5,255,178,64,4], + "unholybolt1": [77,5,255,255,255,0], + "unholybolt2": [78,5,255,255,255,0], + "unholybolt3": [79,5,255,255,255,0], + "unholybolt4": [80,5,255,255,255,0], + "immolationarrow": [85,9,255,178,64,0], + "guidedarrow": [86,3,255,255,255,0], + "freezingarrow": [87,5,81,81,255,0], + "freezingarrowexp1": [88,11,81,81,255,0], + "iceblast": [91,7,81,81,255,0], + "blessedhammer": [92,5,222,222,255,0], + "chainlightning": [93,4,255,255,255,0], + "fistofares": [94,3,255,255,255,0], + "chillblood": [95,4,81,81,255,0], + "glacialspike": [96,5,81,81,255,0], + "teleport": [97,6,255,255,255,0], + "lightningbolt": [98,4,255,255,255,0], + "lightninghit": [99,3,255,255,255,4], + "meteorcenter": [101,4,255,64,64,0], + "meteorexplode": [103,13,255,178,64,0], + "firesmall": [104,2,255,178,64,2], + "firemedium": [105,3,255,178,64,2], + "monblizcenter": [106,9,81,81,255,0], + "teeth": [114,3,255,255,255,0], + "corpseexplosion": [115,5,255,178,64,0], + "poisoncorpseexplosion": [116,5,255,178,64,0], + "monstercorpseexplode": [117,5,255,178,64,0], + "rogue2": [121,5,255,178,64,0], + "rogue3": [122,5,81,81,255,0], + "bat lightning bolt": [123,4,255,255,255,0], + "bat lightning trail": [124,3,255,255,255,4], + "skmage1": [125,5,255,255,255,0], + "skmage2": [126,5,255,255,255,0], + "skmage3": [127,5,255,255,255,0], + "skmage4": [128,5,255,255,255,0], + "vampirefireball": [129,8,255,178,64,0], + "vampirefirewallmaker": [130,8,255,178,64,0], + "vampirefirewall": [131,2,255,178,64,4], + "vampiremeteorcenter": [133,4,255,64,64,0], + "vampiremeteorexp": [134,13,255,178,64,0], + "raven2": [136,9,255,178,64,0], + "amphibiangoo1": [137,2,64,255,64,0], + "amphibiangoo2": [138,2,64,255,64,0], + "tentaclegoo": [139,2,64,255,64,0], + "curseeffectred": [142,1,255,255,255,0], + "fetishinferno1": [144,4,255,178,64,0], + "fetishinferno2": [145,4,255,178,64,0], + "cursecast": [147,1,255,255,255,0], + "redlightmissile": [151,5,255,178,64,0], + "greenlightmissile": [152,5,128,255,128,0], + "bluelightmissile": [153,5,81,81,255,0], + "whitelightmissile": [154,5,255,255,255,0], + "corpsepoisoncloud": [155,2,128,255,128,0], + "chillbloodcloud": [156,3,81,81,255,0], + "chillbloodpuff": [157,3,81,81,255,0], + "blizzardcenter": [158,9,81,81,255,0], + "thunderstorm1": [166,10,255,255,255,0], + "thunderstorm2": [167,10,255,255,255,0], + "thunderstorm3": [168,10,255,255,255,0], + "thunderstorm4": [169,10,255,255,255,0], + "monsterlight": [170,4,255,255,255,0], + "diablight": [172,3,255,255,255,0], + "redemption": [173,6,255,255,255,0], + "redemptionfail": [174,5,255,178,64,0], + "handofgod": [175,10,255,255,255,0], + "diabfire": [176,3,255,255,255,0], + "electric throwaxe": [178,4,255,255,255,0], + "diabwallmaker": [179,3,255,178,64,0], + "diabwall": [180,3,255,178,64,4], + "bonespear": [192,3,255,255,255,0], + "bonespirit": [193,3,255,255,255,0], + "lightunique": [195,3,255,255,255,4], + "skbowarrow4": [199,5,255,178,64,0], + "skbowarrow5": [200,5,255,255,255,0], + "andypoisonbolt": [203,5,255,255,255,0], + "lightningjavelin": [205,6,255,255,255,0], + "lightningfury": [206,6,255,255,255,0], + "bonewallmaker": [207,3,255,255,255,0], + "necromage1": [208,5,255,255,255,0], + "necromage2": [209,5,255,255,255,0], + "necromage3": [210,5,255,255,255,0], + "necromage4": [211,5,255,255,255,0], + "chargedstrikebolt": [215,3,255,255,255,4], + "buglightning": [225,3,255,255,255,4], + "immolationfire": [230,5,255,178,64,4], + "furylightning": [231,4,255,255,255,0], + "lightningstrike": [232,4,255,255,255,0], + "fistoftheheavensbolt": [234,8,222,222,255,0], + "meteorfire": [240,3,255,178,64,2], + "hydra": [247,5,255,255,255,0], + "zakarumlight": [258,3,255,255,255,0], + "frozenorb": [260,6,81,81,255,0], + "frozenorbbolt": [261,4,81,81,255,0], + "frozenorbnova": [262,4,81,81,255,0], + "frozenorbexplode": [263,6,81,81,255,0], + "chillingarmorbolt": [264,4,81,81,255,0], + "chainlightning2": [267,4,255,255,255,0], + "revivesmall": [268,5,255,178,64,0], + "revivemedium": [269,5,255,178,64,0], + "revivelarge": [270,5,255,178,64,0], + "monglacialspike": [271,5,81,81,255,0], + "mephisto": [276,9,255,255,255,0], + "firehead": [277,8,255,178,64,0], + "arcanelightningbolt": [279,8,178,178,255,0], + "frogfire": [280,4,255,64,64,0], + "frogcold": [281,4,64,64,255,0], + "frogpois": [282,4,64,255,64,0], + "desertfireball": [283,6,255,64,64,0], + "brdeathcontrol": [284,6,255,255,255,0], + "brdeathlightningbolt": [285,4,255,255,255,0], + "denofevillight": [287,10,255,255,255,0], + "towermist": [291,2,255,255,255,0], + "towermisttrail": [292,2,255,255,255,0], + "brdeathspirits1": [296,4,255,255,255,0], + "brdeathspiritnu": [297,4,255,255,255,0], + "brdeathspiritdt": [298,4,255,255,255,0], + "mephistodeathcontrol": [299,6,255,255,255,0], + "mephistofirewallmaker": [300,5,255,178,64,0], + "mephistofirewall": [301,5,255,178,64,4], + "mephistoexplosionbig": [303,13,255,178,64,0], + "mephistoexplosionsmall": [305,13,255,178,64,0], + "andyfirewallmaker": [308,8,255,178,64,0], + "andyfirewall": [309,5,255,178,64,4], + "andycolumnfirebase": [310,5,255,178,64,4], + "willowisplightningbolt": [320,8,178,178,255,0], + "queenpoisoncloud": [321,4,64,255,64,0], + "undeadmissile1": [323,9,255,255,255,0], + "undeadmissile2": [324,9,255,255,255,0], + "undeadmissile3": [325,9,255,255,255,0], + "undeadmissile4": [326,9,255,255,255,0], + "monbonespirit": [329,3,255,255,255,0], + "countessfirewall": [331,5,255,178,64,0], + "towerchestspawner": [332,15,192,192,255,0], + "hellmeteordown": [336,5,255,255,255,0], + "hellmeteorball": [337,5,255,255,255,0], + "horadriclightning": [339,3,255,255,255,0], + "horadriclight": [340,2,255,255,255,0], + "regurgitatorcorpse": [341,2,255,64,64,0], + "regurgitatorcorpseexpl": [342,13,255,64,64,0], + "highpriestlightning": [343,1,255,64,64,0], + "radamentdeath": [347,15,255,255,255,0], + "taintedsunflash": [351,4,255,255,255,0], + "healingbolt": [357,2,222,222,255,0], + "orbmist": [368,2,255,125,125,0], + "orbmisttrail": [369,2,255,125,125,0], + "orbmistfade": [370,2,255,125,125,0], + "hfcontrol": [373,8,255,255,255,0], + "izual mist loop": [380,2,255,255,255,0], + "izual lightning": [382,2,255,255,255,0], + "cairn stones bolt": [384,10,255,255,255,0], + "bomb explosion": [387,5,255,178,64,0], + "shock field on ground": [389,3,255,255,255,0], + "catapultchargedball": [407,5,143,143,255,0], + "catapultchargedballbolt": [408,3,255,255,255,4], + "catapult cold ball": [416,5,143,143,255,0], + "catapult plague ball": [419,5,143,143,255,0], + "catapult meteor ball": [422,5,143,143,255,0], + "catapult meteor fire": [423,5,255,178,64,0], + "healing vortex": [425,5,255,178,64,0], + "explodingjavalinexp": [430,15,255,178,64,0], + "lightjavalintrail": [432,3,255,255,255,4], + "lightjavalinexplosion": [433,3,255,255,255,4], + "icejavalinexplode": [435,11,81,81,255,0], + "advlighttrailingjav2": [439,3,255,255,255,4], + "advlightjavexplode": [440,3,255,255,255,4], + "sucfireball": [441,10,255,178,64,0], + "sucfireballexplode": [442,13,255,178,64,0], + "sucfireballtrail": [443,5,255,178,64,4], + "sucshockfieldmissile": [444,6,255,255,255,0], + "sucshockfield": [446,5,255,178,64,0], + "imp charged bolt": [450,3,255,255,255,4], + "moltenboulderfirepath": [455,5,255,178,64,0], + "firestorm": [457,5,255,178,64,4], + "erruption crack 1": [462,5,255,178,64,0], + "erruption crack 2": [463,5,255,178,64,0], + "volcano overlay fire": [480,10,255,64,64,0], + "lurecenter": [486,3,81,81,255,0], + "impmiss1": [488,5,255,64,64,0], + "impmiss2": [489,5,255,64,64,0], + "impmiss3": [490,5,255,64,64,0], + "impmiss4": [491,5,255,64,64,0], + "impmiss5": [492,5,255,64,64,0], + "sentrychargedbolt": [495,3,255,255,255,4], + "recycler explosion": [501,5,255,178,64,0], + "lightningtalons": [512,4,255,255,255,0], + "lightningtalonstrail": [513,3,255,255,255,4], + "phoenixtrail": [514,5,255,178,64,4], + "wake of destruction maker": [517,8,255,178,64,0], + "wake of destruction": [518,5,255,178,64,4], + "tigerfury": [520,3,255,255,255,0], + "tigerfurytrail": [521,5,255,178,64,4], + "inferno sentry 1": [523,5,255,178,64,0], + "inferno sentry 2": [524,5,255,178,64,0], + "sentrylightningbolt": [526,4,255,255,255,0], + "sentrylightninghit": [527,3,255,255,255,4], + "ancient death center": [541,2,255,125,125,0], + "ancient death cloud": [542,2,255,125,125,0], + "chainlightningcharge up": [544,4,255,255,255,0], + "pain worm appear": [545,5,255,178,64,0], + "baal taunt lightning trail": [548,4,255,255,255,0], + "baal spawn monsters": [550,5,81,81,255,0], + "blade shield missile": [552,5,255,255,255,0], + "baal inferno": [554,5,255,255,255,0], + "baal nova": [555,5,255,255,255,0], + "fistsoffireexplode": [556,15,255,178,64,0], + "fistsoffirefirewall": [557,5,255,178,64,0], + "clawsofthunderbolt": [558,3,255,255,255,4], + "bladesoficeexplode": [560,11,81,81,255,0], + "royalstrikemeteorcenter": [564,4,255,64,64,0], + "royalstrikemeteorexplode": [566,13,255,178,64,0], + "royalstrikemeteorfire": [567,5,255,178,64,0], + "royalstrikechainlightning": [568,4,255,255,255,0], + "royalstrikechaosice": [569,4,81,81,255,0], + "highpriestlightning2": [574,12,255,64,64,0], + "infernoflame3": [575,5,255,178,64,0], + "armageddonexplosion": [580,5,255,255,255,0], + "baalcorpseexplodedelay": [587,5,255,178,64,0], + "baalcorpseexplodeexpl": [588,5,255,178,64,0], + "baal cold maker": [589,5,143,143,255,0], + "baal cold trail": [590,5,143,143,255,0], + "baal spawn monsters exp": [591,5,81,81,255,0], + "impmiss21": [592,5,255,64,64,0], + "impmiss22": [593,5,255,64,64,0], + "impmiss23": [594,5,255,64,64,0], + "impmiss24": [595,5,255,64,64,0], + "impmiss25": [596,5,255,64,64,0], + "baalteleport": [613,3,128,64,64,0], + "baalclonedeath": [614,3,128,64,64,0], + "armageddonfire": [623,5,255,178,64,0], + "icesparkle": [624,2,255,178,64,0], + "baalfx control": [625,10,255,178,64,0], + "baalfx baal head 1": [632,5,255,178,64,0], + "baalfx baal head 2": [633,5,255,178,64,0], + "baalfx baal head 3": [634,5,255,178,64,0], + "blessedhammerex": [640,5,222,222,255,0], + "sentrylightningbolt2": [641,4,255,255,255,0], + "sentrylightninghit2": [642,3,255,255,255,1], + "skbowarrow6": [644,2,255,178,64,0], + "skbowarrow7": [645,2,81,81,255,0], + "skbowarrow8": [646,2,255,178,64,0], + "bighead6": [647,3,143,143,255,0], + "viper_fire": [650,3,255,255,255,0], + "viper_bonespear": [652,3,255,255,255,0], + "countessfirewallmaker": [653,8,255,178,64,0], + "skmagepois": [661,2,255,255,255,0], + "skmagecold": [662,2,255,255,255,0], + "skmagefire": [663,2,255,255,255,0], + "skmageltng": [664,2,255,255,255,0], + "succubusmiss": [665,2,255,64,64,0], + "willowisplightningbolt2": [666,8,178,178,255,0], + "impmissex": [669,5,255,64,64,0], + "diablogeddonexplosion": [673,5,255,255,255,0], + "diablogeddonfire": [674,5,255,178,64,0], + "megademoninferno": [675,4,255,178,64,0], + "trapfirebolt": [676,7,255,178,64,0], + "mephlight": [680,4,255,255,255,0], + "vampiremeteorfire": [681,3,255,178,64,2], +}) + +/** Canonical 1.13c Overlay point light metadata from Overlay.txt [Radius, InitRadius, Red, Green, Blue] */ +export const CANONICAL_OVERLAY_LIGHTS: Readonly> = Object.freeze({ + 1: [11,11,255,255,255], + 2: [10,1,255,193,103], + 3: [6,6,255,193,103], + 4: [10,1,210,210,255], + 5: [6,6,210,210,255], + 6: [10,1,255,255,200], + 7: [6,6,255,255,200], + 8: [10,1,211,148,255], + 9: [6,6,211,148,255], + 10: [6,6,211,148,255], + 27: [8,1,255,184,199], + 29: [9,1,255,178,64], + 30: [9,1,255,178,64], + 31: [9,1,81,81,255], + 32: [9,1,81,81,255], + 33: [9,1,81,81,255], + 34: [9,1,255,255,255], + 35: [14,14,255,193,103], + 36: [14,14,210,210,255], + 37: [8,1,255,193,103], + 38: [13,1,200,200,255], + 39: [13,1,200,200,255], + 40: [13,1,200,200,255], + 41: [6,1,81,81,255], + 42: [6,1,81,81,255], + 43: [6,1,81,81,255], + 44: [6,1,81,81,255], + 45: [5,1,255,255,200], + 47: [6,1,255,255,255], + 48: [6,6,255,255,255], + 49: [6,1,255,255,255], + 50: [2,2,255,255,255], + 51: [6,1,255,255,255], + 69: [10,1,255,193,103], + 75: [6,1,255,255,255], + 76: [6,1,255,255,255], + 77: [6,1,255,193,103], + 78: [2,2,103,193,255], + 79: [2,2,255,193,103], + 81: [1,3,255,193,103], + 87: [3,3,255,255,255], + 88: [3,3,255,255,255], + 89: [3,3,255,255,255], + 90: [3,3,255,255,255], + 91: [3,3,255,255,255], + 92: [3,3,255,255,255], + 93: [3,3,255,255,255], + 94: [3,3,255,255,255], + 95: [3,3,81,81,255], + 96: [8,8,255,255,255], + 97: [8,8,255,255,255], + 98: [8,1,255,255,255], + 110: [11,11,255,255,255], + 123: [10,1,210,210,255], + 128: [6,1,255,64,64], + 129: [6,1,255,210,210], + 130: [6,1,255,210,210], + 131: [6,1,255,255,255], + 133: [6,1,255,255,255], + 135: [6,1,255,255,255], + 136: [6,1,255,255,255], + 137: [6,1,255,255,255], + 146: [2,2,255,255,255], + 153: [9,1,255,178,64], + 157: [2,2,255,255,255], + 158: [2,2,255,255,255], + 164: [5,2,255,255,255], + 169: [2,2,255,64,64], + 171: [4,1,255,255,255], + 184: [4,1,178,178,178], + 185: [2,2,255,64,64], + 186: [9,1,255,255,255], + 187: [2,2,255,255,255], + 214: [2,1,255,210,210], + 215: [3,3,255,255,255], + 216: [3,3,255,255,255], + 221: [3,1,255,255,255], + 222: [3,1,255,255,255], + 223: [6,1,255,193,103], + 225: [6,1,255,64,64], + 226: [6,1,255,64,64], + 227: [6,1,255,64,64], +}) + + +export interface MonsterLightConfig { + readonly radius: number + readonly intensity: number + readonly r: number + readonly g: number + readonly b: number +} + +export interface ObjectLightConfig { + readonly radius: number + readonly intensity: number + readonly r: number + readonly g: number + readonly b: number + readonly flicker: number +} + +export interface MissileLightConfig { + readonly radius: number + readonly intensity: number + readonly r: number + readonly g: number + readonly b: number + readonly flicker: number +} + +export interface OverlayLightConfig { + readonly radius: number + readonly initRadius: number + readonly intensity: number + readonly r: number + readonly g: number + readonly b: number +} + +export function getMonsterLightConfig(monsterId: string): MonsterLightConfig | null { + const entry = CANONICAL_MONSTER_LIGHTS[monsterId.trim().toLowerCase()] + if (!entry) return null + return { + radius: entry[0], + intensity: 255, + r: entry[1], + g: entry[2], + b: entry[3], + } +} + +export function getObjectLightConfig(objectId: number, mode?: number): ObjectLightConfig | null { + const entry = CANONICAL_OBJECT_LIGHTS[objectId] + if (!entry) return null + const [lits, r, g, b, flicker] = entry + let radius = 0 + if (mode !== undefined && mode >= 0 && mode < lits.length) { + radius = lits[mode] ?? 0 + } else { + radius = (lits[1] ?? 0) > 0 ? (lits[1] ?? 0) : ((lits[2] ?? 0) > 0 ? (lits[2] ?? 0) : Math.max(...lits)) + } + if (radius <= 0) return null + return { + radius, + intensity: 255, + r, + g, + b, + flicker, + } +} + +const MISSILE_LIGHT_BY_ID: Map = (() => { + const map = new Map() + for (const entry of Object.values(CANONICAL_MISSILE_LIGHTS)) { + map.set(entry[0], entry) + } + return map +})() + +export function getMissileLightConfig(idOrName: string | number): MissileLightConfig | null { + let entry: readonly [id: number, radius: number, r: number, g: number, b: number, flicker: number] | undefined + if (typeof idOrName === 'number') { + entry = MISSILE_LIGHT_BY_ID.get(idOrName) + } else { + entry = CANONICAL_MISSILE_LIGHTS[idOrName.trim().toLowerCase()] + } + if (!entry) return null + return { + radius: entry[1], + intensity: 255, + r: entry[2], + g: entry[3], + b: entry[4], + flicker: entry[5], + } +} + +export function getOverlayLightConfig(overlayId: number): OverlayLightConfig | null { + const entry = CANONICAL_OVERLAY_LIGHTS[overlayId] + if (!entry) return null + return { + radius: entry[0], + initRadius: entry[1], + intensity: 255, + r: entry[2], + g: entry[3], + b: entry[4], + } +} + diff --git a/src/client/scene/missile-overlay-renderer.ts b/src/client/scene/missile-overlay-renderer.ts index 1455b58..e95e6b8 100644 --- a/src/client/scene/missile-overlay-renderer.ts +++ b/src/client/scene/missile-overlay-renderer.ts @@ -7,7 +7,6 @@ import { import type { AtlasFrame } from '../render/atlas.ts' import { LightningTrailManager, - drawElectricBoltFallback, } from '../render/missile-renderer.ts' import { MISSILE_METAS, @@ -29,7 +28,6 @@ export { LightningTrailManager, MISSILE_METAS, OVERLAY_METAS, - drawElectricBoltFallback, getMissileTxtData, velocityToDccDirection, type MissileFrameTuple, diff --git a/src/common/drlg/light-grid.ts b/src/common/drlg/light-grid.ts index 9f8ca93..91d4728 100644 --- a/src/common/drlg/light-grid.ts +++ b/src/common/drlg/light-grid.ts @@ -65,7 +65,7 @@ export interface LightSource { /** Blue channel [0..255]. */ readonly blue: number /** Optional flicker perturbation flag or offset. */ - readonly flicker?: number + readonly flicker?: number | undefined } /** diff --git a/tests/issue-606-614-621-parity.test.ts b/tests/issue-606-614-621-parity.test.ts new file mode 100644 index 0000000..a519425 --- /dev/null +++ b/tests/issue-606-614-621-parity.test.ts @@ -0,0 +1,231 @@ +import { describe, it, expect } from 'vitest' +import { + applyLightingInputs, + CANONICAL_MONSTER_LIGHTS, + CANONICAL_OBJECT_LIGHTS, + CANONICAL_MISSILE_LIGHTS, + CANONICAL_OVERLAY_LIGHTS, + getMonsterLightConfig, + getObjectLightConfig, + getMissileLightConfig, + getOverlayLightConfig, +} from '../src/client/scene/lighting-presets.ts' +import type { CollisionGrid } from '../src/common/world/d2map.ts' +import { LightGrid, LIGHT_GRID_SIZE } from '../src/common/drlg/light-grid.ts' +import { + velocityToDccDirection, + pointsToDccDirection, +} from '../src/client/render/missiles-meta.ts' +import { + fragmentShaderSource, +} from '../src/client/render/renderer.ts' + +describe('Issue #606: Data-Driven Point Lights & Authentic 1.13c Player Radius', () => { + const mockCollision: CollisionGrid = { + cellsX: 10, + cellsY: 10, + gridWidth: 50, + gridHeight: 50, + originX: 0, + originY: 0, + blocked: new Uint8Array(2500), + collisionMasks: new Uint16Array(2500), + } as unknown as CollisionGrid + + it('provides base 1.13c player light radius of 11 sub-tiles with pure white color', () => { + const lightGrid = new LightGrid() + let appliedLighting: any = null + const renderer = { + setLighting: (params: any) => { + appliedLighting = params + }, + } + + applyLightingInputs( + { + collisionGrid: mockCollision, + playerSubTile: { x: 50, y: 50 }, + pointLights: [], + preset: 'auto', + levelId: 1, + tick: 0, + environmentIntensity: 100, + }, + { lightGrid, renderer }, + ) + + expect(appliedLighting).not.toBeNull() + // Player center cell is (24, 24) + const centerIdx = 24 * LIGHT_GRID_SIZE + 24 + const intensity = lightGrid.intensity[centerIdx] + expect(intensity).toBeGreaterThan(0) + }) + + it('adds player light radius bonus correctly', () => { + const gridNoBonus = new LightGrid() + const gridWithBonus = new LightGrid() + const renderer = { setLighting: () => {} } + + applyLightingInputs( + { + collisionGrid: mockCollision, + playerSubTile: { x: 50, y: 50 }, + pointLights: [], + preset: 'auto', + levelId: 1, + tick: 0, + playerLightRadiusBonus: 0, + environmentIntensity: 0, + }, + { lightGrid: gridNoBonus, renderer }, + ) + + applyLightingInputs( + { + collisionGrid: mockCollision, + playerSubTile: { x: 50, y: 50 }, + pointLights: [], + preset: 'auto', + levelId: 1, + tick: 0, + playerLightRadiusBonus: 5, + environmentIntensity: 0, + }, + { lightGrid: gridWithBonus, renderer }, + ) + + // Check cells 2 cells away from player center (24, 24) -> (26, 24) + const targetIdx = 24 * LIGHT_GRID_SIZE + 26 + const intensityNoBonus = gridNoBonus.intensity[targetIdx]! + const intensityWithBonus = gridWithBonus.intensity[targetIdx]! + + expect(intensityWithBonus).toBeGreaterThanOrEqual(intensityNoBonus) + }) + + it('preserves true pitch-black darkness when ambient environment intensity is 0', () => { + const lightGrid = new LightGrid() + let appliedLighting: any = null + const renderer = { + setLighting: (params: any) => { + appliedLighting = params + }, + } + + applyLightingInputs( + { + collisionGrid: mockCollision, + playerSubTile: { x: 50, y: 50 }, + pointLights: [], + preset: 'auto', + levelId: 1, + tick: 0, + environmentIntensity: 0, + environmentColor: [20, 20, 20], + }, + { lightGrid, renderer }, + ) + + expect(appliedLighting.ambient[0]).toBe(0) + expect(appliedLighting.ambient[1]).toBe(0) + expect(appliedLighting.ambient[2]).toBe(0) + }) + + it('includes canonical 1.13c static tables for lit entities', () => { + expect(Object.keys(CANONICAL_MONSTER_LIGHTS).length).toBeGreaterThanOrEqual(80) + expect(Object.keys(CANONICAL_OBJECT_LIGHTS).length).toBeGreaterThanOrEqual(150) + expect(Object.keys(CANONICAL_MISSILE_LIGHTS).length).toBeGreaterThanOrEqual(100) + expect(Object.keys(CANONICAL_OVERLAY_LIGHTS).length).toBeGreaterThanOrEqual(40) + + // Andariel has a lit config in monstats2 + const andariel = getMonsterLightConfig('andariel') + expect(andariel).toBeDefined() + expect(andariel?.radius).toBeGreaterThan(0) + + // Mephisto has a lit config in monstats2 + const mephisto = getMonsterLightConfig('mephisto') + expect(mephisto).toBeDefined() + expect(mephisto?.radius).toBeGreaterThan(0) + + // Firebolt missile (missiles.txt id 58) has a lit config + const firebolt = getMissileLightConfig('firebolt') + expect(firebolt).toBeDefined() + expect(firebolt?.radius).toBe(7) + expect(firebolt?.r).toBe(255) + const fireboltById = getMissileLightConfig(58) + expect(fireboltById?.radius).toBe(7) + + // Torch / campfire object + const campfire = getObjectLightConfig(14) + expect(campfire).toBeDefined() + + // Overlay lookup + const overlay = getOverlayLightConfig(1) + expect(overlay).toBeDefined() + }) + + it('processes point lights with optional flicker into lightGrid', () => { + const lightGrid = new LightGrid() + const renderer = { setLighting: () => {} } + + applyLightingInputs( + { + collisionGrid: mockCollision, + playerSubTile: { x: 50, y: 50 }, + pointLights: [ + { + subTileX: 60, + subTileY: 60, + radius: 10, + intensity: 200, + r: 255, + g: 100, + b: 50, + flicker: 1, + }, + ], + preset: 'auto', + levelId: 1, + tick: 0, + environmentIntensity: 0, + }, + { lightGrid, renderer }, + ) + + const cellX = Math.floor((60 - lightGrid.originSubX) / 8) + const cellY = Math.floor((60 - lightGrid.originSubY) / 8) + const cellIdx = cellY * LIGHT_GRID_SIZE + cellX + expect(lightGrid.intensity[cellIdx]).toBeGreaterThan(0) + expect(lightGrid.red[cellIdx]).toBe(255) + }) +}) + +describe('Issue #614: Elevation-Aware Shaders & Linear Dynamic Light', () => { + it('uses authentic linear dynamic lighting without arbitrary non-linear curve in fragment shader', () => { + const source = fragmentShaderSource(8) + expect(source).toContain('vec3 dynamicLight = lightSample.rgb;') + expect(source).not.toContain('0.22 + 0.78 * lightSample.a') + }) +}) + +describe('Issue #621: Dead Code Cleanup & Canonical Direction Mapping', () => { + it('pointsToDccDirection maps positions to 0-63 DCC frames', () => { + // South in world coordinates (dx > 0, dy > 0): dir64 = 0 -> DCC-16 index 4 + const south = pointsToDccDirection(0, 0, 10, 10, 16) + expect(south).toBe(4) + + // Result is always a valid DCC direction within range + const anyDir = pointsToDccDirection(10, 20, 50, 40, 16) + expect(anyDir).toBeGreaterThanOrEqual(0) + expect(anyDir).toBeLessThan(16) + }) + + it('velocityToDccDirection maps velocity components to valid DCC frames', () => { + // South velocity (vx = 0, vy > 0): DCC-16 index 4 + const south = velocityToDccDirection(0, 10, 16) + expect(south).toBe(4) + + const east = velocityToDccDirection(10, 0, 16) + expect(east).toBeGreaterThanOrEqual(0) + expect(east).toBeLessThan(16) + }) +})