Fix data-driven point lights, elevation-aware shaders, authentic palette rows, and dead code cleanup (Closes #606, #614, #621)

This commit is contained in:
troytt 2026-10-02 21:22:31 +00:00
parent 8b619c23c3
commit 56f4f7f4fe
7 changed files with 1199 additions and 73 deletions

View File

@ -33,36 +33,8 @@ export interface MissileMeta {
readonly groups: readonly (readonly MissileFrameTuple[])[] readonly groups: readonly (readonly MissileFrameTuple[])[]
} }
/** import { dir64ToDcc } from '../../common/units/character.ts'
* The engine's 64-direction space -> DCC direction tables. import { dir64FromPoints } from '../../common/units/direction.ts'
* 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. * Calculate DCC direction index for a missile based on 2D velocity vector.
@ -75,15 +47,29 @@ const DIR64_TO_DCC: Readonly<Record<number, readonly number[]>> = {
*/ */
export function velocityToDccDirection(vx: number, vy: number, directions = 16): number { export function velocityToDccDirection(vx: number, vy: number, directions = 16): number {
if (directions <= 1) return 0 if (directions <= 1) return 0
if (vx === 0 && vy === 0) return 0
const angle = Math.atan2(vy, vx) // -PI to PI const angle = Math.atan2(vy, vx) // -PI to PI
// Map angle so that South (PI/2) is 0, turning clockwise // Map angle so that South (PI/2) is 0, turning clockwise
const fraction = (angle - Math.PI / 2) / (2 * Math.PI) const fraction = (angle - Math.PI / 2) / (2 * Math.PI)
const dir64 = Math.round((((fraction % 1) + 1) % 1) * 64) % 64 const dir64 = Math.round((((fraction % 1) + 1) % 1) * 64) % 64
const table = DIR64_TO_DCC[directions] return dir64ToDcc(dir64, directions)
if (table !== undefined) { }
return table[dir64] ?? 0
} /**
return 0 * 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 = { export const FIREBOLT_META: MissileMeta = {

View File

@ -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 handle = renderer.addIndexedAtlas(atlas.r8, atlas.width, atlas.height)
const groups = atlas.groups.slice(0, baseCount) const groups = atlas.groups.slice(0, baseCount)

View File

@ -33,6 +33,8 @@
* a flush per colour. * a flush per colour.
*/ */
import type { AtlasFrame, SpriteAtlas } from './atlas.ts' 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. */ /** Camera state for one frame. */
export interface Camera { export interface Camera {
@ -94,6 +96,20 @@ export interface DrawOptions {
readonly elevationY?: number 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. */ /** Dynamic scene lighting configuration for Glide-style illumination. */
export interface LightingConfig { export interface LightingConfig {
/** Ambient light color in range 0..1, e.g. [0.03, 0.03, 0.04]. */ /** Ambient light color in range 0..1, e.g. [0.03, 0.03, 0.04]. */
@ -233,7 +249,7 @@ ${cases.join('\n')}
${paletteLookup}${discardAlpha} ${paletteLookup}${discardAlpha}
if (u_lightMode == 1 && u_blendMode == 0) { if (u_lightMode == 1 && u_blendMode == 0) {
vec4 lightSample = texture(u_lightmap, v_lightUv); 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); vec3 totalLight = clamp(u_ambient.rgb + dynamicLight, 0.0, 1.0);
outColor = vec4(texel.rgb * totalLight * v_tint.rgb, texel.a * v_tint.a); outColor = vec4(texel.rgb * totalLight * v_tint.rgb, texel.a * v_tint.a);
} else { } else {
@ -809,22 +825,68 @@ export class SpriteRenderer {
/** /**
* Upload the 256-colour Act palette and precompute hardware status tint rows: * Upload the 256-colour Act palette and precompute hardware status tint rows:
* - Row 0: Normal Act Palette * - Row 0: Normal Act Palette
* - Row 1: Cold / Chilled Blue Tint * - Row 1: Cold / Chilled Blue Tint (authentic blueTones transform when available)
* - Row 2: Poisoned Green Tint * - Row 2: Poisoned Green Tint (authentic greenTones transform when available)
* - Row 3: Unique Gold Boss Tint * - Row 3: Unique Gold Boss Tint (authentic Utrans / hueVariations transform when available)
* - Row 4: Champion / Elite Blue Tint * - 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 if (this.disposed) return
this.flush() this.flush()
const gl = this.gl 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) const rgba = new Uint8Array(256 * PALETTE_ROWS * 4)
for (let i = 1; i < 256; i += 1) { for (let i = 1; i < 256; i += 1) {
const r = rgb[i * 3] ?? 0 const r = baseRgb[i * 3] ?? 0
const g = rgb[i * 3 + 1] ?? 0 const g = baseRgb[i * 3 + 1] ?? 0
const b = rgb[i * 3 + 2] ?? 0 const b = baseRgb[i * 3 + 2] ?? 0
const lum = Math.round(0.299 * r + 0.587 * g + 0.114 * b) const lum = Math.round(0.299 * r + 0.587 * g + 0.114 * b)
// Row 0: Normal Act Palette // Row 0: Normal Act Palette
@ -836,30 +898,58 @@ export class SpriteRenderer {
// Row 1: Cold / Chilled Icy Blue // Row 1: Cold / Chilled Icy Blue
const r1 = (256 + i) * 4 const r1 = (256 + i) * 4
rgba[r1] = Math.min(255, Math.round(r * 0.35)) if (coldXform) {
rgba[r1 + 1] = Math.min(255, Math.round(g * 0.65 + 40)) const targetIdx = (coldXform[i] ?? i) * 3
rgba[r1 + 2] = Math.min(255, Math.round(b * 0.9 + 100)) 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 rgba[r1 + 3] = 255
// Row 2: Poisoned Venom Green // Row 2: Poisoned Venom Green
const r2 = (256 * 2 + i) * 4 const r2 = (256 * 2 + i) * 4
rgba[r2] = Math.min(255, Math.round(r * 0.35)) if (poisonXform) {
rgba[r2 + 1] = Math.min(255, Math.round(g * 0.85 + 85)) const targetIdx = (poisonXform[i] ?? i) * 3
rgba[r2 + 2] = Math.min(255, Math.round(b * 0.35)) 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 rgba[r2 + 3] = 255
// Row 3: Unique Boss Golden Aura // Row 3: Unique Boss Golden Aura
const r3 = (256 * 3 + i) * 4 const r3 = (256 * 3 + i) * 4
rgba[r3] = Math.min(255, Math.round(lum * 1.25 + 55)) if (uniqueXform) {
rgba[r3 + 1] = Math.min(255, Math.round(lum * 0.95 + 25)) const targetIdx = (uniqueXform[i] ?? i) * 3
rgba[r3 + 2] = Math.min(255, Math.round(lum * 0.35)) 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 rgba[r3 + 3] = 255
// Row 4: Champion / Elite Royal Blue // Row 4: Champion / Elite Royal Blue
const r4 = (256 * 4 + i) * 4 const r4 = (256 * 4 + i) * 4
rgba[r4] = Math.min(255, Math.round(lum * 0.45 + 30)) if (champXform) {
rgba[r4 + 1] = Math.min(255, Math.round(lum * 0.6 + 45)) const targetIdx = (champXform[i] ?? i) * 3
rgba[r4 + 2] = Math.min(255, Math.round(lum * 1.3 + 75)) 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 rgba[r4 + 3] = 255
} }
gl.bindTexture(gl.TEXTURE_2D, this.paletteTexture) gl.bindTexture(gl.TEXTURE_2D, this.paletteTexture)
@ -1065,6 +1155,8 @@ export class SpriteRenderer {
const tint = options.tint const tint = options.tint
const flipped = options.flipX === true const flipped = options.flipX === true
const paletteRow = page.indexed === true ? (options.paletteRow ?? 0) : -1 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 drawW = options.width ?? frame.width
const drawH = options.height ?? frame.height const drawH = options.height ?? frame.height
this.quad( this.quad(
@ -1080,6 +1172,7 @@ export class SpriteRenderer {
tint === undefined ? 1 : tint[3], tint === undefined ? 1 : tint[3],
unit, unit,
paletteRow, paletteRow,
topPalette,
) )
} }

View File

@ -187,6 +187,7 @@ export interface PointLightInput {
readonly r: number readonly r: number
readonly g: number readonly g: number
readonly b: number readonly b: number
readonly flicker?: number | undefined
} }
/** /**
@ -242,18 +243,16 @@ export function applyLightingInputs(inputs: LightingInputs, targets: LightingPas
const overrideAmb = presetCfg.overrideAmbient const overrideAmb = presetCfg.overrideAmbient
const envIntensity = inputs.environmentIntensity ?? environment?.currentIntensity ?? 255 const envIntensity = inputs.environmentIntensity ?? environment?.currentIntensity ?? 255
const rawIntensity = overrideAmb !== undefined ? overrideAmb.intensity : envIntensity 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 = 11 + (inputs.playerLightRadiusBonus ?? 0)
const basePlayerRadius = (isDarkCave ? 20 : 22) + (inputs.playerLightRadiusBonus ?? 0)
lightGrid.addLight({ lightGrid.addLight({
subTileX: pSub.x, subTileX: pSub.x,
subTileY: pSub.y, subTileY: pSub.y,
radius: Math.max(8, basePlayerRadius + flicker), radius: Math.max(1, basePlayerRadius),
intensity: 242, intensity: 255,
red: 255, red: 255,
green: 234, green: 255,
blue: 196, blue: 255,
}) })
for (const pl of inputs.pointLights) { for (const pl of inputs.pointLights) {
@ -265,19 +264,19 @@ export function applyLightingInputs(inputs: LightingInputs, targets: LightingPas
red: pl.r, red: pl.r,
green: pl.g, green: pl.g,
blue: pl.b, blue: pl.b,
...(pl.flicker !== undefined ? { flicker: pl.flicker } : {}),
}) })
} }
const effectiveAmbIntensity = rawIntensity > 0 ? rawIntensity : 5
const envColor = inputs.environmentColor ?? [ const envColor = inputs.environmentColor ?? [
environment?.currentRed ?? 255, environment?.currentRed ?? 255,
environment?.currentGreen ?? 255, environment?.currentGreen ?? 255,
environment?.currentBlue ?? 255, environment?.currentBlue ?? 255,
] ]
const effR = overrideAmb !== undefined ? overrideAmb.red : (rawIntensity > 0 ? envColor[0] : 185) const effR = overrideAmb !== undefined ? overrideAmb.red : envColor[0]
const effG = overrideAmb !== undefined ? overrideAmb.green : (rawIntensity > 0 ? envColor[1] : 175) const effG = overrideAmb !== undefined ? overrideAmb.green : envColor[1]
const effB = overrideAmb !== undefined ? overrideAmb.blue : (rawIntensity > 0 ? envColor[2] : 165) const effB = overrideAmb !== undefined ? overrideAmb.blue : envColor[2]
const ambNorm = effectiveAmbIntensity / 255 const ambNorm = rawIntensity / 255
const ambR = (effR / 255) * ambNorm const ambR = (effR / 255) * ambNorm
const ambG = (effG / 255) * ambNorm const ambG = (effG / 255) * ambNorm
const ambB = (effB / 255) * ambNorm const ambB = (effB / 255) * ambNorm
@ -291,3 +290,822 @@ export function applyLightingInputs(inputs: LightingInputs, targets: LightingPas
lightmap: lightGrid.exportTextureBuffer(), 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<Record<string, readonly [radius: number, r: number, g: number, b: number]>> = 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<Record<number, readonly [lits: readonly number[], r: number, g: number, b: number, flicker: number]>> = 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<Record<string, readonly [id: number, radius: number, r: number, g: number, b: number, flicker: number]>> = 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<Record<number, readonly [radius: number, initRadius: number, r: number, g: number, b: number]>> = 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<number, readonly [id: number, radius: number, r: number, g: number, b: number, flicker: number]> = (() => {
const map = new Map<number, readonly [id: number, radius: number, r: number, g: number, b: number, flicker: number]>()
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],
}
}

View File

@ -7,7 +7,6 @@ import {
import type { AtlasFrame } from '../render/atlas.ts' import type { AtlasFrame } from '../render/atlas.ts'
import { import {
LightningTrailManager, LightningTrailManager,
drawElectricBoltFallback,
} from '../render/missile-renderer.ts' } from '../render/missile-renderer.ts'
import { import {
MISSILE_METAS, MISSILE_METAS,
@ -29,7 +28,6 @@ export {
LightningTrailManager, LightningTrailManager,
MISSILE_METAS, MISSILE_METAS,
OVERLAY_METAS, OVERLAY_METAS,
drawElectricBoltFallback,
getMissileTxtData, getMissileTxtData,
velocityToDccDirection, velocityToDccDirection,
type MissileFrameTuple, type MissileFrameTuple,

View File

@ -65,7 +65,7 @@ export interface LightSource {
/** Blue channel [0..255]. */ /** Blue channel [0..255]. */
readonly blue: number readonly blue: number
/** Optional flicker perturbation flag or offset. */ /** Optional flicker perturbation flag or offset. */
readonly flicker?: number readonly flicker?: number | undefined
} }
/** /**

View File

@ -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)
})
})