중단 후 대기와 2배속 복구 경계 및 전체 공지 적용
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { asGameTick, GAME_TICKS_PER_TURN, type GameTick } from './gameTimeUnits.js';
|
||||
|
||||
/** 정상 시간표는 바꾸지 않고, 정수 턴의 지연만 두 배 속도로 소진한다. */
|
||||
/** 정상 시간표를 유지하며 대기 후 두 배 속도로 월 경계에 합류한다. */
|
||||
export interface TurnRecoveryWindow {
|
||||
startTick: GameTick;
|
||||
endTick: GameTick;
|
||||
@@ -68,9 +68,12 @@ export const turnShiftTicks = (turns: number): GameTick => {
|
||||
return asGameTick(turns * GAME_TICKS_PER_TURN);
|
||||
};
|
||||
|
||||
/** 즉시 처리 여부는 12턴 묶음 생략 전의 전체 지연으로 판정한다. */
|
||||
export const immediateRecoveryLimitSeconds = (turnSeconds: number): number => Math.min(600, turnSeconds / 10);
|
||||
|
||||
/**
|
||||
* observedTick은 중단 전에 저장한 관측 지점, normalTick은 기존 시간표의 현재 지점이다.
|
||||
* 잔여 한 턴 미만은 정상 실행하고, 다음 경계부터 정수 턴 지연을 두 배속으로 처리한다.
|
||||
* 짧은 전체 지연만 즉시 처리한다. 그 외에는 나머지도 생략하지 않고 대기 후 두 배속으로 처리한다.
|
||||
* 반환한 skip은 호출자가 미래 일정과 실행 cursor에 원자적으로 적용해야 한다.
|
||||
*/
|
||||
export const planTurnRecovery = (input: {
|
||||
@@ -86,26 +89,32 @@ export const planTurnRecovery = (input: {
|
||||
throw new Error('Recovery requires a representable positive turn length.');
|
||||
}
|
||||
if (!Number.isFinite(wallNow.getTime())) throw new Error('Recovery wall instant is invalid.');
|
||||
const overdueTurns = Math.max(0, Math.floor((normalTick - observedTick) / GAME_TICKS_PER_TURN));
|
||||
const skippedTurns = Math.floor(overdueTurns / 12) * 12;
|
||||
const recoveryTurns = overdueTurns % 12;
|
||||
const initialTick = asGameTick(
|
||||
Math.max(observedTick + turnShiftTicks(skippedTurns), normalTick - turnShiftTicks(recoveryTurns))
|
||||
);
|
||||
if (recoveryTurns === 0) return { skippedTurns, recoveryTurns, initialTick, recovery: null };
|
||||
const boundary = nextTurnBoundary(normalTick);
|
||||
const startWallAt = new Date(
|
||||
wallNow.getTime() + Math.ceil(((boundary - normalTick) * turnSeconds * 1_000) / GAME_TICKS_PER_TURN)
|
||||
);
|
||||
const gap = Math.max(0, normalTick - observedTick);
|
||||
const ticksPerSecond = GAME_TICKS_PER_TURN / turnSeconds;
|
||||
const immediateLimit = immediateRecoveryLimitSeconds(turnSeconds) * ticksPerSecond;
|
||||
if (gap < immediateLimit) {
|
||||
return {
|
||||
skippedTurns: 0,
|
||||
recoveryTurns: 0,
|
||||
initialTick: asGameTick(Math.max(observedTick, normalTick)),
|
||||
recovery: null,
|
||||
};
|
||||
}
|
||||
const skippedTurns = Math.floor(gap / (12 * GAME_TICKS_PER_TURN)) * 12;
|
||||
const initialTick = asGameTick(observedTick + turnShiftTicks(skippedTurns));
|
||||
const remaining = normalTick - initialTick;
|
||||
if (remaining === 0) return { skippedTurns, recoveryTurns: 0, initialTick, recovery: null };
|
||||
|
||||
// 즉시 2배속으로 따라잡을 수 있는 가장 이른 지점 이후의 월 경계를 고른다.
|
||||
// 대기도 추가 지연이므로 경계까지 여유의 절반만 기다린다. 밀리초 반올림은 종료 시각을 보존한다.
|
||||
const endTick = nextTurnBoundary(normalTick + remaining);
|
||||
const endWallMs = wallNow.getTime() + Math.ceil(((endTick - normalTick) * 1_000) / ticksPerSecond);
|
||||
const durationMs = Math.ceil(((endTick - initialTick) * 1_000) / (2 * ticksPerSecond));
|
||||
return {
|
||||
skippedTurns,
|
||||
recoveryTurns,
|
||||
recoveryTurns: remaining / GAME_TICKS_PER_TURN,
|
||||
initialTick,
|
||||
recovery: {
|
||||
startTick: asGameTick(boundary - turnShiftTicks(recoveryTurns)),
|
||||
endTick: asGameTick(boundary + turnShiftTicks(recoveryTurns)),
|
||||
startWallAt,
|
||||
},
|
||||
recovery: { startTick: initialTick, endTick, startWallAt: new Date(endWallMs - durationMs) },
|
||||
};
|
||||
};
|
||||
|
||||
@@ -115,23 +124,32 @@ export const validateTurnRecovery = (window: TurnRecoveryWindow): void => {
|
||||
const span = window.endTick - window.startTick;
|
||||
if (
|
||||
!Number.isFinite(window.startWallAt.getTime()) ||
|
||||
window.startTick % GAME_TICKS_PER_TURN !== 0 ||
|
||||
window.endTick % GAME_TICKS_PER_TURN !== 0 ||
|
||||
span <= 0 ||
|
||||
span % (2 * GAME_TICKS_PER_TURN) !== 0 ||
|
||||
span >= 24 * GAME_TICKS_PER_TURN
|
||||
span >= 25 * GAME_TICKS_PER_TURN
|
||||
)
|
||||
throw new Error('Recovery must join turn boundaries after one to eleven turns at double speed.');
|
||||
throw new Error('Recovery must end at a turn boundary with a positive span below twenty-five turns.');
|
||||
};
|
||||
|
||||
/** 경계 전에는 정상 속도, 복구 구간은 두 배, 합류 경계 이후는 정상 속도이다. */
|
||||
export const observeTurnRecovery = (window: TurnRecoveryWindow, wallNow: Date, ticksPerSecond: number): GameTick => {
|
||||
validateTurnRecovery(window);
|
||||
const elapsed = asGameTick(
|
||||
Math.trunc(((wallNow.getTime() - window.startWallAt.getTime()) * ticksPerSecond) / 1_000)
|
||||
Math.trunc(
|
||||
((wallNow.getTime() - window.startWallAt.getTime()) *
|
||||
ticksPerSecond *
|
||||
(wallNow < window.startWallAt ? 1 : 2)) /
|
||||
1_000
|
||||
)
|
||||
);
|
||||
const halfSpan = (window.endTick - window.startTick) / 2;
|
||||
return asGameTick(window.startTick + elapsed + Math.max(0, Math.min(elapsed, halfSpan)));
|
||||
const endWallAt = projectRecoveryDeadline(window, window.endTick, ticksPerSecond);
|
||||
if (wallNow >= endWallAt) {
|
||||
return asGameTick(
|
||||
window.endTick + Math.trunc(((wallNow.getTime() - endWallAt.getTime()) * ticksPerSecond) / 1_000)
|
||||
);
|
||||
}
|
||||
// 이전 복구 창은 시작 전 1배속이었다. 새 창은 GameClock의 저장 tick 하한으로 대기한다.
|
||||
return asGameTick(Math.min(window.endTick, window.startTick + elapsed));
|
||||
};
|
||||
|
||||
/** 게임 좌표의 예정 시각을 사용자에게 표시할 실제 실행 시각으로 투영한다. */
|
||||
@@ -140,6 +158,10 @@ export const projectRecoveryDeadline = (window: TurnRecoveryWindow, tick: number
|
||||
asGameTick(tick);
|
||||
const offset = tick - window.startTick;
|
||||
const span = window.endTick - window.startTick;
|
||||
const elapsed = offset < 0 ? offset : offset <= span ? offset / 2 : offset - span / 2;
|
||||
if (offset > span) {
|
||||
const endWallMs = window.startWallAt.getTime() + Math.ceil((span * 1_000) / (2 * ticksPerSecond));
|
||||
return new Date(endWallMs + Math.ceil(((offset - span) * 1_000) / ticksPerSecond));
|
||||
}
|
||||
const elapsed = offset < 0 ? offset : offset / 2;
|
||||
return new Date(window.startWallAt.getTime() + Math.ceil((elapsed * 1_000) / ticksPerSecond));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user