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('