import crypto from 'node:crypto'; import type { EffectsEvent, EffectsState, EffectToolState } from '../../shared/types'; function nowMs(): number { return Date.now(); } function defaultTool(): EffectToolState { return { tool: 'fog', radiusN: 0.08, intensity: 0.6 }; } export class EffectsStore { private state: EffectsState = { revision: 1, serverNowMs: nowMs(), tool: defaultTool(), instances: [], }; getState(): EffectsState { // Всегда обновляем serverNowMs при чтении — это наш "таймкод" для рендереров. return { ...this.state, serverNowMs: nowMs() }; } clear(): EffectsState { this.state = { ...this.state, revision: this.state.revision + 1, serverNowMs: nowMs(), instances: [], }; return this.state; } dispatch(event: EffectsEvent): EffectsState { const s = this.state; const next: EffectsState = applyEvent(s, event); this.state = next; return next; } /** Удаляет истёкшие (по lifetime) эффекты, чтобы state не разрастался бесконечно. */ pruneExpired(): boolean { const now = nowMs(); const before = this.state.instances.length; const kept = this.state.instances.filter((i) => { // Пятно льда не истекает по таймеру (только «очистить все» или ластик в UI). if (i.type === 'ice') return true; if (i.type === 'lightning' || i.type === 'sunbeam' || i.type === 'poisonCloud') { return now - i.createdAtMs < i.lifetimeMs; } if (i.type === 'scorch') { return now - i.createdAtMs < i.lifetimeMs; } if (i.type === 'fog' || i.type === 'water') { if (i.lifetimeMs === null) return true; return now - i.createdAtMs < i.lifetimeMs; } return true; }); if (kept.length === before) return false; this.state = { ...this.state, revision: this.state.revision + 1, serverNowMs: now, instances: kept, }; return true; } makeId(prefix: string): string { return `${prefix}_${crypto.randomBytes(6).toString('hex')}_${String(nowMs())}`; } } function applyEvent(state: EffectsState, event: EffectsEvent): EffectsState { const bump = (patch: Omit): EffectsState => ({ ...patch, revision: state.revision + 1, serverNowMs: nowMs(), }); switch (event.kind) { case 'tool.set': return bump({ ...state, tool: event.tool }); case 'instances.clear': return bump({ ...state, instances: [] }); case 'instance.add': return bump({ ...state, instances: [...state.instances, event.instance] }); case 'instance.remove': return bump({ ...state, instances: state.instances.filter((i) => i.id !== event.id) }); default: { // Exhaustiveness // eslint-disable-next-line @typescript-eslint/no-unused-vars const _x: never = event; return state; } } }