结构(旧 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
96 lines
2.9 KiB
TypeScript
96 lines
2.9 KiB
TypeScript
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 时自动停 particle(detail 在 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)
|
||
}
|