fix: 가오픈 빙의 후보 시간을 논리 시계에 맞춤

This commit is contained in:
2026-09-23 04:15:20 +00:00
parent b2b6723c4d
commit dee72bdf7b
2 changed files with 26 additions and 7 deletions
+6 -3
View File
@@ -5,7 +5,7 @@ import { gameBasePath, gameProfile, gameTrpcRoute } from './gameTestPaths.js';
const wall = new Date('2026-09-16T12:00:00Z'); const wall = new Date('2026-09-16T12:00:00Z');
const game = new Date('2026-09-16T11:50:00Z'); const game = new Date('2026-09-16T11:50:00Z');
type ClockCase = 'normal' | 'recovery' | 'recovery-end' | 'suspended'; type ClockCase = 'normal' | 'recovery' | 'recovery-end' | 'suspended' | 'preopen';
const install = async (page: Page, routeName: string, clockCase: ClockCase, displayMode: string) => { const install = async (page: Page, routeName: string, clockCase: ClockCase, displayMode: string) => {
await page.clock.install({ time: wall }); await page.clock.install({ time: wall });
await page.clock.setFixedTime(wall); await page.clock.setFixedTime(wall);
@@ -48,7 +48,9 @@ const install = async (page: Page, routeName: string, clockCase: ClockCase, disp
serverTime: game.toISOString(), serverTime: game.toISOString(),
serverWallTime: wall.toISOString(), serverWallTime: wall.toISOString(),
clockMode: 'realtime', clockMode: 'realtime',
clockRunning: clockCase !== 'suspended', clockRunning: clockCase !== 'suspended' && clockCase !== 'preopen',
clockStartsAt: clockCase === 'preopen' ? new Date(wall.getTime() + 600_000).toISOString() : null,
turnEngineRunning: clockCase === 'preopen' ? false : true,
clockRecovery: clockRecovery:
clockCase === 'recovery' || clockCase === 'recovery-end' clockCase === 'recovery' || clockCase === 'recovery-end'
? { ? {
@@ -170,8 +172,9 @@ const capture = async (page: Page, info: TestInfo, name: string) => {
await page.screenshot({ path: info.outputPath(`${name}.png`), fullPage: true }); await page.screenshot({ path: info.outputPath(`${name}.png`), fullPage: true });
}; };
for (const width of [1365, 390]) { for (const width of [1365, 390]) {
for (const clockCase of ['normal', 'recovery', 'recovery-end', 'suspended'] as const) { for (const clockCase of ['normal', 'recovery', 'recovery-end', 'suspended', 'preopen'] as const) {
for (const routeName of ['survey', 'select-general', 'join']) { for (const routeName of ['survey', 'select-general', 'join']) {
if (clockCase === 'preopen' && routeName !== 'join') continue;
test(`${routeName} GAME deadline ${clockCase} ${width}px`, async ({ page }, info) => { test(`${routeName} GAME deadline ${clockCase} ${width}px`, async ({ page }, info) => {
await page.setViewportSize({ width, height: 900 }); await page.setViewportSize({ width, height: 900 });
const calls = await install(page, routeName, clockCase, width === 390 ? 'real' : 'game'); const calls = await install(page, routeName, clockCase, width === 390 ? 'real' : 'game');
@@ -18,6 +18,7 @@ const mode = computed({
}, },
}); });
const sample = shallowRef<ReturnType<typeof sampleServerClock>>(null); const sample = shallowRef<ReturnType<typeof sampleServerClock>>(null);
const preopenLogicalSample = shallowRef<ReturnType<typeof sampleServerClock>>(null);
const now = ref(Date.now()); const now = ref(Date.now());
const engineRunning = ref<boolean | null>(null); const engineRunning = ref<boolean | null>(null);
const haltedAt = ref<number | null>(null); const haltedAt = ref<number | null>(null);
@@ -32,6 +33,9 @@ export const receiveClockSample = (
): void => { ): void => {
if (!input.serverTime) return; if (!input.serverTime) return;
const wallTime = input.serverWallTime ? new Date(input.serverWallTime).getTime() : undefined; const wallTime = input.serverWallTime ? new Date(input.serverWallTime).getTime() : undefined;
const startsAt = input.clockStartsAt ? new Date(input.clockStartsAt).getTime() : NaN;
const isPreopenSample =
input.clockMode === 'realtime' && wallTime !== undefined && Number.isFinite(startsAt) && wallTime < startsAt;
// 메인 화면의 이전 표본을 heartbeat가 재전달해도 더 최신 lobby 표본을 되감지 않는다. // 메인 화면의 이전 표본을 heartbeat가 재전달해도 더 최신 lobby 표본을 되감지 않는다.
if ( if (
wallTime !== undefined && wallTime !== undefined &&
@@ -44,14 +48,20 @@ export const receiveClockSample = (
wallTime !== undefined && wallTime !== undefined &&
wallTime === sample.value.serverWallTimeMs && wallTime === sample.value.serverWallTimeMs &&
new Date(input.serverTime).getTime() === sample.value.serverTimeMs && new Date(input.serverTime).getTime() === sample.value.serverTimeMs &&
input.turnEngineRunning === engineRunning.value input.turnEngineRunning === engineRunning.value &&
isPreopenSample === (preopenLogicalSample.value !== null)
) )
return; return;
const sampledAt = Date.now();
// PREOPEN's displayed clock is intentionally frozen. Its negative logical tick
// still advances, so candidate deadlines need the unpaused server sample.
preopenLogicalSample.value = isPreopenSample ? sampleServerClock(input, sampledAt) : null;
receiveClockEngineState(input.turnEngineRunning ?? null); receiveClockEngineState(input.turnEngineRunning ?? null);
sample.value = sampleServerClock( sample.value = sampleServerClock(
input.turnEngineRunning === false ? { ...input, clockRunning: false, clockStartsAt: null } : input input.turnEngineRunning === false ? { ...input, clockRunning: false, clockStartsAt: null } : input,
sampledAt
); );
now.value = Date.now(); now.value = sampledAt;
}; };
export const advanceClockDisplay = (): void => { export const advanceClockDisplay = (): void => {
@@ -64,7 +74,13 @@ const projection = computed(() =>
sample.value ? projectServerClock(sample.value, haltedAt.value ?? now.value) : null sample.value ? projectServerClock(sample.value, haltedAt.value ?? now.value) : null
); );
// 마감 판정은 표시 모드와 무관하게 서버가 투영한 GAME 시각을 사용한다. // 마감 판정은 표시 모드와 무관하게 서버가 투영한 GAME 시각을 사용한다.
const gameTime = computed(() => projection.value?.time ?? null); const gameTime = computed(() => {
const preopen = preopenLogicalSample.value;
if (preopen) {
return new Date(preopen.serverTimeMs + Math.max(0, now.value - preopen.sampledClientTimeMs));
}
return projection.value?.time ?? null;
});
const accelerated = computed(() => projection.value?.rate === 2 && engineRunning.value !== false); const accelerated = computed(() => projection.value?.rate === 2 && engineRunning.value !== false);
const label = computed(() => (mode.value === 'real' ? '실제 시간 기준' : '게임 시간 기준')); const label = computed(() => (mode.value === 'real' ? '실제 시간 기준' : '게임 시간 기준'));
const time = computed(() => { const time = computed(() => {