fix: 개인 턴 경계에 맞춰 입력기 연월을 표시한다
Ref처럼 장수와 월 실행 시각을 logical tick bucket으로 비교해 이미 실행된 장수만 다음 달부터 예약 턴을 표시한다. 12월 연도 전환과 Date-only fallback을 단위 테스트 및 production Chromium으로 검증한다.
This commit is contained in:
@@ -45,6 +45,7 @@ import {
|
||||
import {
|
||||
resolveGeneralTypeCall,
|
||||
resolveLeadershipBonus,
|
||||
resolveNextTurnMonthOffset,
|
||||
resolveRefreshScoreText,
|
||||
resolveRemainingMinutes,
|
||||
} from '../../services/generalBasicCardProjection.js';
|
||||
@@ -265,6 +266,7 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
|
||||
dedication: true,
|
||||
age: true,
|
||||
turnTime: true,
|
||||
turnTick: true,
|
||||
recentWarTime: true,
|
||||
crewTypeId: true,
|
||||
personalCode: true,
|
||||
@@ -333,7 +335,14 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
|
||||
})
|
||||
: Promise.resolve(NEUTRAL_NATION_CONTEXT),
|
||||
ctx.db.worldState.findFirst({
|
||||
select: { currentYear: true, currentMonth: true, tickSeconds: true, config: true, meta: true },
|
||||
select: {
|
||||
currentYear: true,
|
||||
currentMonth: true,
|
||||
tickSeconds: true,
|
||||
lastTurnTick: true,
|
||||
config: true,
|
||||
meta: true,
|
||||
},
|
||||
}),
|
||||
officerCityId > 0
|
||||
? ctx.db.city.findUnique({ where: { id: officerCityId }, select: { name: true } })
|
||||
@@ -520,6 +529,13 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
|
||||
age: general.age,
|
||||
retirementYear,
|
||||
turnTime: general.turnTime.toISOString(),
|
||||
nextTurnMonthOffset: resolveNextTurnMonthOffset({
|
||||
turnTime: general.turnTime,
|
||||
turnTick: general.turnTick,
|
||||
lastExecuted: parsedLastExecuted,
|
||||
lastTurnTick: worldState?.lastTurnTick,
|
||||
turnSeconds: worldState?.tickSeconds ?? 0,
|
||||
}),
|
||||
recentWar: general.recentWarTime?.toISOString() ?? null,
|
||||
defenceTrain: settings.defence_train,
|
||||
killTurn: readNumber(metaRecord.killturn ?? metaRecord.killTurn, 0),
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { GAME_TICKS_PER_TURN } from '@sammo-ts/common';
|
||||
|
||||
export interface GeneralBasicStats {
|
||||
leadership: number;
|
||||
strength: number;
|
||||
@@ -53,3 +55,41 @@ export const resolveRemainingMinutes = (
|
||||
}
|
||||
return Math.floor(Math.min(999, Math.max(0, (nextTurnMillis - lastExecuted.getTime()) / 60_000)));
|
||||
};
|
||||
|
||||
export interface NextTurnMonthOffsetInput {
|
||||
turnTime: Date;
|
||||
turnTick?: bigint | number | null;
|
||||
lastExecuted: Date | null;
|
||||
lastTurnTick?: bigint | number | null;
|
||||
turnSeconds: number;
|
||||
}
|
||||
|
||||
const normalizeTick = (value: bigint | number | null | undefined): bigint | null => {
|
||||
if (typeof value === 'bigint') return value;
|
||||
if (typeof value === 'number' && Number.isSafeInteger(value)) return BigInt(value);
|
||||
return null;
|
||||
};
|
||||
|
||||
const turnBucket = (tick: bigint): bigint => {
|
||||
const ticksPerTurn = BigInt(GAME_TICKS_PER_TURN);
|
||||
const quotient = tick / ticksPerTurn;
|
||||
return tick % ticksPerTurn < 0 ? quotient - 1n : quotient;
|
||||
};
|
||||
|
||||
/**
|
||||
* Ref Command.GetReservedCommand cuts both clocks to a gameplay-turn bucket.
|
||||
* A general in the next bucket has already acted in the displayed world month,
|
||||
* so the first reserved command belongs to the following month.
|
||||
*/
|
||||
export const resolveNextTurnMonthOffset = (input: NextTurnMonthOffsetInput): 0 | 1 => {
|
||||
const turnTick = normalizeTick(input.turnTick);
|
||||
const lastTurnTick = normalizeTick(input.lastTurnTick);
|
||||
if (turnTick !== null && lastTurnTick !== null) {
|
||||
return turnBucket(turnTick) > turnBucket(lastTurnTick) ? 1 : 0;
|
||||
}
|
||||
|
||||
const turnTimeMs = input.turnTime.getTime();
|
||||
const lastExecutedMs = input.lastExecuted?.getTime() ?? Number.NaN;
|
||||
if (!Number.isFinite(turnTimeMs) || !Number.isFinite(lastExecutedMs) || input.turnSeconds <= 0) return 0;
|
||||
return turnTimeMs >= lastExecutedMs + input.turnSeconds * 1_000 ? 1 : 0;
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
resolveGeneralTypeCall,
|
||||
resolveLeadershipBonus,
|
||||
resolveNextTurnMonthOffset,
|
||||
resolveRefreshScoreText,
|
||||
resolveRemainingMinutes,
|
||||
} from '../src/services/generalBasicCardProjection.js';
|
||||
@@ -41,4 +42,55 @@ describe('general basic card Ref projection', () => {
|
||||
expect(resolveRemainingMinutes(new Date('2026-08-12T23:59:00.000Z'), lastExecuted, 3_600)).toBe(59);
|
||||
expect(resolveRemainingMinutes(new Date('2026-08-13T00:07:06.000Z'), null, 3_600)).toBeNull();
|
||||
});
|
||||
|
||||
it('moves the first reserved month only after the general turn bucket has passed', () => {
|
||||
const lastExecuted = new Date('2026-08-13T00:00:00.000Z');
|
||||
const lastTurnTick = 36_000_000n * 11n;
|
||||
|
||||
expect(
|
||||
resolveNextTurnMonthOffset({
|
||||
turnTime: new Date('2026-08-13T00:07:00.000Z'),
|
||||
turnTick: lastTurnTick + 12_000_000n,
|
||||
lastExecuted,
|
||||
lastTurnTick,
|
||||
turnSeconds: 600,
|
||||
})
|
||||
).toBe(0);
|
||||
expect(
|
||||
resolveNextTurnMonthOffset({
|
||||
turnTime: new Date('2026-08-13T00:17:00.000Z'),
|
||||
turnTick: lastTurnTick + 48_000_000n,
|
||||
lastExecuted,
|
||||
lastTurnTick,
|
||||
turnSeconds: 600,
|
||||
})
|
||||
).toBe(1);
|
||||
expect(
|
||||
resolveNextTurnMonthOffset({
|
||||
turnTime: new Date('2026-08-13T00:10:00.000Z'),
|
||||
turnTick: 1n,
|
||||
lastExecuted,
|
||||
lastTurnTick: -1n,
|
||||
turnSeconds: 600,
|
||||
})
|
||||
).toBe(1);
|
||||
});
|
||||
|
||||
it('keeps the same boundary for legacy Date-only schedules', () => {
|
||||
const lastExecuted = new Date('2026-08-13T00:00:00.000Z');
|
||||
expect(
|
||||
resolveNextTurnMonthOffset({
|
||||
turnTime: new Date('2026-08-13T00:07:00.000Z'),
|
||||
lastExecuted,
|
||||
turnSeconds: 600,
|
||||
})
|
||||
).toBe(0);
|
||||
expect(
|
||||
resolveNextTurnMonthOffset({
|
||||
turnTime: new Date('2026-08-13T00:10:00.000Z'),
|
||||
lastExecuted,
|
||||
turnSeconds: 600,
|
||||
})
|
||||
).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user