From 5079f364ec26af0e868aacce759d719ea36a0849 Mon Sep 17 00:00:00 2001
From: troytt <47798984@qq.com>
Date: Thu, 24 Sep 2026 10:20:18 +0000
Subject: [PATCH] feat(ui): add interactive scene lighting selector (#lighting)
after variant selector in acts.html
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add
+
I背包 · A角色 · T技能 · Q任务 · V小站 · ~腰带 · R奔跑 · W换手
正在加载地图…
diff --git a/src/scene/act-scene.ts b/src/scene/act-scene.ts
index f16e049..a6f2bf1 100644
--- a/src/scene/act-scene.ts
+++ b/src/scene/act-scene.ts
@@ -292,6 +292,8 @@ export interface ActSceneState {
culledDraws: number
/** Wall-clock milliseconds the last `onRender` took. */
renderMs: number
+ /** Active lighting preset id ('auto', 'noon', 'morning', 'sunset', 'night', 'cave', 'hell', 'eclipse', 'off'). */
+ lightingPreset?: LightingPreset | undefined
/** Active lighting mode ('Glide 1.13c' or undefined). */
lightingMode?: string | undefined
/** Ambient light intensity [0..255]. */
@@ -343,10 +345,98 @@ declare global {
__d2webActScene?: {
switchHero: (token: string) => Promise
getHero: () => string
+ switchLighting?: (preset: string) => LightingPreset
+ getLighting?: () => LightingPreset
}
}
}
+export type LightingPreset =
+ | 'auto'
+ | 'noon'
+ | 'morning'
+ | 'sunset'
+ | 'night'
+ | 'cave'
+ | 'hell'
+ | 'eclipse'
+ | 'off'
+
+export interface LightingPresetConfig {
+ readonly id: LightingPreset
+ readonly labelZh: string
+ readonly enabled: boolean
+ readonly overrideAmbient?: {
+ readonly intensity: number
+ readonly red: number
+ readonly green: number
+ readonly blue: number
+ }
+}
+
+export const LIGHTING_PRESETS: Readonly> = {
+ auto: {
+ id: 'auto',
+ labelZh: '自动 (原版 1.13c)',
+ enabled: true,
+ },
+ noon: {
+ id: 'noon',
+ labelZh: '正午 (明亮日光)',
+ enabled: true,
+ overrideAmbient: { intensity: 255, red: 255, green: 255, blue: 255 },
+ },
+ morning: {
+ id: 'morning',
+ labelZh: '清晨 (晨曦暖金)',
+ enabled: true,
+ overrideAmbient: { intensity: 190, red: 225, green: 198, blue: 145 },
+ },
+ sunset: {
+ id: 'sunset',
+ labelZh: '黄昏 (暮色紫金)',
+ enabled: true,
+ overrideAmbient: { intensity: 145, red: 210, green: 155, blue: 198 },
+ },
+ night: {
+ id: 'night',
+ labelZh: '午夜 (幽暗蓝月)',
+ enabled: true,
+ overrideAmbient: { intensity: 68, red: 125, green: 148, blue: 245 },
+ },
+ cave: {
+ id: 'cave',
+ labelZh: '深渊地牢 (火炬暗黑)',
+ enabled: true,
+ overrideAmbient: { intensity: 14, red: 225, green: 215, blue: 205 },
+ },
+ hell: {
+ id: 'hell',
+ labelZh: '地狱血月 (第四幕猩红)',
+ enabled: true,
+ overrideAmbient: { intensity: 105, red: 255, green: 68, blue: 58 },
+ },
+ eclipse: {
+ id: 'eclipse',
+ labelZh: '蚀日之咒 (幽蓝暗蚀)',
+ enabled: true,
+ overrideAmbient: { intensity: 52, red: 45, green: 85, blue: 245 },
+ },
+ off: {
+ id: 'off',
+ labelZh: '关闭光照 (全亮无光影)',
+ enabled: false,
+ },
+}
+
+export function normalizeLightingPreset(raw: string | null | undefined): LightingPreset {
+ const clean = (raw ?? '').trim().toLowerCase()
+ if (clean === '0' || clean === 'false' || clean === 'off' || clean === 'none') return 'off'
+ if (clean === '1' || clean === 'true' || clean === 'auto' || clean === '') return 'auto'
+ if (clean in LIGHTING_PRESETS) return clean as LightingPreset
+ return 'auto'
+}
+
export interface HeroClassInfo {
readonly token: string
readonly code: string
@@ -5287,6 +5377,7 @@ async function runScene(initialRuntime: MapRuntime, renderer: SpriteRenderer, st
const heroSelect = document.querySelector('#hero, #class')
const actSelect = document.querySelector('#act')!
const variantSelect = document.querySelector('#variant')!
+ const lightingSelect = document.querySelector('#lighting')
const input = new KeyboardInput()
input.attach()
@@ -5318,7 +5409,47 @@ async function runScene(initialRuntime: MapRuntime, renderer: SpriteRenderer, st
const playerAnimator = new ActorAnimator()
const lightGrid = new LightGrid()
const environment = new Environment()
- const lightingEnabled = param('lighting', '1') !== '0'
+ let currentLightingPreset: LightingPreset = normalizeLightingPreset(param('lighting', 'auto'))
+ let lightingEnabled = LIGHTING_PRESETS[currentLightingPreset].enabled
+ state.lightingPreset = currentLightingPreset
+ state.lightingMode = LIGHTING_PRESETS[currentLightingPreset].labelZh
+
+ const switchLighting = (preset: string): LightingPreset => {
+ currentLightingPreset = normalizeLightingPreset(preset)
+ const cfg = LIGHTING_PRESETS[currentLightingPreset]
+ lightingEnabled = cfg.enabled
+ state.lightingPreset = currentLightingPreset
+ state.lightingMode = cfg.labelZh
+ if (lightingSelect !== null && lightingSelect.value !== currentLightingPreset) {
+ lightingSelect.value = currentLightingPreset
+ }
+ try {
+ const url = new URL(location.href)
+ if (currentLightingPreset === 'auto') {
+ url.searchParams.delete('lighting')
+ } else {
+ url.searchParams.set('lighting', currentLightingPreset)
+ }
+ history.replaceState(null, '', url.toString())
+ } catch {
+ // ignore in non-browser context
+ }
+ status.textContent = `已切换场景光照为:${cfg.labelZh}`
+ setTimeout(() => {
+ if (status.textContent.startsWith('已切换场景光照为:')) {
+ status.textContent = ''
+ }
+ }, 2500)
+ return currentLightingPreset
+ }
+
+ if (lightingSelect !== null) {
+ lightingSelect.disabled = false
+ lightingSelect.value = currentLightingPreset
+ lightingSelect.onchange = () => {
+ switchLighting(lightingSelect.value)
+ }
+ }
const switchHero = async (newHeroToken: string): Promise => {
const nextToken = normalizeHeroToken(newHeroToken)
@@ -5383,8 +5514,11 @@ async function runScene(initialRuntime: MapRuntime, renderer: SpriteRenderer, st
window.__d2webActScene = {
switchHero,
getHero: () => currentHeroToken,
+ switchLighting,
+ getLighting: () => currentLightingPreset,
}
;(state as any).switchHero = switchHero
+ ;(state as any).switchLighting = switchLighting
}
const monsterAnimators = new Map()
const camera = new ViewportCamera(canvas, runtime.widthPx, runtime.heightPx)
@@ -5764,7 +5898,10 @@ if (typeof window !== 'undefined') {
}
if (next.source === 'pack') {
- history.replaceState(null, '', `?act=${String(next.act)}&level=${encodeURIComponent(next.variant)}`)
+ const nextParams = new URLSearchParams(location.search)
+ nextParams.set('act', String(next.act))
+ nextParams.set('level', next.variant)
+ history.replaceState(null, '', `?${nextParams.toString()}`)
void populatePackSelectors(next.base, { act: next.act, label: next.variant })
}
@@ -6301,14 +6438,18 @@ if (typeof window !== 'undefined') {
}
}
- // Maintain a subtle warm-stone minimum ambient baseline in zero-intensity indoor caves
- // so distant cave walls retain authentic silhouette depth against the [0, 0, 0] clear color.
- const rawIntensity = environment.currentIntensity
+ // Resolve ambient from active lighting preset override or 1.13c Environment engine
+ const presetCfg = LIGHTING_PRESETS[currentLightingPreset]
+ const overrideAmb = presetCfg.overrideAmbient
+ const rawIntensity = overrideAmb !== undefined ? overrideAmb.intensity : environment.currentIntensity
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 ambR = ((rawIntensity > 0 ? environment.currentRed : 225) / 255) * ambNorm
- const ambG = ((rawIntensity > 0 ? environment.currentGreen : 215) / 255) * ambNorm
- const ambB = ((rawIntensity > 0 ? environment.currentBlue : 205) / 255) * ambNorm
+ const ambR = (effR / 255) * ambNorm
+ const ambG = (effG / 255) * ambNorm
+ const ambB = (effB / 255) * ambNorm
renderer.setLighting({
ambient: [ambR, ambG, ambB],
@@ -6319,11 +6460,16 @@ if (typeof window !== 'undefined') {
lightmap: lightGrid.exportTextureBuffer(),
})
- state.lightingMode = 'Glide 1.13c'
- state.ambientIntensity = environment.currentIntensity
- state.ambientColor = [environment.currentRed, environment.currentGreen, environment.currentBlue]
+ state.lightingPreset = currentLightingPreset
+ state.lightingMode = presetCfg.labelZh
+ state.ambientIntensity = effectiveAmbIntensity
+ state.ambientColor = [effR, effG, effB]
} else {
renderer.setLighting(null)
+ state.lightingPreset = currentLightingPreset
+ state.lightingMode = LIGHTING_PRESETS[currentLightingPreset].labelZh
+ state.ambientIntensity = 255
+ state.ambientColor = [255, 255, 255]
}
skippedDraws = 0
let culledDraws = 0
@@ -7268,6 +7414,20 @@ async function boot(): Promise {
const actSelect = document.querySelector('#act')!
const sceneSelect = document.querySelector('#scene')!
const variantSelect = document.querySelector('#variant')!
+ const lightingSelect = document.querySelector('#lighting')
+ if (lightingSelect !== null) {
+ lightingSelect.disabled = false
+ lightingSelect.value = normalizeLightingPreset(param('lighting', 'auto'))
+ lightingSelect.onchange = () => {
+ const params = new URLSearchParams(location.search)
+ if (lightingSelect.value === 'auto') {
+ params.delete('lighting')
+ } else {
+ params.set('lighting', lightingSelect.value)
+ }
+ location.search = params.toString()
+ }
+ }
if (heroSelect !== null) {
heroSelect.disabled = false
heroSelect.value = normalizeHeroToken(param('hero', param('class', 'so')))
diff --git a/tests/scene-lighting.test.ts b/tests/scene-lighting.test.ts
index 303d01d..9e88e1a 100644
--- a/tests/scene-lighting.test.ts
+++ b/tests/scene-lighting.test.ts
@@ -381,4 +381,29 @@ describe('Scene Lighting & Environment Integration', () => {
const frontAlpha = buf[wallFrontIdx * 4 + 3]
expect(frontAlpha).toBe(grid.intensity[wallFrontIdx])
})
+
+ it('supports interactive scene lighting presets and #lighting selector in acts.html', async () => {
+ const { LIGHTING_PRESETS, normalizeLightingPreset } = await import('../src/scene/act-scene.ts')
+ const fs = await import('node:fs')
+
+ expect(normalizeLightingPreset(undefined)).toBe('auto')
+ expect(normalizeLightingPreset('1')).toBe('auto')
+ expect(normalizeLightingPreset('0')).toBe('off')
+ expect(normalizeLightingPreset('off')).toBe('off')
+ expect(normalizeLightingPreset('noon')).toBe('noon')
+ expect(normalizeLightingPreset('night')).toBe('night')
+ expect(normalizeLightingPreset('cave')).toBe('cave')
+ expect(normalizeLightingPreset('hell')).toBe('hell')
+ expect(normalizeLightingPreset('eclipse')).toBe('eclipse')
+
+ expect(LIGHTING_PRESETS.noon.overrideAmbient?.intensity).toBe(255)
+ expect(LIGHTING_PRESETS.cave.overrideAmbient?.intensity).toBe(14)
+ expect(LIGHTING_PRESETS.off.enabled).toBe(false)
+
+ const actsHtml = fs.readFileSync('acts.html', 'utf8')
+ expect(actsHtml).toContain('