fix(game-ui): 내정보와 감찰부 장수 패널을 통일한다
두 화면이 같은 장수 정보 패널에서 전투 통계와 숙련도를 렌더링하도록 정리한다. 감찰부 통계를 rank_data에서 읽고 삭턴, 사관, 최근 전투와 승률·살상률 계약을 맞춘다.
This commit is contained in:
@@ -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}`)),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -85,7 +85,7 @@ const createContext = (options: {
|
||||
troopLeaderAction?: string | null;
|
||||
refreshScore?: number;
|
||||
refreshScoreTotal?: number;
|
||||
rankRows?: Array<{ type: string; value: number }>;
|
||||
rankRows?: Array<{ generalId?: number; type: string; value: number }>;
|
||||
requestId?: string;
|
||||
transaction?: ReturnType<typeof vi.fn>;
|
||||
}) => {
|
||||
@@ -130,7 +130,9 @@ const createContext = (options: {
|
||||
})),
|
||||
},
|
||||
rankData: {
|
||||
findMany: vi.fn(async () => options.rankRows ?? []),
|
||||
findMany: vi.fn(async () =>
|
||||
(options.rankRows ?? []).map((row) => ({ generalId: row.generalId ?? me?.id ?? 0, ...row }))
|
||||
),
|
||||
},
|
||||
city: {
|
||||
findUnique: vi.fn(async () => options.city ?? null),
|
||||
@@ -812,7 +814,23 @@ describe('battle-center general and user permissions', () => {
|
||||
});
|
||||
|
||||
const tenured = createContext({
|
||||
me: buildGeneral({ officerLevel: 1, meta: { belong: 3, permission: 'normal' } }),
|
||||
me: buildGeneral({
|
||||
officerLevel: 1,
|
||||
meta: {
|
||||
belong: 3,
|
||||
permission: 'normal',
|
||||
killturn: 6,
|
||||
defence_train: 80,
|
||||
},
|
||||
}),
|
||||
rankRows: [
|
||||
{ type: 'warnum', value: 8 },
|
||||
{ type: 'killnum', value: 5 },
|
||||
{ type: 'deathnum', value: 3 },
|
||||
{ type: 'firenum', value: 12 },
|
||||
{ type: 'killcrew', value: 12_345 },
|
||||
{ type: 'deathcrew', value: 6_789 },
|
||||
],
|
||||
nationMeta: { secretlimit: 3 },
|
||||
});
|
||||
await expect(appRouter.createCaller(tenured.context).nation.getBattleCenter()).resolves.toMatchObject({
|
||||
@@ -823,6 +841,9 @@ describe('battle-center general and user permissions', () => {
|
||||
picture: 'default.jpg',
|
||||
imageServer: 0,
|
||||
officerLevelText: '일반',
|
||||
warnum: 8,
|
||||
defenceTrain: 80,
|
||||
killTurn: 6,
|
||||
crewTypeName: '-',
|
||||
equipmentNames: { weapon: '-', book: '-', horse: '-', item: '-' },
|
||||
traits: { personal: '-', specialDomestic: '-', specialWar: '-' },
|
||||
@@ -834,10 +855,25 @@ describe('battle-center general and user permissions', () => {
|
||||
statUpgradeLimit: 20,
|
||||
dex: [0, 0, 0, 0, 0],
|
||||
},
|
||||
battleStats: { kills: 0, deaths: 0, fire: 0, killCrew: 0, deathCrew: 0, dex: [0, 0, 0, 0, 0] },
|
||||
serviceYears: 3,
|
||||
battleStats: {
|
||||
kills: 5,
|
||||
deaths: 3,
|
||||
fire: 12,
|
||||
killCrew: 12_345,
|
||||
deathCrew: 6_789,
|
||||
dex: [0, 0, 0, 0, 0],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(tenured.db.rankData.findMany).toHaveBeenCalledWith({
|
||||
where: {
|
||||
generalId: { in: [7] },
|
||||
type: { in: ['firenum', 'warnum', 'killnum', 'deathnum', 'killcrew', 'deathcrew'] },
|
||||
},
|
||||
select: { generalId: true, type: true, value: true },
|
||||
});
|
||||
|
||||
const auditor = createContext({
|
||||
me: buildGeneral({ officerLevel: 1, meta: { belong: 0, permission: 'auditor' } }),
|
||||
|
||||
Reference in New Issue
Block a user