완료 턴 시각 대신 lobby serverTime을 분 경계에 맞춰 표시한다. 게임 API 응답과 SSE 및 같은 계정 탭 전달을 최근 통신으로 기록하고, 45초 공백에는 시계를 멈춘다. 공통 시계 투영과 단위 및 Chromium 회귀 검증을 추가한다.
68 lines
2.1 KiB
TypeScript
68 lines
2.1 KiB
TypeScript
export type ServerClockProjectionInput = {
|
|
serverTime?: string;
|
|
serverWallTime?: string;
|
|
clockMode?: 'realtime' | 'manual';
|
|
clockRunning?: boolean;
|
|
clockStartsAt?: string | null;
|
|
};
|
|
|
|
export type SampledServerClock = {
|
|
serverTimeMs: number;
|
|
sampledClientTimeMs: number;
|
|
clockMode: 'realtime' | 'manual';
|
|
startDelayMs: number | null;
|
|
};
|
|
|
|
const parseInstant = (value?: string | null): number | null => {
|
|
if (!value) return null;
|
|
const parsed = new Date(value).getTime();
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
};
|
|
|
|
export const sampleServerClock = (
|
|
input: ServerClockProjectionInput,
|
|
sampledClientTimeMs = Date.now()
|
|
): SampledServerClock | null => {
|
|
const serverTimeMs = parseInstant(input.serverTime);
|
|
if (serverTimeMs === null) return null;
|
|
|
|
let startDelayMs: number | null;
|
|
if (input.clockMode === 'manual') {
|
|
startDelayMs = null;
|
|
} else if (input.clockRunning !== false) {
|
|
startDelayMs = 0;
|
|
} else {
|
|
const serverWallTimeMs = parseInstant(input.serverWallTime);
|
|
const clockStartsAtMs = parseInstant(input.clockStartsAt);
|
|
startDelayMs =
|
|
serverWallTimeMs !== null && clockStartsAtMs !== null
|
|
? Math.max(0, clockStartsAtMs - serverWallTimeMs)
|
|
: null;
|
|
}
|
|
|
|
return {
|
|
serverTimeMs,
|
|
sampledClientTimeMs,
|
|
clockMode: input.clockMode ?? 'realtime',
|
|
startDelayMs,
|
|
};
|
|
};
|
|
|
|
export const projectServerClock = (sample: SampledServerClock, clientTimeMs = Date.now()) => {
|
|
const clientElapsedMs = Math.max(0, clientTimeMs - sample.sampledClientTimeMs);
|
|
const elapsedGameMs =
|
|
sample.clockMode === 'manual' || sample.startDelayMs === null
|
|
? 0
|
|
: Math.max(0, clientElapsedMs - sample.startDelayMs);
|
|
|
|
return {
|
|
clientElapsedMs,
|
|
time: new Date(sample.serverTimeMs + elapsedGameMs),
|
|
};
|
|
};
|
|
|
|
export const millisecondsUntilNextMinute = (time: Date): number => {
|
|
const remainder = ((time.getTime() % 60_000) + 60_000) % 60_000;
|
|
return remainder === 0 ? 60_000 : 60_000 - remainder;
|
|
};
|