refactor: 턴 경계와 12턴 묶음을 보존하는 2배속 복구 구현

This commit is contained in:
2026-09-06 10:04:33 +00:00
parent 63da0921cb
commit d3443ec7f5
53 changed files with 1467 additions and 160 deletions
@@ -29,6 +29,7 @@ const props = defineProps<{
clockMode?: 'realtime' | 'manual';
clockRunning?: boolean;
clockStartsAt?: string | null;
clockRecovery?: { startsAt: string; endsAt: string } | null;
autorunLimit?: number | null;
storageKey?: string;
mapData?: CommandMapData | null;
@@ -60,11 +61,7 @@ const labelMap = computed(() => {
});
const firstReservedMonth = computed(
() =>
(props.currentYear ?? 0) * 12 +
(props.currentMonth ?? 1) -
1 +
(props.general?.nextTurnMonthOffset ?? 0)
() => (props.currentYear ?? 0) * 12 + (props.currentMonth ?? 1) - 1 + (props.general?.nextTurnMonthOffset ?? 0)
);
const rows = computed<ReservedCommandRow[]>(() => {
@@ -119,7 +116,7 @@ const updateServerClock = () => {
currentServerTime.value = '--:--:--';
return;
}
const { clientElapsedMs, time: projectedTime } = projectServerClock(serverClockSample);
const { clientElapsedMs, time: projectedTime, rate } = projectServerClock(serverClockSample);
currentServerTime.value = formatLocalTimeSeconds(projectedTime);
if (serverClockSample.clockMode !== 'manual' && serverClockSample.startDelayMs !== null) {
const untilStartMs = serverClockSample.startDelayMs - clientElapsedMs;
@@ -127,15 +124,30 @@ const updateServerClock = () => {
updateServerClock,
untilStartMs > 0
? Math.min(untilStartMs, MAX_SERVER_CLOCK_TIMER_DELAY_MS)
: 1_000 - projectedTime.getMilliseconds()
: (1_000 - projectedTime.getMilliseconds()) / rate
);
}
};
watch(
() => [props.serverTime, props.serverWallTime, props.clockMode, props.clockRunning, props.clockStartsAt] as const,
([serverTime, serverWallTime, clockMode, clockRunning, clockStartsAt]) => {
serverClockSample = sampleServerClock({ serverTime, serverWallTime, clockMode, clockRunning, clockStartsAt });
() =>
[
props.serverTime,
props.serverWallTime,
props.clockMode,
props.clockRunning,
props.clockStartsAt,
props.clockRecovery,
] as const,
([serverTime, serverWallTime, clockMode, clockRunning, clockStartsAt, clockRecovery]) => {
serverClockSample = sampleServerClock({
serverTime,
serverWallTime,
clockMode,
clockRunning,
clockStartsAt,
clockRecovery,
});
updateServerClock();
},
{ immediate: true }
@@ -16,6 +16,7 @@ const props = defineProps<{
clockMode?: 'realtime' | 'manual';
clockRunning?: boolean;
clockStartsAt?: string | null;
clockRecovery?: { startsAt: string; endsAt: string } | null;
turnEngineRunning?: boolean | null;
status: {
onlineUserCount: number;
@@ -34,6 +35,7 @@ const props = defineProps<{
const tournamentStatus = computed(() => resolveTournamentStageName(props.tournamentStage));
const currentServerTime = ref('기록 없음');
const hasServerClock = ref(false);
const recovering = ref(false);
const turnEngineStopped = computed(() => props.turnEngineRunning === false);
const turnEngineStatusUnknown = computed(() => typeof props.turnEngineRunning !== 'boolean');
const serverClockTitle = computed(() => {
@@ -57,6 +59,7 @@ const updateServerClock = () => {
const now = Date.now();
const projection = projectServerClock(serverClockSample, now);
recovering.value = projection.rate === 2;
currentServerTime.value = formatServerDateTime(projection.time, {
format: 'monthDayTime',
fallback: '기록 없음',
@@ -67,16 +70,37 @@ const updateServerClock = () => {
const nextDelays: number[] = [];
if (serverClockSample.clockMode !== 'manual' && serverClockSample.startDelayMs !== null) {
const untilStartMs = serverClockSample.startDelayMs - projection.clientElapsedMs;
nextDelays.push(untilStartMs > 0 ? untilStartMs : millisecondsUntilNextMinute(projection.time));
nextDelays.push(
untilStartMs > 0 ? untilStartMs : millisecondsUntilNextMinute(projection.time) / projection.rate
);
}
for (const boundary of [serverClockSample.recoveryStartDelayMs, serverClockSample.recoveryEndDelayMs]) {
if (boundary !== undefined && boundary > projection.clientElapsedMs)
nextDelays.push(boundary - projection.clientElapsedMs);
}
if (nextDelays.length === 0) return;
serverClockTimer = setTimeout(updateServerClock, Math.max(1, Math.min(...nextDelays)));
};
watch(
() => [props.serverTime, props.serverWallTime, props.clockMode, props.clockRunning, props.clockStartsAt] as const,
([serverTime, serverWallTime, clockMode, clockRunning, clockStartsAt]) => {
serverClockSample = sampleServerClock({ serverTime, serverWallTime, clockMode, clockRunning, clockStartsAt });
() =>
[
props.serverTime,
props.serverWallTime,
props.clockMode,
props.clockRunning,
props.clockStartsAt,
props.clockRecovery,
] as const,
([serverTime, serverWallTime, clockMode, clockRunning, clockStartsAt, clockRecovery]) => {
serverClockSample = sampleServerClock({
serverTime,
serverWallTime,
clockMode,
clockRunning,
clockStartsAt,
clockRecovery,
});
updateServerClock();
},
{ immediate: true }
@@ -100,7 +124,7 @@ onUnmounted(() => {
}"
:title="serverClockTitle"
>
현재 시각: {{ currentServerTime }}
현재 시각: {{ currentServerTime }}<span v-if="recovering"> · 복구 2배속</span>
</div>
<div class="status-row tournament-status">
<RouterLink to="/tournament">
@@ -4,6 +4,7 @@ export type ServerClockProjectionInput = {
clockMode?: 'realtime' | 'manual';
clockRunning?: boolean;
clockStartsAt?: string | null;
clockRecovery?: { startsAt: string; endsAt: string } | null;
};
export type SampledServerClock = {
@@ -11,6 +12,8 @@ export type SampledServerClock = {
sampledClientTimeMs: number;
clockMode: 'realtime' | 'manual';
startDelayMs: number | null;
recoveryStartDelayMs?: number;
recoveryEndDelayMs?: number;
};
const parseInstant = (value?: string | null): number | null => {
@@ -40,11 +43,20 @@ export const sampleServerClock = (
: null;
}
const wallSample = parseInstant(input.serverWallTime);
const recoveryStart = parseInstant(input.clockRecovery?.startsAt);
const recoveryEnd = parseInstant(input.clockRecovery?.endsAt);
return {
serverTimeMs,
sampledClientTimeMs,
clockMode: input.clockMode ?? 'realtime',
startDelayMs,
...(wallSample !== null && recoveryStart !== null && recoveryEnd !== null && recoveryEnd > recoveryStart
? {
recoveryStartDelayMs: recoveryStart - wallSample,
recoveryEndDelayMs: recoveryEnd - wallSample,
}
: {}),
};
};
@@ -55,9 +67,25 @@ export const projectServerClock = (sample: SampledServerClock, clientTimeMs = Da
? 0
: Math.max(0, clientElapsedMs - sample.startDelayMs);
const accelerationMs =
sample.startDelayMs === null || sample.clockMode === 'manual'
? 0
: Math.max(
0,
Math.min(clientElapsedMs, sample.recoveryEndDelayMs ?? 0) -
Math.max(0, sample.recoveryStartDelayMs ?? 0)
);
const rate =
sample.startDelayMs !== null &&
sample.clockMode !== 'manual' &&
clientElapsedMs >= (sample.recoveryStartDelayMs ?? Infinity) &&
clientElapsedMs < (sample.recoveryEndDelayMs ?? -Infinity)
? 2
: 1;
return {
clientElapsedMs,
time: new Date(sample.serverTimeMs + elapsedGameMs),
rate,
time: new Date(sample.serverTimeMs + elapsedGameMs + accelerationMs),
};
};
+3
View File
@@ -311,6 +311,7 @@ watch(
:clock-mode="lobbyInfo?.clockMode"
:clock-running="lobbyInfo?.clockRunning"
:clock-starts-at="lobbyInfo?.clockStartsAt"
:clock-recovery="lobbyInfo?.clockRecovery"
:turn-engine-running="lobbyInfo?.turnEngineRunning"
/>
</div>
@@ -365,6 +366,7 @@ watch(
:clock-mode="lobbyInfo?.clockMode"
:clock-running="lobbyInfo?.clockRunning"
:clock-starts-at="lobbyInfo?.clockStartsAt"
:clock-recovery="lobbyInfo?.clockRecovery"
:autorun-limit="reservedGeneralAutorunLimit"
:map-data="worldMap"
:map-layout="mapLayout"
@@ -547,6 +549,7 @@ watch(
:clock-mode="lobbyInfo?.clockMode"
:clock-running="lobbyInfo?.clockRunning"
:clock-starts-at="lobbyInfo?.clockStartsAt"
:clock-recovery="lobbyInfo?.clockRecovery"
:autorun-limit="reservedGeneralAutorunLimit"
:map-data="worldMap"
:map-layout="mapLayout"