Files
ChronicleOfTheImmortalClan/src/renderer/ui/fx.ts
T
thzxx ff6df8054c refactor(0.1.14-P1): 内核归位——game/ 按引擎架构重排(零行为漂移)
结构(旧 game/core+engine → 新 engine/ 域):
- engine/kernel/  时钟/随机/插件协议/fxqueue/timesense/format/urgency/guide/names(原 core)
- engine/narrative/  legacy/报告/列传/谱系/年轴(原 core 叙事族)
- engine/runtime/   World/creation/pcgen/ApiFacade/capabilities/pluginManager/boot/clocks + Systems/*(12 系统)
- engine/sim/     Market(未来 WorldSim 同行)
- 旧 game/core、engine/systems、engine/world.ts 等路径全部废弃(无 re-export 兼容层)

验证:35 套件/967 测试全绿(金钟罩三档零漂移=纯搬迁无行为变化)
typecheck 0 error
2026-08-23 13:23:25 +08:00

96 lines
2.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { FxGate, FxKind } from '../game/engine/kernel/fxqueue'
import { seasonOf } from '../game/data/season'
let gate: FxGate | null = null
export function fx(): FxGate | null {
return gate
}
export function ensureFx(opts?: { onEmit?: (kind: FxKind, payload?: Record<string, number | string>) => void }): FxGate {
if (gate) return gate
gate = new FxGate({
emit: (e) => {
switch (e.kind) {
case 'mist': {
window.dispatchEvent(new CustomEvent('fx:mist'))
break
}
case 'spark': {
window.dispatchEvent(new CustomEvent('fx:spark'))
break
}
case 'ripple': {
window.dispatchEvent(new CustomEvent('fx:ripple', { detail: e.payload ?? {} }))
break
}
case 'blade': {
window.dispatchEvent(new CustomEvent('fx:blade'))
break
}
case 'seasonShift': {
window.dispatchEvent(new CustomEvent('fx:season', { detail: e.payload ?? {} }))
break
}
case 'drift': {
window.dispatchEvent(new CustomEvent('fx:drift', { detail: e.payload ?? {} }))
break
}
case 'pulse': {
window.dispatchEvent(new CustomEvent('fx:pulse', { detail: e.payload ?? {} }))
break
}
}
opts?.onEmit?.(e.kind, e.payload)
}
})
if (typeof window !== 'undefined' && typeof window.matchMedia === 'function') {
const mq = window.matchMedia('(prefers-reduced-motion: reduce)')
gate.setReduced(mq.matches)
mq.addEventListener('change', (ev) => gate?.setReduced(ev.matches))
}
return gate
}
/** store.advance 后调用:检测季节变化 → 季节脉动 + 季节色温 CSS 变量 */
export function fxSeason(prevMonth: number, nextMonth: number): void {
const g = ensureFx()
const prev = seasonOf(prevMonth)
const next = seasonOf(nextMonth)
if (prev !== next) {
g.emit('seasonShift', { season: next, accent: labelOf(next) })
}
applySeason(next)
}
function labelOf(s: string): string {
return { spring: '春', summer: '夏', autumn: '秋', winter: '冬' }[s] ?? s
}
export function seasonTintOf(month: number): string {
const s = seasonOf(month)
return labelOf(s)
}
/** 直接应用季节色温 CSS 变量(单源 data-season */
export function applySeason(season: string): void {
if (typeof document === 'undefined') return
document.documentElement.setAttribute('data-season', season)
}
/** 慢帧保险:document.hidden 时自动停 particledetail 在 canvas 端监听) */
export function fxVisibilityAutopause(): () => void {
const onVis = () => {
const g = ensureFx()
if (document.hidden) {
// hidden 时禁止未来发射
g.setReduced(true)
} else {
const mq = window.matchMedia('(prefers-reduced-motion: reduce)')
g.setReduced(mq.matches)
}
}
document.addEventListener('visibilitychange', onVis)
return () => document.removeEventListener('visibilitychange', onVis)
}