Files
DndGamePlayer/app/main/sessionTimer/timerSessionStore.ts
T
Ivan Fontosh 877fbaa8e7 feat: save games and edit scene darkness
Persist show state to local saves; author map darkness in scene editor with bake-to-scene and help updates.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 11:41:42 +08:00

148 lines
4.5 KiB
TypeScript

import {
clampSessionTimerDurationMs,
clampSessionTimerLayout,
createInitialSessionTimerState,
getSessionTimerRemainingMs,
type SessionTimerEvent,
type SessionTimerState,
} from '../../shared/types/sessionTimer';
export class TimerSessionStore {
private state: SessionTimerState = createInitialSessionTimerState();
getState(): SessionTimerState {
return this.state;
}
clear(): SessionTimerState {
this.state = createInitialSessionTimerState();
return this.state;
}
dispatch(event: SessionTimerEvent, now = Date.now()): SessionTimerState {
switch (event.kind) {
case 'setDuration': {
const durationMs = clampSessionTimerDurationMs(event.durationMs);
if (this.state.running) {
const remaining = getSessionTimerRemainingMs(this.state, now);
this.state = {
...this.state,
revision: this.state.revision + 1,
durationMs,
remainingMs: Math.min(remaining, durationMs),
endsAt: now + Math.min(remaining, durationMs),
};
} else {
this.state = {
...this.state,
revision: this.state.revision + 1,
durationMs,
remainingMs: durationMs,
endsAt: null,
running: false,
};
}
return this.state;
}
case 'start': {
const remaining = getSessionTimerRemainingMs(this.state, now);
if (remaining <= 0) {
// Restart from full duration if already at zero.
const durationMs = this.state.durationMs;
this.state = {
...this.state,
revision: this.state.revision + 1,
remainingMs: durationMs,
running: true,
endsAt: now + durationMs,
};
return this.state;
}
this.state = {
...this.state,
revision: this.state.revision + 1,
running: true,
endsAt: now + remaining,
remainingMs: remaining,
};
return this.state;
}
case 'pause': {
if (!this.state.running) return this.state;
const remaining = getSessionTimerRemainingMs(this.state, now);
this.state = {
...this.state,
revision: this.state.revision + 1,
running: false,
endsAt: null,
remainingMs: remaining,
};
return this.state;
}
case 'reset': {
this.state = {
...this.state,
revision: this.state.revision + 1,
running: false,
endsAt: null,
remainingMs: this.state.durationMs,
};
return this.state;
}
case 'setVisibleToPlayers': {
if (this.state.visibleToPlayers === event.visible) return this.state;
this.state = {
...this.state,
revision: this.state.revision + 1,
visibleToPlayers: event.visible,
};
return this.state;
}
case 'layout.set': {
const layout = clampSessionTimerLayout(event.layout);
this.state = {
...this.state,
revision: this.state.revision + 1,
layout,
};
return this.state;
}
default: {
const _exhaustive: never = event;
return _exhaustive;
}
}
}
/** Когда running и время вышло — зафиксировать 00:00 (вызывается из UI/main по желанию). */
settleIfExpired(now = Date.now()): SessionTimerState {
if (!this.state.running) return this.state;
const remaining = getSessionTimerRemainingMs(this.state, now);
if (remaining > 0) return this.state;
this.state = {
...this.state,
revision: this.state.revision + 1,
running: false,
endsAt: null,
remainingMs: 0,
};
return this.state;
}
/** Слепок: таймер на паузе, без абсолютного момента конца. */
loadPaused(next: Pick<SessionTimerState, 'durationMs' | 'remainingMs' | 'visibleToPlayers' | 'layout'>): SessionTimerState {
const durationMs = clampSessionTimerDurationMs(next.durationMs);
const remainingMs = Math.max(0, Math.min(durationMs, Math.round(next.remainingMs)));
this.state = {
revision: this.state.revision + 1,
durationMs,
remainingMs,
running: false,
endsAt: null,
visibleToPlayers: Boolean(next.visibleToPlayers),
layout: clampSessionTimerLayout(next.layout),
};
return this.state;
}
}