Merge branch 'fix/cave-darkness-tuning'

This commit is contained in:
troytt 2026-09-24 10:46:43 +00:00
commit a19ded4c7b
4 changed files with 63 additions and 31 deletions

View File

@ -233,8 +233,8 @@ export class LightGrid {
const cellCenterSubX = cx * SUB_TILES_PER_LIGHT_CELL + (SUB_TILES_PER_LIGHT_CELL >> 1) const cellCenterSubX = cx * SUB_TILES_PER_LIGHT_CELL + (SUB_TILES_PER_LIGHT_CELL >> 1)
const dSubX = cellCenterSubX - relSubX const dSubX = cellCenterSubX - relSubX
// Fast Euclidean distance approximation // Fast Euclidean distance approximation (emitter's own cell receives full peak intensity at dist = 0)
const dist = fastDistanceApprox(dSubX, dSubY) const dist = (cx === centerCellX && cy === centerCellY) ? 0 : fastDistanceApprox(dSubX, dSubY)
if (dist >= effectiveRadius) continue if (dist >= effectiveRadius) continue
// Linear falloff: ((R - dist) * IntensitySlope) >> 16 // Linear falloff: ((R - dist) * IntensitySlope) >> 16

View File

@ -233,7 +233,8 @@ ${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 totalLight = clamp(u_ambient.rgb + lightSample.rgb, 0.0, 1.0); vec3 dynamicLight = lightSample.rgb * (0.22 + 0.78 * lightSample.a);
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 {
outColor = texel * v_tint; outColor = texel * v_tint;

View File

@ -407,25 +407,25 @@ export const LIGHTING_PRESETS: Readonly<Record<LightingPreset, LightingPresetCon
id: 'night', id: 'night',
labelZh: '午夜 (幽暗蓝月)', labelZh: '午夜 (幽暗蓝月)',
enabled: true, enabled: true,
overrideAmbient: { intensity: 68, red: 125, green: 148, blue: 245 }, overrideAmbient: { intensity: 38, red: 115, green: 140, blue: 240 },
}, },
cave: { cave: {
id: 'cave', id: 'cave',
labelZh: '深渊地牢 (火炬暗黑)', labelZh: '深渊地牢 (火炬暗黑)',
enabled: true, enabled: true,
overrideAmbient: { intensity: 14, red: 225, green: 215, blue: 205 }, overrideAmbient: { intensity: 5, red: 185, green: 175, blue: 165 },
}, },
hell: { hell: {
id: 'hell', id: 'hell',
labelZh: '地狱血月 (第四幕猩红)', labelZh: '地狱血月 (第四幕猩红)',
enabled: true, enabled: true,
overrideAmbient: { intensity: 105, red: 255, green: 68, blue: 58 }, overrideAmbient: { intensity: 72, red: 255, green: 62, blue: 52 },
}, },
eclipse: { eclipse: {
id: 'eclipse', id: 'eclipse',
labelZh: '蚀日之咒 (幽蓝暗蚀)', labelZh: '蚀日之咒 (幽蓝暗蚀)',
enabled: true, enabled: true,
overrideAmbient: { intensity: 52, red: 45, green: 85, blue: 245 }, overrideAmbient: { intensity: 28, red: 45, green: 85, blue: 245 },
}, },
off: { off: {
id: 'off', id: 'off',
@ -6432,16 +6432,24 @@ if (typeof window !== 'undefined') {
lightGrid.clear() lightGrid.clear()
lightGrid.updateObstacles(runtime.grid) lightGrid.updateObstacles(runtime.grid)
// Authentic 1.13c player torch light: base radius 11 cells * 8 sub-tiles = 88 sub-tiles // Resolve ambient from active lighting preset override or 1.13c Environment engine
const flicker = (Math.sin(engine.world.tick * 0.35) * 2) | 0 const presetCfg = LIGHTING_PRESETS[currentLightingPreset]
const overrideAmb = presetCfg.overrideAmbient
const rawIntensity = overrideAmb !== undefined ? overrideAmb.intensity : environment.currentIntensity
const isDarkCave = rawIntensity <= 10 || currentLightingPreset === 'cave'
// Authentic 1.13c player torch light in isometric sub-tile space:
// 20-22 sub-tiles = ~240-260px horizontal screen radius (~500px diameter pool around hero)
const flicker = (Math.sin(engine.world.tick * 0.35) * 1.5) | 0
const basePlayerRadius = isDarkCave ? 20 : 22
lightGrid.addLight({ lightGrid.addLight({
subTileX: pSub.x, subTileX: pSub.x,
subTileY: pSub.y, subTileY: pSub.y,
radius: 88 + flicker, radius: basePlayerRadius + flicker,
intensity: 255, intensity: 242,
red: 255, red: 255,
green: 246, green: 234,
blue: 228, blue: 196,
}) })
// Active missile lights (with skillId fallback for all spells including Glacial Spike '55') // Active missile lights (with skillId fallback for all spells including Glacial Spike '55')
@ -6474,8 +6482,8 @@ if (typeof window !== 'undefined') {
lightGrid.addLight({ lightGrid.addLight({
subTileX: shotSubX, subTileX: shotSubX,
subTileY: shotSubY, subTileY: shotSubY,
radius: Math.max(48, lightUnits * 8), radius: Math.max(14, lightUnits * 2),
intensity: 255, intensity: 250,
red: isCold ? Math.max(120, mData.red) : mData.red, red: isCold ? Math.max(120, mData.red) : mData.red,
green: isCold ? Math.max(175, mData.green) : mData.green, green: isCold ? Math.max(175, mData.green) : mData.green,
blue: mData.blue, blue: mData.blue,
@ -6495,7 +6503,7 @@ if (typeof window !== 'undefined') {
lightGrid.addLight({ lightGrid.addLight({
subTileX: expSubX, subTileX: expSubX,
subTileY: expSubY, subTileY: expSubY,
radius: lightUnits * 8, radius: Math.round(lightUnits * 2.5),
intensity: 255, intensity: 255,
red: isColdExp ? 130 : (expData?.red ?? 255), red: isColdExp ? 130 : (expData?.red ?? 255),
green: isColdExp ? 185 : (expData?.green ?? 160), green: isColdExp ? 185 : (expData?.green ?? 160),
@ -6522,34 +6530,30 @@ if (typeof window !== 'undefined') {
if (isTorchOrFire || isWaypoint || isShrineOrBeam) { if (isTorchOrFire || isWaypoint || isShrineOrBeam) {
const dx = obj.x - engine.world.player.x const dx = obj.x - engine.world.player.x
const dy = obj.y - engine.world.player.y const dy = obj.y - engine.world.player.y
if (dx * dx + dy * dy < 1500 * 1500) { if (dx * dx + dy * dy < 950 * 950) {
const odx = (obj.x - runtime.grid.originX) / ORTHO_SUB_TILE_WIDTH const odx = (obj.x - runtime.grid.originX) / ORTHO_SUB_TILE_WIDTH
const ody = (obj.y - runtime.grid.originY) / ORTHO_SUB_TILE_HEIGHT const ody = (obj.y - runtime.grid.originY) / ORTHO_SUB_TILE_HEIGHT
const objSubX = Math.round((ody + odx) / 2) const objSubX = Math.round((ody + odx) / 2)
const objSubY = Math.round((ody - odx) / 2) const objSubY = Math.round((ody - odx) / 2)
const objFlicker = isTorchOrFire ? ((Math.sin(engine.world.tick * 0.45 + obj.x * 0.05) * 2) | 0) : 0 const objFlicker = isTorchOrFire ? ((Math.sin(engine.world.tick * 0.45 + obj.x * 0.05) * 1.2) | 0) : 0
lightGrid.addLight({ lightGrid.addLight({
subTileX: objSubX, subTileX: objSubX,
subTileY: objSubY, subTileY: objSubY,
radius: (isTorchOrFire ? 56 : isWaypoint ? 52 : 44) + objFlicker, radius: (isTorchOrFire ? 13 : isWaypoint ? 14 : 11) + objFlicker,
intensity: isTorchOrFire ? 235 : 210, intensity: isTorchOrFire ? 175 : 160,
red: isWaypoint ? 120 : 255, red: isWaypoint ? 120 : 255,
green: isWaypoint ? 150 : (isTorchOrFire ? 220 : 210), green: isWaypoint ? 150 : (isTorchOrFire ? 195 : 205),
blue: isWaypoint ? 255 : (isTorchOrFire ? 150 : 180), blue: isWaypoint ? 255 : (isTorchOrFire ? 115 : 175),
}) })
} }
} }
} }
} }
// Resolve ambient from active lighting preset override or 1.13c Environment engine const effectiveAmbIntensity = rawIntensity > 0 ? rawIntensity : 5
const presetCfg = LIGHTING_PRESETS[currentLightingPreset] const effR = overrideAmb !== undefined ? overrideAmb.red : (rawIntensity > 0 ? environment.currentRed : 185)
const overrideAmb = presetCfg.overrideAmbient const effG = overrideAmb !== undefined ? overrideAmb.green : (rawIntensity > 0 ? environment.currentGreen : 175)
const rawIntensity = overrideAmb !== undefined ? overrideAmb.intensity : environment.currentIntensity const effB = overrideAmb !== undefined ? overrideAmb.blue : (rawIntensity > 0 ? environment.currentBlue : 165)
const effectiveAmbIntensity = rawIntensity > 0 ? rawIntensity : 14
const effR = overrideAmb !== undefined ? overrideAmb.red : (rawIntensity > 0 ? environment.currentRed : 225)
const effG = overrideAmb !== undefined ? overrideAmb.green : (rawIntensity > 0 ? environment.currentGreen : 215)
const effB = overrideAmb !== undefined ? overrideAmb.blue : (rawIntensity > 0 ? environment.currentBlue : 205)
const ambNorm = effectiveAmbIntensity / 255 const ambNorm = effectiveAmbIntensity / 255
const ambR = (effR / 255) * ambNorm const ambR = (effR / 255) * ambNorm
const ambG = (effG / 255) * ambNorm const ambG = (effG / 255) * ambNorm

View File

@ -397,7 +397,7 @@ describe('Scene Lighting & Environment Integration', () => {
expect(normalizeLightingPreset('eclipse')).toBe('eclipse') expect(normalizeLightingPreset('eclipse')).toBe('eclipse')
expect(LIGHTING_PRESETS.noon.overrideAmbient?.intensity).toBe(255) expect(LIGHTING_PRESETS.noon.overrideAmbient?.intensity).toBe(255)
expect(LIGHTING_PRESETS.cave.overrideAmbient?.intensity).toBe(14) expect(LIGHTING_PRESETS.cave.overrideAmbient?.intensity).toBe(5)
expect(LIGHTING_PRESETS.off.enabled).toBe(false) expect(LIGHTING_PRESETS.off.enabled).toBe(false)
const actsHtml = fs.readFileSync('acts.html', 'utf8') const actsHtml = fs.readFileSync('acts.html', 'utf8')
@ -405,5 +405,32 @@ describe('Scene Lighting & Environment Integration', () => {
for (const preset of Object.keys(LIGHTING_PRESETS)) { for (const preset of Object.keys(LIGHTING_PRESETS)) {
expect(actsHtml).toContain(`value="${preset}"`) expect(actsHtml).toContain(`value="${preset}"`)
} }
}, 15000)
it('keeps cave player torch (radius=20 sub-tiles) and wall torches (radius=13 sub-tiles) localized without saturating outer viewport cells', () => {
const grid = new LightGrid()
grid.centerOnSubTile(200, 200)
// Add calibrated cave player torch at center (200, 200), radius=20 sub-tiles (~240px horizontal radius)
grid.addLight({
subTileX: 200,
subTileY: 200,
radius: 20,
intensity: 242,
red: 255,
green: 234,
blue: 196,
})
const centerIdx = 24 * LIGHT_GRID_SIZE + 24
// 1 cell away (dx=+1, dy=-1 => 8 sub-tiles each => dist=11, 160px horizontally): illuminated
const nearIdx = 23 * LIGHT_GRID_SIZE + 25
// 3 cells away (dx=+3, dy=-3 => 24 sub-tiles each => dist=32, 480px horizontally, well within 640px half-screen): completely dark!
const outerScreenIdx = 21 * LIGHT_GRID_SIZE + 27
expect(grid.intensity[centerIdx]).toBeGreaterThan(220)
expect(grid.intensity[nearIdx]).toBeGreaterThan(80)
expect(grid.intensity[outerScreenIdx]).toBe(0)
}) })
}) })