fix(game-ui): 메인 턴과 장수 상태 표시를 보완
This commit is contained in:
@@ -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 ?? '-',
|
||||
|
||||
@@ -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: '열심' },
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user