feat(ui): add interactive scene lighting selector (#lighting) after variant selector in acts.html
- Add <select id="lighting"> in acts.html right after #variant with 9 lighting modes: * auto: 自动 (原版 1.13c) * noon: 正午 (明亮日光) * morning: 清晨 (晨曦暖金) * sunset: 黄昏 (暮色紫金) * night: 午夜 (幽暗蓝月) * cave: 深渊地牢 (火炬暗黑) * hell: 地狱血月 (第四幕猩红) * eclipse: 蚀日之咒 (幽蓝暗蚀) * off: 关闭光照 (全亮无光影) - Wire real-time zero-reload switching via switchLighting() in act-scene.ts and sync URL ?lighting= query param
This commit is contained in:
parent
cef09dbd81
commit
5079f364ec
11
acts.html
11
acts.html
|
|
@ -57,6 +57,17 @@
|
|||
</select></label>
|
||||
<label>场景 <select id="scene" disabled><option>正在读取场景列表…</option></select></label>
|
||||
<label>细分场景 <select id="variant" disabled></select></label>
|
||||
<label>光照 <select id="lighting">
|
||||
<option value="auto">自动 (原版 1.13c)</option>
|
||||
<option value="noon">正午 (明亮日光)</option>
|
||||
<option value="morning">清晨 (晨曦暖金)</option>
|
||||
<option value="sunset">黄昏 (暮色紫金)</option>
|
||||
<option value="night">午夜 (幽暗蓝月)</option>
|
||||
<option value="cave">深渊地牢 (火炬暗黑)</option>
|
||||
<option value="hell">地狱血月 (第四幕猩红)</option>
|
||||
<option value="eclipse">蚀日之咒 (幽蓝暗蚀)</option>
|
||||
<option value="off">关闭光照 (全亮无光影)</option>
|
||||
</select></label>
|
||||
<span class="hint" style="margin:0">I背包 · A角色 · T技能 · Q任务 · V小站 · ~腰带 · R奔跑 · W换手</span>
|
||||
</div>
|
||||
<div id="status" class="overlay">正在加载地图…</div>
|
||||
|
|
|
|||
|
|
@ -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<boolean>
|
||||
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<Record<LightingPreset, LightingPresetConfig>> = {
|
||||
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<HTMLSelectElement>('#hero, #class')
|
||||
const actSelect = document.querySelector<HTMLSelectElement>('#act')!
|
||||
const variantSelect = document.querySelector<HTMLSelectElement>('#variant')!
|
||||
const lightingSelect = document.querySelector<HTMLSelectElement>('#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<boolean> => {
|
||||
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<number, ActorAnimator>()
|
||||
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<void> {
|
|||
const actSelect = document.querySelector<HTMLSelectElement>('#act')!
|
||||
const sceneSelect = document.querySelector<HTMLSelectElement>('#scene')!
|
||||
const variantSelect = document.querySelector<HTMLSelectElement>('#variant')!
|
||||
const lightingSelect = document.querySelector<HTMLSelectElement>('#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')))
|
||||
|
|
|
|||
|
|
@ -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('<select id="lighting">')
|
||||
for (const preset of Object.keys(LIGHTING_PRESETS)) {
|
||||
expect(actsHtml).toContain(`value="${preset}"`)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue