232 lines
7.1 KiB
TypeScript
232 lines
7.1 KiB
TypeScript
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)
|
|
})
|
|
})
|