feat: complete nation personnel and finance parity

This commit is contained in:
2026-07-26 04:30:12 +00:00
parent b27c529a3d
commit f4c09a9b02
16 changed files with 2834 additions and 1222 deletions
@@ -8,7 +8,10 @@ export const changePermission = authedProcedure
.input(
z.object({
isAmbassador: z.boolean(),
targetGeneralIds: z.array(z.number().int().positive()),
targetGeneralIds: z
.array(z.number().int().positive())
.max(2)
.refine((ids) => new Set(ids).size === ids.length, '중복된 장수를 지정할 수 없습니다.'),
})
)
.mutation(async ({ ctx, input }) => {
@@ -10,7 +10,7 @@ export const getPersonnelInfo = authedProcedure.query(async ({ ctx }) => {
const me = await getMyGeneral(ctx);
assertNationAccess(me);
const [nation, cityRows, troopRows, generalRows, worldState] = await Promise.all([
const [nation, cityRows, troopRows, generalRows, worldState, rankRows] = await Promise.all([
ctx.db.nation.findUnique({
where: { id: me.nationId },
select: {
@@ -25,7 +25,7 @@ export const getPersonnelInfo = authedProcedure.query(async ({ ctx }) => {
}),
ctx.db.city.findMany({
where: { nationId: me.nationId },
select: { id: true, name: true, level: true, region: true },
select: { id: true, name: true, level: true, region: true, meta: true },
orderBy: { id: 'asc' },
}),
ctx.db.troop.findMany({ select: { troopLeaderId: true, name: true } }),
@@ -38,6 +38,8 @@ export const getPersonnelInfo = authedProcedure.query(async ({ ctx }) => {
nationId: true,
cityId: true,
troopId: true,
picture: true,
imageServer: true,
officerLevel: true,
leadership: true,
strength: true,
@@ -57,6 +59,15 @@ export const getPersonnelInfo = authedProcedure.query(async ({ ctx }) => {
orderBy: { id: 'asc' },
}),
ctx.db.worldState.findFirst(),
ctx.db.rankData.findMany({
where: {
nationId: me.nationId,
type: { in: ['killnum', 'firenum'] },
value: { gt: 0 },
},
select: { generalId: true, type: true, value: true },
orderBy: [{ value: 'desc' }, { generalId: 'asc' }],
}),
]);
if (!nation) {
@@ -66,20 +77,42 @@ export const getPersonnelInfo = authedProcedure.query(async ({ ctx }) => {
const cityNameMap = new Map(cityRows.map((city) => [city.id, city.name]));
const troopNameMap = new Map(troopRows.map((troop) => [troop.troopLeaderId, troop.name]));
const mappedGenerals = await mapGeneralList(generalRows, cityNameMap, troopNameMap);
const canManage = me.officerLevel >= 5;
const responseGenerals = canManage
? mappedGenerals
: mappedGenerals.map((general) => ({
...general,
stats: { leadership: 0, strength: 0, intelligence: 0 },
experience: 0,
dedication: 0,
injury: 0,
gold: 0,
rice: 0,
crew: 0,
troopId: 0,
troopName: null,
personality: null,
specialDomestic: null,
specialWar: null,
}));
const responseGeneralMap = new Map(responseGenerals.map((general) => [general.id, general]));
const visibleGenerals = responseGenerals.filter((general) => canManage || general.officerLevel >= 2);
const chiefAssignments = mappedGenerals
const chiefAssignments = responseGenerals
.filter((general) => general.officerLevel >= 5)
.reduce<Record<number, (typeof mappedGenerals)[number]>>((acc, general) => {
.reduce<Record<number, (typeof responseGenerals)[number]>>((acc, general) => {
acc[general.officerLevel] = general;
return acc;
}, {});
const cityAssignments = cityRows.map((city) => {
const officers = mappedGenerals.filter(
(general) => general.officerLevel >= 2 && general.officerLevel <= 4 && general.officerCity === city.id
);
const officers = mappedGenerals
.filter(
(general) => general.officerLevel >= 2 && general.officerLevel <= 4 && general.officerCity === city.id
)
.map((general) => responseGeneralMap.get(general.id)!);
const officerMap: Record<number, (typeof mappedGenerals)[number] | null> = {
const officerMap: Record<number, (typeof responseGenerals)[number] | null> = {
4: null,
3: null,
2: null,
@@ -94,6 +127,7 @@ export const getPersonnelInfo = authedProcedure.query(async ({ ctx }) => {
name: city.name,
level: city.level,
region: city.region,
officerSet: Number(asRecord(city.meta).officer_set ?? 0),
officers: officerMap,
};
});
@@ -116,17 +150,37 @@ export const getPersonnelInfo = authedProcedure.query(async ({ ctx }) => {
};
});
const ambassadors = permissionCandidates.filter(
(candidate) => candidate.permission === 'ambassador' || candidate.maxPermission === 4
);
const auditors = permissionCandidates.filter(
(candidate) => candidate.permission === 'auditor' || candidate.maxPermission >= 3
);
const canChangePermissions = me.officerLevel === 12;
const ambassadors = canChangePermissions
? permissionCandidates.filter(
(candidate) => candidate.permission === 'ambassador' || candidate.maxPermission === 4
)
: [];
const auditors = canChangePermissions
? permissionCandidates.filter((candidate) => candidate.permission === 'auditor' || candidate.maxPermission >= 3)
: [];
const generalNameMap = new Map(mappedGenerals.map((general) => [general.id, general.name]));
const awards = {
tigers: rankRows
.filter((row) => row.type === 'killnum')
.slice(0, 5)
.map((row) => ({ id: row.generalId, name: generalNameMap.get(row.generalId) ?? '-', value: row.value })),
eagles: rankRows
.filter((row) => row.type === 'firenum')
.slice(0, 7)
.map((row) => ({ id: row.generalId, name: generalNameMap.get(row.generalId) ?? '-', value: row.value })),
};
const nationMeta = asRecord(nation.meta);
const mePenalty = penaltyMap.get(me.id) ?? {};
const chiefSet = Number(nationMeta.chief_set ?? 0);
return {
me: {
id: me.id,
officerLevel: me.officerLevel,
canManage,
canChangePermissions,
canKick: canManage && mePenalty.noBanGeneral !== true && (chiefSet & (1 << me.officerLevel)) === 0,
},
nation: {
id: nation.id,
@@ -135,11 +189,13 @@ export const getPersonnelInfo = authedProcedure.query(async ({ ctx }) => {
level: nation.level,
typeCode: nation.typeCode,
capitalCityId: nation.capitalCityId ?? 0,
chiefSet,
},
chiefStatMin: resolveChiefStatMin(worldState),
generals: mappedGenerals,
generals: visibleGenerals,
chiefAssignments,
cityAssignments,
awards,
permissionCandidates: {
ambassadors,
auditors,
+8 -6
View File
@@ -83,6 +83,8 @@ export type GeneralListRow = {
nationId: number;
cityId: number;
troopId: number;
picture?: string | null;
imageServer?: number;
officerLevel: number;
leadership: number;
strength: number;
@@ -270,7 +272,8 @@ export const resolveNationScoutMessage = (meta: Record<string, unknown>): string
export const resolveWarSettingRemain = (meta: Record<string, unknown>): number => {
const legacy = readMetaNumber(meta, 'available_war_setting_cnt', -1);
const fallback = legacy >= 0 ? legacy : readMetaNumber(meta, 'availableWarSettingCnt', MAX_AVAILABLE_WAR_SETTING_CNT);
const fallback =
legacy >= 0 ? legacy : readMetaNumber(meta, 'availableWarSettingCnt', MAX_AVAILABLE_WAR_SETTING_CNT);
return Math.max(0, Math.min(MAX_AVAILABLE_WAR_SETTING_CNT, fallback));
};
@@ -287,10 +290,7 @@ export const checkSecretMaxPermission = (penalty: Record<string, unknown>): numb
return 4;
};
export const loadTraitNames = async (
keys: Array<string | null>,
kind: keyof TraitCache
): Promise<TraitNameMap> => {
export const loadTraitNames = async (keys: Array<string | null>, kind: keyof TraitCache): Promise<TraitNameMap> => {
const cache = traitCache[kind];
const unique = Array.from(new Set(keys.filter((key): key is string => Boolean(key))));
const missing = unique.filter((key) => !cache.has(key));
@@ -481,8 +481,10 @@ export const mapGeneralList = async (
cityName: cityNameMap.get(general.cityId) ?? null,
troopId: general.troopId,
troopName: troopNameMap.get(general.troopId) ?? null,
picture: general.picture ?? null,
imageServer: general.imageServer ?? 0,
officerCity,
officerCityName: officerCity > 0 ? cityNameMap.get(officerCity) ?? null : null,
officerCityName: officerCity > 0 ? (cityNameMap.get(officerCity) ?? null) : null,
stats: {
leadership: general.leadership,
strength: general.strength,