fix(game-ui): 메인 턴과 장수 상태 표시를 보완

This commit is contained in:
2026-08-29 03:30:50 +00:00
parent fa9fe71539
commit 26952fcdc7
11 changed files with 120 additions and 49 deletions
+3 -2
View File
@@ -333,6 +333,7 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
troopLeaderFirstTurn,
accessLog,
rankRows,
gameTime,
] = await Promise.all([
general.cityId > 0
? ctx.db.city.findUnique({
@@ -413,6 +414,7 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
where: { generalId: general.id, type: { in: [...PERSONAL_RECORD_TYPES] } },
select: { type: true, value: true },
}),
loadCurrentGameTime(ctx.db),
]);
const nation = queriedNation ?? NEUTRAL_NATION_CONTEXT;
@@ -588,8 +590,7 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
killTurn: readNumber(metaRecord.killturn ?? metaRecord.killTurn, 0),
remainingMinutes: resolveRemainingMinutes(
general.turnTime,
parsedLastExecuted,
worldState?.tickSeconds ?? 0
gameTime.now
),
crewTypeId: general.crewTypeId,
crewTypeName: crewTypeDetails.get(general.crewTypeId)?.name ?? '-',
+4 -12
View File
@@ -23,6 +23,7 @@ import {
resolveRefreshScoreText,
resolveRemainingMinutes,
} from '../../services/generalBasicCardProjection.js';
import { loadCurrentGameTime } from '../../services/gameClock.js';
import { loadTraitNames } from '../nation/shared.js';
import { getAuthenticatedUserId, getMyGeneral } from '../shared/general.js';
import { throwIfCommandRejected } from '../shared/turnDaemon.js';
@@ -72,7 +73,7 @@ export const troopRouter = router({
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: '국가에 소속되어 있지 않습니다.' });
}
const [nation, troops, generals, cities, worldState] = await Promise.all([
const [nation, troops, generals, cities, worldState, gameTime] = await Promise.all([
ctx.db.nation.findUnique({
where: { id: me.nationId },
select: { id: true, name: true, color: true, level: true, meta: true },
@@ -121,6 +122,7 @@ export const troopRouter = router({
select: { id: true, name: true },
}),
ctx.db.worldState.findFirst({ select: { tickSeconds: true, config: true, meta: true } }),
loadCurrentGameTime(ctx.db),
]);
if (!nation) {
throw new TRPCError({ code: 'NOT_FOUND', message: '국가 정보를 찾을 수 없습니다.' });
@@ -214,15 +216,6 @@ export const troopRouter = router({
const traitName = (code: string, names: Map<string, { name: string }>): string =>
names.get(code)?.name ?? sanitizeInternalDisplayCode(code);
const itemName = (code: string): string => itemNames.get(code) ?? sanitizeInternalDisplayCode(code);
const worldMeta = asRecord(worldState?.meta);
const rawLastExecuted = worldMeta.lastTurnTime ?? worldMeta.turntime;
const lastExecuted =
rawLastExecuted instanceof Date
? rawLastExecuted
: typeof rawLastExecuted === 'string'
? new Date(rawLastExecuted)
: null;
const mappedTroops = troops
.map((troop) => {
const leader = generalMap.get(troop.troopLeaderId);
@@ -348,8 +341,7 @@ export const troopRouter = router({
killTurn: readNumber(meta.killturn ?? meta.killTurn),
remainingMinutes: resolveRemainingMinutes(
general.turnTime,
lastExecuted,
worldState?.tickSeconds ?? 0
gameTime.now
),
troopId: general.troopId,
troop: {
@@ -45,15 +45,10 @@ export const resolveRefreshScoreText = (score: number): string => {
export const resolveRemainingMinutes = (
turnTime: Date,
lastExecuted: Date | null,
turnTermSeconds: number
currentGameTime: Date | null
): number | null => {
if (!lastExecuted || !Number.isFinite(lastExecuted.getTime()) || turnTermSeconds <= 0) return null;
let nextTurnMillis = turnTime.getTime();
if (nextTurnMillis < lastExecuted.getTime()) {
nextTurnMillis += turnTermSeconds * 1_000;
}
return Math.floor(Math.min(999, Math.max(0, (nextTurnMillis - lastExecuted.getTime()) / 60_000)));
if (!currentGameTime || !Number.isFinite(currentGameTime.getTime())) return null;
return Math.floor(Math.max(0, (turnTime.getTime() - currentGameTime.getTime()) / 60_000));
};
export interface NextTurnMonthOffsetInput {
@@ -36,11 +36,12 @@ describe('general basic card Ref projection', () => {
expect(resolveRefreshScoreText(12_800)).toBe('헐...');
});
it('matches the Ref remaining-minute calculation and one-turn rollover', () => {
const lastExecuted = new Date('2026-08-13T00:00:00.000Z');
expect(resolveRemainingMinutes(new Date('2026-08-13T00:07:06.000Z'), lastExecuted, 3_600)).toBe(7);
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('matches the Ref remaining-minute calculation against the current game clock', () => {
const currentGameTime = new Date('2026-08-13T00:20:00.000Z');
expect(resolveRemainingMinutes(new Date('2026-08-13T01:20:00.000Z'), currentGameTime)).toBe(60);
expect(resolveRemainingMinutes(new Date('2026-08-13T01:08:59.000Z'), currentGameTime)).toBe(48);
expect(resolveRemainingMinutes(new Date('2026-08-13T00:19:00.000Z'), currentGameTime)).toBe(0);
expect(resolveRemainingMinutes(new Date('2026-08-13T00:20:00.000Z'), null)).toBeNull();
});
it('moves the first reserved month only after the general turn bucket has passed', () => {
@@ -85,6 +85,7 @@ const createContext = (options: {
troopLeaderAction?: string | null;
refreshScore?: number;
refreshScoreTotal?: number;
gameClockNow?: Date;
rankRows?: Array<{ generalId?: number; type: string; value: number }>;
requestId?: string;
transaction?: ReturnType<typeof vi.fn>;
@@ -164,6 +165,10 @@ const createContext = (options: {
currentMonth: 1,
tickSeconds: 600,
config: { const: { upgradeLimit: 20 } },
clockBaseTime: options.gameClockNow ?? null,
clockTick: options.gameClockNow ? 0n : null,
clockMode: 'manual',
clockWallAnchor: options.gameClockNow ?? null,
})),
},
logEntry: {
@@ -485,6 +490,7 @@ describe('in-game my information ownership', () => {
troopLeaderAction: '휴식',
refreshScore: 3,
refreshScoreTotal: 1_141,
gameClockNow: new Date('2026-01-01T00:00:00.000Z'),
});
await expect(appRouter.createCaller(fixture.context).general.me()).resolves.toMatchObject({
@@ -495,7 +501,7 @@ describe('in-game my information ownership', () => {
retirementYear: 70,
defenceTrain: 80,
killTurn: 6,
remainingMinutes: null,
remainingMinutes: 7,
troop: { name: '정밀검증부대', status: 'inactive', leaderCityName: '업' },
refreshScore: { current: 3, total: 1_141, text: '열심' },
},