fix(game-ui): 내정보와 감찰부 장수 패널을 통일한다

두 화면이 같은 장수 정보 패널에서 전투 통계와 숙련도를 렌더링하도록 정리한다.

감찰부 통계를 rank_data에서 읽고 삭턴, 사관, 최근 전투와 승률·살상률 계약을 맞춘다.
This commit is contained in:
2026-08-21 16:20:50 +00:00
parent 76f58222bd
commit d5d57bbabc
9 changed files with 267 additions and 117 deletions
+2
View File
@@ -265,6 +265,7 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
dedication: true,
age: true,
turnTime: true,
recentWarTime: true,
crewTypeId: true,
personalCode: true,
specialCode: true,
@@ -519,6 +520,7 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
age: general.age,
retirementYear,
turnTime: general.turnTime.toISOString(),
recentWar: general.recentWarTime?.toISOString() ?? null,
defenceTrain: settings.defence_train,
killTurn: readNumber(metaRecord.killturn ?? metaRecord.killTurn, 0),
remainingMinutes: resolveRemainingMinutes(
@@ -1,6 +1,6 @@
import { TRPCError } from '@trpc/server';
import { asRecord } from '@sammo-ts/common';
import { asRecord, type RankDataType } from '@sammo-ts/common';
import { LogCategory } from '@sammo-ts/logic';
import { accessAuthedProcedure } from '../../../trpc.js';
@@ -14,6 +14,15 @@ import {
import { getMyGeneral } from '../../shared/general.js';
import { assertNationAccess, formatDateTime, loadTraitNames, resolveNationPermission } from '../shared.js';
const BATTLE_CENTER_RECORD_TYPES = [
'firenum',
'warnum',
'killnum',
'deathnum',
'killcrew',
'deathcrew',
] as const satisfies readonly RankDataType[];
export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
const me = await getMyGeneral(ctx);
assertNationAccess(me);
@@ -81,23 +90,38 @@ export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
}
const generalIds = generalRows.map((general) => general.id);
const battleCounts =
const [battleCounts, rankRows] =
generalIds.length > 0
? await ctx.db.logEntry.groupBy({
by: ['generalId'],
where: {
generalId: { in: generalIds },
category: LogCategory.BATTLE_BRIEF,
},
_count: { _all: true },
})
: [];
? await Promise.all([
ctx.db.logEntry.groupBy({
by: ['generalId'],
where: {
generalId: { in: generalIds },
category: LogCategory.BATTLE_BRIEF,
},
_count: { _all: true },
}),
ctx.db.rankData.findMany({
where: {
generalId: { in: generalIds },
type: { in: [...BATTLE_CENTER_RECORD_TYPES] },
},
select: { generalId: true, type: true, value: true },
}),
])
: [[], []];
const battleCountMap = new Map<number, number>();
for (const row of battleCounts) {
if (row.generalId !== null) {
battleCountMap.set(row.generalId, row._count._all);
}
}
const rankValueMap = new Map<number, Map<RankDataType, number>>();
for (const row of rankRows) {
const values = rankValueMap.get(row.generalId) ?? new Map<RankDataType, number>();
values.set(row.type as (typeof BATTLE_CENTER_RECORD_TYPES)[number], row.value);
rankValueMap.set(row.generalId, values);
}
const worldConfig = asRecord(worldState.config);
const constValues = asRecord(worldConfig.const ?? worldConfig.consts);
@@ -141,10 +165,18 @@ export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
general.meta && typeof general.meta === 'object' && !Array.isArray(general.meta)
? (general.meta as Record<string, unknown>)
: {};
const metaNumber = (key: string): number => {
const value = meta[key];
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
const metaNumber = (keys: string | string[], fallback = 0): number => {
for (const key of Array.isArray(keys) ? keys : [keys]) {
const value = meta[key];
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
}
return fallback;
};
const rankValue = (type: (typeof BATTLE_CENTER_RECORD_TYPES)[number], fallback = 0): number =>
rankValueMap.get(general.id)?.get(type) ?? fallback;
const warnum = rankValue('warnum', metaNumber(['rank_warnum', 'warnum'], battleCountMap.get(general.id) ?? 0));
const storedDedicationLevel = metaNumber('dedlevel');
const dedicationLevel =
storedDedicationLevel > 0
@@ -161,7 +193,7 @@ export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
cityId: general.cityId,
turnTime: formatDateTime(general.turnTime),
recentWar: formatDateTime(general.recentWarTime),
warnum: battleCountMap.get(general.id) ?? 0,
warnum,
stats: {
leadership: general.leadership,
strength: general.strength,
@@ -176,6 +208,8 @@ export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
train: general.train,
atmos: general.atmos,
age: general.age,
defenceTrain: metaNumber('defence_train', 80),
killTurn: metaNumber(['killturn', 'killTurn']),
crewTypeId: general.crewTypeId,
crewTypeName: crewTypeNames.get(general.crewTypeId) ?? '-',
equipment: {
@@ -207,12 +241,13 @@ export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
statUpgradeLimit,
dex: [1, 2, 3, 4, 5].map((index) => metaNumber(`dex${index}`)),
},
serviceYears: metaNumber('belong'),
battleStats: {
kills: metaNumber('rank_killnum') || metaNumber('killnum'),
deaths: metaNumber('deathnum'),
fire: metaNumber('firenum'),
killCrew: metaNumber('killcrew'),
deathCrew: metaNumber('deathcrew'),
kills: rankValue('killnum', metaNumber(['rank_killnum', 'killnum'])),
deaths: rankValue('deathnum', metaNumber(['rank_deathnum', 'deathnum'])),
fire: rankValue('firenum', metaNumber(['rank_firenum', 'firenum'])),
killCrew: rankValue('killcrew', metaNumber(['rank_killcrew', 'killcrew'])),
deathCrew: rankValue('deathcrew', metaNumber(['rank_deathcrew', 'deathcrew'])),
dex: [1, 2, 3, 4, 5].map((index) => metaNumber(`dex${index}`)),
},
};