fix: render Ref-compatible GUI display names
This commit is contained in:
@@ -3,7 +3,9 @@ import { z } from 'zod';
|
||||
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
|
||||
import { resolveOfficerLevelName, sanitizeInternalDisplayCode } from '../../services/gameDisplayNames.js';
|
||||
import { readOnlyAuthedProcedure, router } from '../../trpc.js';
|
||||
import { loadTraitNames } from '../nation/shared.js';
|
||||
|
||||
const numberOrNull = (value: unknown): number | null =>
|
||||
typeof value === 'number' && Number.isFinite(value) ? value : null;
|
||||
@@ -87,6 +89,31 @@ export const archiveRouter = router({
|
||||
nationByServerAndId.set(key, nation);
|
||||
}
|
||||
}
|
||||
const archivedRoles = generals.map((general) => {
|
||||
const data = asRecord(general.data);
|
||||
const role = asRecord(data.role);
|
||||
return {
|
||||
personal: displayTextOrNull(data.personalCode ?? data.personal ?? role.personality),
|
||||
special: displayTextOrNull(data.specialCode ?? data.special ?? role.specialDomestic),
|
||||
special2: displayTextOrNull(data.special2Code ?? data.special2 ?? role.specialWar),
|
||||
};
|
||||
});
|
||||
const [personalityNames, domesticNames, warNames] = await Promise.all([
|
||||
loadTraitNames(
|
||||
archivedRoles.map((role) => role.personal),
|
||||
'personality'
|
||||
),
|
||||
loadTraitNames(
|
||||
archivedRoles.map((role) => role.special),
|
||||
'domestic'
|
||||
),
|
||||
loadTraitNames(
|
||||
archivedRoles.map((role) => role.special2),
|
||||
'war'
|
||||
),
|
||||
]);
|
||||
const displayRole = (value: string | null, names: Awaited<ReturnType<typeof loadTraitNames>>): string | null =>
|
||||
value ? (names.get(value)?.name ?? sanitizeInternalDisplayCode(value)) : null;
|
||||
|
||||
const seasons = new Map<
|
||||
string,
|
||||
@@ -110,6 +137,7 @@ export const archiveRouter = router({
|
||||
experience: number | null;
|
||||
dedication: number | null;
|
||||
officerLevel: number | null;
|
||||
officerLevelText: string | null;
|
||||
personal: string | null;
|
||||
special: string | null;
|
||||
special2: string | null;
|
||||
@@ -118,7 +146,7 @@ export const archiveRouter = router({
|
||||
}
|
||||
>();
|
||||
|
||||
for (const general of generals) {
|
||||
for (const [generalIndex, general] of generals.entries()) {
|
||||
const game = gameByServer.get(general.serverId);
|
||||
let season = seasons.get(general.serverId);
|
||||
if (!season) {
|
||||
@@ -136,10 +164,12 @@ export const archiveRouter = router({
|
||||
|
||||
const data = asRecord(general.data);
|
||||
const stats = asRecord(data.stats);
|
||||
const role = asRecord(data.role);
|
||||
const nationId = firstNumber(data, 'nationId', 'nation') ?? 0;
|
||||
const nation = nationByServerAndId.get(`${general.serverId}:${nationId}`);
|
||||
const nationData = asRecord(nation?.data);
|
||||
const officerLevel = firstNumber(data, 'officerLevel', 'officer_level');
|
||||
const nationLevel = firstNumber(nationData, 'level', 'nationLevel');
|
||||
const archivedRole = archivedRoles[generalIndex]!;
|
||||
season.generals.push({
|
||||
generalNo: general.generalNo,
|
||||
name: general.name,
|
||||
@@ -152,10 +182,14 @@ export const archiveRouter = router({
|
||||
intel: firstNumber(data, 'intel', 'intelligence') ?? numberOrNull(stats.intelligence),
|
||||
experience: numberOrNull(data.experience),
|
||||
dedication: numberOrNull(data.dedication),
|
||||
officerLevel: firstNumber(data, 'officerLevel', 'officer_level'),
|
||||
personal: displayTextOrNull(data.personalCode ?? data.personal ?? role.personality),
|
||||
special: displayTextOrNull(data.specialCode ?? data.special ?? role.specialDomestic),
|
||||
special2: displayTextOrNull(data.special2Code ?? data.special2 ?? role.specialWar),
|
||||
officerLevel,
|
||||
officerLevelText:
|
||||
officerLevel === null
|
||||
? null
|
||||
: resolveOfficerLevelName(officerLevel, nationLevel === null ? undefined : nationLevel),
|
||||
personal: displayRole(archivedRole.personal, personalityNames),
|
||||
special: displayRole(archivedRole.special, domesticNames),
|
||||
special2: displayRole(archivedRole.special2, warNames),
|
||||
historyCount: parseHistory(data.history).length,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -17,6 +17,16 @@ import {
|
||||
import { ConflictingTurnDaemonCommandError } from '../../daemon/databaseTransport.js';
|
||||
import { resolveAccessWindows } from '../../services/generalAccess.js';
|
||||
import { adjustAccountIconForUser } from '../../services/accountIconSync.js';
|
||||
import {
|
||||
loadCrewTypeDisplayNames,
|
||||
loadItemDisplayNames,
|
||||
resolveCityLevelName,
|
||||
resolveDedicationLevelName,
|
||||
resolveNationLevelName,
|
||||
resolveOfficerLevelName,
|
||||
resolveRegionName,
|
||||
sanitizeInternalDisplayCode,
|
||||
} from '../../services/gameDisplayNames.js';
|
||||
import { getMyGeneral } from '../shared/general.js';
|
||||
import { loadTraitNames, resolveNationNotice, type TraitNameMap } from '../nation/shared.js';
|
||||
|
||||
@@ -155,7 +165,7 @@ const resolveTraitDisplayName = (code: string, names: TraitNameMap): string => {
|
||||
return loadedName;
|
||||
}
|
||||
// Ref는 class getName()을 표시하므로 로더가 모르는 선택적 특기도 raw namespace는 노출하지 않는다.
|
||||
return code.replace(/^che_(?:event_)?/u, '');
|
||||
return sanitizeInternalDisplayCode(code);
|
||||
};
|
||||
|
||||
const resolveUserSettings = (meta: Record<string, unknown>) => {
|
||||
@@ -244,7 +254,7 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
|
||||
return null;
|
||||
}
|
||||
|
||||
const [city, nation, worldState] = await Promise.all([
|
||||
const [city, queriedNation, worldState] = await Promise.all([
|
||||
general.cityId > 0
|
||||
? ctx.db.city.findUnique({
|
||||
where: { id: general.cityId },
|
||||
@@ -291,18 +301,36 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
|
||||
: Promise.resolve(NEUTRAL_NATION_CONTEXT),
|
||||
ctx.db.worldState.findFirst({ select: { config: true } }),
|
||||
]);
|
||||
const nation = queriedNation ?? NEUTRAL_NATION_CONTEXT;
|
||||
|
||||
const [personalityNames, domesticNames, warNames] = await Promise.all([
|
||||
const [capitalCity, cityNation] = await Promise.all([
|
||||
nation.capitalCityId
|
||||
? ctx.db.city.findUnique({ where: { id: nation.capitalCityId }, select: { name: true } })
|
||||
: Promise.resolve(null),
|
||||
city && city.nationId > 0
|
||||
? ctx.db.nation.findUnique({ where: { id: city.nationId }, select: { name: true } })
|
||||
: Promise.resolve(null),
|
||||
]);
|
||||
const [personalityNames, domesticNames, warNames, nationTypeNames, crewTypeNames, itemNames] = await Promise.all([
|
||||
loadTraitNames([general.personalCode], 'personality'),
|
||||
loadTraitNames([general.specialCode], 'domestic'),
|
||||
loadTraitNames([general.special2Code], 'war'),
|
||||
loadTraitNames([nation.typeCode], 'nation'),
|
||||
loadCrewTypeDisplayNames(worldState, ctx.profile.id),
|
||||
loadItemDisplayNames([general.horseCode, general.weaponCode, general.bookCode, general.itemCode]),
|
||||
]);
|
||||
|
||||
const metaRecord = asRecord(general.meta);
|
||||
const worldConfig = asRecord(worldState?.config);
|
||||
const constValues = asRecord(worldConfig.const ?? worldConfig.consts);
|
||||
const maxDedicationLevel = readNumber(constValues.maxDedLevel, 30);
|
||||
const settings = resolveUserSettings(metaRecord);
|
||||
const penalties = resolvePenalty(general.penalty);
|
||||
const dedicationLevel = readNumber(metaRecord.dedlevel, 0);
|
||||
const itemName = (code: string | null): string | null => {
|
||||
const normalized = normalizeItemCode(code);
|
||||
return normalized ? (itemNames.get(normalized) ?? sanitizeInternalDisplayCode(normalized)) : null;
|
||||
};
|
||||
|
||||
return {
|
||||
general: {
|
||||
@@ -315,6 +343,7 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
|
||||
picture: general.picture,
|
||||
imageServer: general.imageServer,
|
||||
officerLevel: general.officerLevel,
|
||||
officerLevelText: resolveOfficerLevelName(general.officerLevel, nation.level),
|
||||
stats: {
|
||||
leadership: general.leadership,
|
||||
strength: general.strength,
|
||||
@@ -331,6 +360,7 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
|
||||
age: general.age,
|
||||
turnTime: general.turnTime.toISOString(),
|
||||
crewTypeId: general.crewTypeId,
|
||||
crewTypeName: crewTypeNames.get(general.crewTypeId) ?? '-',
|
||||
traits: {
|
||||
personal: resolveTraitDisplayName(general.personalCode, personalityNames),
|
||||
specialDomestic: resolveTraitDisplayName(general.specialCode, domesticNames),
|
||||
@@ -338,7 +368,8 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
|
||||
},
|
||||
progression: {
|
||||
experienceLevel: readNumber(metaRecord.explevel, 0),
|
||||
dedicationLevel: readNumber(metaRecord.dedlevel, 0),
|
||||
dedicationLevel,
|
||||
dedicationText: resolveDedicationLevelName(dedicationLevel, maxDedicationLevel),
|
||||
statExperience: {
|
||||
leadership: readNumber(metaRecord.leadership_exp, 0),
|
||||
strength: readNumber(metaRecord.strength_exp, 0),
|
||||
@@ -353,6 +384,12 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
|
||||
book: normalizeItemCode(general.bookCode),
|
||||
item: normalizeItemCode(general.itemCode),
|
||||
},
|
||||
itemNames: {
|
||||
horse: itemName(general.horseCode),
|
||||
weapon: itemName(general.weaponCode),
|
||||
book: itemName(general.bookCode),
|
||||
item: itemName(general.itemCode),
|
||||
},
|
||||
},
|
||||
iconChoices: ctx.auth?.user.canUseGeneralPicture === false ? [] : (ctx.auth?.user.icons ?? []),
|
||||
canChangeIcon: general.npcState === 0 && ctx.auth?.user.canUseGeneralPicture !== false,
|
||||
@@ -360,8 +397,23 @@ export const getGeneralContext = async (ctx: GameApiContext) => {
|
||||
typeof metaRecord.generalIconChangedAt === 'string'
|
||||
? new Date(new Date(metaRecord.generalIconChangedAt).getTime() + 24 * 60 * 60 * 1000).toISOString()
|
||||
: null,
|
||||
city,
|
||||
nation,
|
||||
city: city
|
||||
? {
|
||||
...city,
|
||||
levelName: resolveCityLevelName(city.level),
|
||||
regionName: resolveRegionName(city.region),
|
||||
nationName: city.nationId > 0 ? (cityNation?.name ?? '-') : '공백지',
|
||||
}
|
||||
: null,
|
||||
nation: {
|
||||
...nation,
|
||||
levelName: resolveNationLevelName(nation.level),
|
||||
typeName:
|
||||
nation.id === 0
|
||||
? '해당 없음'
|
||||
: (nationTypeNames.get(nation.typeCode)?.name ?? sanitizeInternalDisplayCode(nation.typeCode)),
|
||||
capitalCityName: nation.id === 0 ? null : (capitalCity?.name ?? null),
|
||||
},
|
||||
settings,
|
||||
penalties,
|
||||
};
|
||||
|
||||
@@ -4,8 +4,15 @@ import { asRecord } from '@sammo-ts/common';
|
||||
import { LogCategory } from '@sammo-ts/logic';
|
||||
|
||||
import { accessAuthedProcedure } from '../../../trpc.js';
|
||||
import {
|
||||
loadCrewTypeDisplayNames,
|
||||
loadItemDisplayNames,
|
||||
resolveDedicationLevelName,
|
||||
resolveOfficerLevelName,
|
||||
sanitizeInternalDisplayCode,
|
||||
} from '../../../services/gameDisplayNames.js';
|
||||
import { getMyGeneral } from '../../shared/general.js';
|
||||
import { assertNationAccess, formatDateTime, resolveNationPermission } from '../shared.js';
|
||||
import { assertNationAccess, formatDateTime, loadTraitNames, resolveNationPermission } from '../shared.js';
|
||||
|
||||
export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
const me = await getMyGeneral(ctx);
|
||||
@@ -98,6 +105,36 @@ export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
typeof constValues.upgradeLimit === 'number' && Number.isFinite(constValues.upgradeLimit)
|
||||
? constValues.upgradeLimit
|
||||
: 30;
|
||||
const maxDedicationLevel =
|
||||
typeof constValues.maxDedLevel === 'number' && Number.isFinite(constValues.maxDedLevel)
|
||||
? Math.max(0, Math.trunc(constValues.maxDedLevel))
|
||||
: 30;
|
||||
const [personalityNames, domesticNames, warNames, crewTypeNames, itemNames] = await Promise.all([
|
||||
loadTraitNames(
|
||||
generalRows.map((general) => general.personalCode),
|
||||
'personality'
|
||||
),
|
||||
loadTraitNames(
|
||||
generalRows.map((general) => general.specialCode),
|
||||
'domestic'
|
||||
),
|
||||
loadTraitNames(
|
||||
generalRows.map((general) => general.special2Code),
|
||||
'war'
|
||||
),
|
||||
loadCrewTypeDisplayNames(worldState, ctx.profile.id),
|
||||
loadItemDisplayNames(
|
||||
generalRows.flatMap((general) => [
|
||||
general.weaponCode,
|
||||
general.bookCode,
|
||||
general.horseCode,
|
||||
general.itemCode,
|
||||
])
|
||||
),
|
||||
]);
|
||||
const traitName = (code: string, names: Awaited<ReturnType<typeof loadTraitNames>>): string =>
|
||||
names.get(code)?.name ?? sanitizeInternalDisplayCode(code);
|
||||
const itemName = (code: string): string => itemNames.get(code) ?? sanitizeInternalDisplayCode(code);
|
||||
|
||||
const generals = generalRows.map((general) => {
|
||||
const meta =
|
||||
@@ -108,6 +145,11 @@ export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
const value = meta[key];
|
||||
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
||||
};
|
||||
const storedDedicationLevel = metaNumber('dedlevel');
|
||||
const dedicationLevel =
|
||||
storedDedicationLevel > 0
|
||||
? storedDedicationLevel
|
||||
: Math.max(0, Math.min(Math.ceil(Math.sqrt(general.dedication) / 10), maxDedicationLevel));
|
||||
return {
|
||||
id: general.id,
|
||||
name: general.name,
|
||||
@@ -115,6 +157,7 @@ export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
imageServer: general.imageServer,
|
||||
npcState: general.npcState,
|
||||
officerLevel: general.officerLevel,
|
||||
officerLevelText: resolveOfficerLevelName(general.officerLevel, nation.level),
|
||||
cityId: general.cityId,
|
||||
turnTime: formatDateTime(general.turnTime),
|
||||
recentWar: formatDateTime(general.recentWarTime),
|
||||
@@ -134,19 +177,28 @@ export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
atmos: general.atmos,
|
||||
age: general.age,
|
||||
crewTypeId: general.crewTypeId,
|
||||
crewTypeName: crewTypeNames.get(general.crewTypeId) ?? '-',
|
||||
equipment: {
|
||||
weapon: general.weaponCode,
|
||||
book: general.bookCode,
|
||||
horse: general.horseCode,
|
||||
item: general.itemCode,
|
||||
},
|
||||
equipmentNames: {
|
||||
weapon: itemName(general.weaponCode),
|
||||
book: itemName(general.bookCode),
|
||||
horse: itemName(general.horseCode),
|
||||
item: itemName(general.itemCode),
|
||||
},
|
||||
traits: {
|
||||
personal: general.personalCode,
|
||||
specialDomestic: general.specialCode,
|
||||
specialWar: general.special2Code,
|
||||
personal: traitName(general.personalCode, personalityNames),
|
||||
specialDomestic: traitName(general.specialCode, domesticNames),
|
||||
specialWar: traitName(general.special2Code, warNames),
|
||||
},
|
||||
progression: {
|
||||
experienceLevel: metaNumber('explevel'),
|
||||
dedicationLevel,
|
||||
dedicationText: resolveDedicationLevelName(dedicationLevel, maxDedicationLevel),
|
||||
statExperience: {
|
||||
leadership: metaNumber('leadership_exp'),
|
||||
strength: metaNumber('strength_exp'),
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import { asNumber, asRecord } from '@sammo-ts/common';
|
||||
|
||||
import { accessAuthedProcedure } from '../../../trpc.js';
|
||||
import { resolveDedicationLevelName, sanitizeInternalDisplayCode } from '../../../services/gameDisplayNames.js';
|
||||
import { getMyGeneral } from '../../shared/general.js';
|
||||
import {
|
||||
assertNationAccess,
|
||||
@@ -15,8 +17,8 @@ const experienceLevel = (experience: number): number =>
|
||||
0,
|
||||
Math.min(100, experience < 1000 ? Math.floor(experience / 100) : Math.floor(Math.sqrt(experience / 10)))
|
||||
);
|
||||
const dedicationLevel = (dedication: number): number =>
|
||||
Math.max(0, Math.min(10, Math.ceil(Math.sqrt(dedication) / 10)));
|
||||
const dedicationLevel = (dedication: number, maxLevel: number): number =>
|
||||
Math.max(0, Math.min(maxLevel, Math.ceil(Math.sqrt(dedication) / 10)));
|
||||
|
||||
export const getGeneralList = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
const general = await getMyGeneral(ctx);
|
||||
@@ -85,14 +87,22 @@ export const getGeneralList = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
const accessByGeneral = new Map(accessRows.map((entry) => [entry.generalId, entry.refreshScoreTotal]));
|
||||
const nationTrait = (await loadTraitNames([nation.typeCode], 'nation')).get(nation.typeCode);
|
||||
const permission = resolveNationPermission(general, nation.meta, true);
|
||||
const config = asRecord(worldState?.config);
|
||||
const maxDedicationLevel = Math.max(0, Math.trunc(asNumber(asRecord(config.const).maxDedLevel, 30)));
|
||||
const visibleList = list.map((entry) => {
|
||||
const entryDedicationLevel = dedicationLevel(entry.dedication, maxDedicationLevel);
|
||||
const dedicationDisplay = {
|
||||
dedicationLevel: entryDedicationLevel,
|
||||
dedicationText: resolveDedicationLevelName(entryDedicationLevel, maxDedicationLevel),
|
||||
bill: entryDedicationLevel * 200 + 400,
|
||||
};
|
||||
const { permission: _targetPermission, ...safeEntry } = entry;
|
||||
if (permission >= 1) {
|
||||
return {
|
||||
...safeEntry,
|
||||
refreshScoreTotal: accessByGeneral.get(entry.id) ?? 0,
|
||||
experienceLevel: experienceLevel(entry.experience),
|
||||
dedicationLevel: dedicationLevel(entry.dedication),
|
||||
...dedicationDisplay,
|
||||
};
|
||||
}
|
||||
const { crew: _crew, experience: _experience, dedication: _dedication, ...visible } = safeEntry;
|
||||
@@ -105,7 +115,7 @@ export const getGeneralList = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
officerCity: 0,
|
||||
officerCityName: null,
|
||||
experienceLevel: experienceLevel(entry.experience),
|
||||
dedicationLevel: dedicationLevel(entry.dedication),
|
||||
...dedicationDisplay,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -118,7 +128,7 @@ export const getGeneralList = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
typeCode: nation.typeCode,
|
||||
type: {
|
||||
key: nation.typeCode,
|
||||
name: nationTrait?.name ?? nation.typeCode,
|
||||
name: nationTrait?.name ?? sanitizeInternalDisplayCode(nation.typeCode),
|
||||
info: nationTrait?.info ?? '',
|
||||
},
|
||||
capitalCityId: nation.capitalCityId ?? 0,
|
||||
|
||||
@@ -28,6 +28,7 @@ import {
|
||||
|
||||
import type { GameApiContext, InputJsonValue, WorldStateRow } from '../../context.js';
|
||||
import { purifyNationHtml } from '../../security/nationHtml.js';
|
||||
import { sanitizeInternalDisplayCode } from '../../services/gameDisplayNames.js';
|
||||
import { resolveSecretPermission } from '../shared/secretPermission.js';
|
||||
|
||||
export type PermissionKind = 'normal' | 'ambassador' | 'auditor';
|
||||
@@ -313,10 +314,7 @@ export const loadTraitNames = async (keys: Array<string | null>, kind: keyof Tra
|
||||
}
|
||||
}
|
||||
if (eventFiltered.length) {
|
||||
const modules = await loadEventDomesticTraitModules(
|
||||
eventFiltered,
|
||||
new EventDomesticTraitLoader()
|
||||
);
|
||||
const modules = await loadEventDomesticTraitModules(eventFiltered, new EventDomesticTraitLoader());
|
||||
for (const module of modules) {
|
||||
cache.set(module.key, { name: module.name, info: module.info ?? '' });
|
||||
}
|
||||
@@ -513,21 +511,21 @@ export const mapGeneralList = async (
|
||||
personality: personalityKey
|
||||
? {
|
||||
key: personalityKey,
|
||||
name: personalityMap.get(personalityKey)?.name ?? personalityKey,
|
||||
name: personalityMap.get(personalityKey)?.name ?? sanitizeInternalDisplayCode(personalityKey),
|
||||
info: personalityMap.get(personalityKey)?.info ?? '',
|
||||
}
|
||||
: null,
|
||||
specialDomestic: domesticKey
|
||||
? {
|
||||
key: domesticKey,
|
||||
name: domesticMap.get(domesticKey)?.name ?? domesticKey,
|
||||
name: domesticMap.get(domesticKey)?.name ?? sanitizeInternalDisplayCode(domesticKey),
|
||||
info: domesticMap.get(domesticKey)?.info ?? '',
|
||||
}
|
||||
: null,
|
||||
specialWar: warKey
|
||||
? {
|
||||
key: warKey,
|
||||
name: warMap.get(warKey)?.name ?? warKey,
|
||||
name: warMap.get(warKey)?.name ?? sanitizeInternalDisplayCode(warKey),
|
||||
info: warMap.get(warKey)?.info ?? '',
|
||||
}
|
||||
: null,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { zWorldStateConfig, zWorldStateMeta } from '../../context.js';
|
||||
import { loadMapLayout } from '../../maps/mapLayout.js';
|
||||
import { loadPublicMap } from '../../maps/worldMap.js';
|
||||
import { accessPages, recordGeneralAccess } from '../../services/generalAccess.js';
|
||||
import { sanitizeInternalDisplayCode } from '../../services/gameDisplayNames.js';
|
||||
import { accessInputProcedure, procedure, router, sessionActivityProcedure } from '../../trpc.js';
|
||||
import { loadTraitNames } from '../nation/shared.js';
|
||||
|
||||
@@ -478,174 +479,170 @@ export const publicRouter = router({
|
||||
}));
|
||||
}),
|
||||
getNpcList: accessInputProcedure(
|
||||
z
|
||||
.object({
|
||||
sort: z.number().int().min(1).max(8).catch(1).optional(),
|
||||
includeAllWithToken: z.boolean().optional(),
|
||||
})
|
||||
.optional()
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const sort = (input?.sort ?? 1) as NpcListSort;
|
||||
const includeAllWithToken = input?.includeAllWithToken === true;
|
||||
if (includeAllWithToken && !ctx.auth) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
||||
}
|
||||
const now = new Date(Math.floor(Date.now() / 1000) * 1000);
|
||||
const poolGeneralIds = includeAllWithToken
|
||||
? []
|
||||
: (
|
||||
await ctx.db.selectPoolEntry.findMany({
|
||||
where: { generalId: { not: null } },
|
||||
select: { generalId: true },
|
||||
})
|
||||
).flatMap(({ generalId }) => (generalId === null ? [] : [generalId]));
|
||||
const [generals, nations, activeTokens, worldState] = await Promise.all([
|
||||
ctx.db.general.findMany({
|
||||
...(includeAllWithToken
|
||||
? {}
|
||||
: {
|
||||
where: {
|
||||
OR: [
|
||||
{ npcState: 1 },
|
||||
{ npcState: 0, id: { in: poolGeneralIds } },
|
||||
],
|
||||
},
|
||||
}),
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
picture: true,
|
||||
imageServer: true,
|
||||
npcState: true,
|
||||
age: true,
|
||||
officerLevel: true,
|
||||
nationId: true,
|
||||
leadership: true,
|
||||
strength: true,
|
||||
intel: true,
|
||||
experience: true,
|
||||
dedication: true,
|
||||
personalCode: true,
|
||||
specialCode: true,
|
||||
special2Code: true,
|
||||
meta: true,
|
||||
},
|
||||
orderBy: { id: 'asc' },
|
||||
}),
|
||||
ctx.db.nation.findMany({
|
||||
select: { id: true, name: true, level: true },
|
||||
}),
|
||||
includeAllWithToken
|
||||
? ctx.db.npcSelectionToken.findMany({
|
||||
where: { validUntil: { gte: now } },
|
||||
select: { pickResult: true },
|
||||
})
|
||||
: [],
|
||||
includeAllWithToken
|
||||
? ctx.db.worldState.findFirst({
|
||||
select: { config: true },
|
||||
})
|
||||
: null,
|
||||
]);
|
||||
z
|
||||
.object({
|
||||
sort: z.number().int().min(1).max(8).catch(1).optional(),
|
||||
includeAllWithToken: z.boolean().optional(),
|
||||
})
|
||||
.optional()
|
||||
).query(async ({ ctx, input }) => {
|
||||
const sort = (input?.sort ?? 1) as NpcListSort;
|
||||
const includeAllWithToken = input?.includeAllWithToken === true;
|
||||
if (includeAllWithToken && !ctx.auth) {
|
||||
throw new TRPCError({ code: 'UNAUTHORIZED' });
|
||||
}
|
||||
const now = new Date(Math.floor(Date.now() / 1000) * 1000);
|
||||
const poolGeneralIds = includeAllWithToken
|
||||
? []
|
||||
: (
|
||||
await ctx.db.selectPoolEntry.findMany({
|
||||
where: { generalId: { not: null } },
|
||||
select: { generalId: true },
|
||||
})
|
||||
).flatMap(({ generalId }) => (generalId === null ? [] : [generalId]));
|
||||
const [generals, nations, activeTokens, worldState] = await Promise.all([
|
||||
ctx.db.general.findMany({
|
||||
...(includeAllWithToken
|
||||
? {}
|
||||
: {
|
||||
where: {
|
||||
OR: [{ npcState: 1 }, { npcState: 0, id: { in: poolGeneralIds } }],
|
||||
},
|
||||
}),
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
picture: true,
|
||||
imageServer: true,
|
||||
npcState: true,
|
||||
age: true,
|
||||
officerLevel: true,
|
||||
nationId: true,
|
||||
leadership: true,
|
||||
strength: true,
|
||||
intel: true,
|
||||
experience: true,
|
||||
dedication: true,
|
||||
personalCode: true,
|
||||
specialCode: true,
|
||||
special2Code: true,
|
||||
meta: true,
|
||||
},
|
||||
orderBy: { id: 'asc' },
|
||||
}),
|
||||
ctx.db.nation.findMany({
|
||||
select: { id: true, name: true, level: true },
|
||||
}),
|
||||
includeAllWithToken
|
||||
? ctx.db.npcSelectionToken.findMany({
|
||||
where: { validUntil: { gte: now } },
|
||||
select: { pickResult: true },
|
||||
})
|
||||
: [],
|
||||
includeAllWithToken
|
||||
? ctx.db.worldState.findFirst({
|
||||
select: { config: true },
|
||||
})
|
||||
: null,
|
||||
]);
|
||||
|
||||
const personalityKeys = generals.map((general) => normalizeTraitKey(general.personalCode));
|
||||
const domesticKeys = generals.map((general) => normalizeTraitKey(general.specialCode));
|
||||
const warKeys = generals.map((general) => normalizeTraitKey(general.special2Code));
|
||||
const [personalityMap, domesticMap, warMap] = await Promise.all([
|
||||
loadTraitNames(personalityKeys, 'personality'),
|
||||
loadTraitNames(domesticKeys, 'domestic'),
|
||||
loadTraitNames(warKeys, 'war'),
|
||||
]);
|
||||
const nationMap = new Map(nations.map((nation) => [nation.id, nation]));
|
||||
const worldConfig = asRecord(worldState?.config);
|
||||
const worldConstants = asRecord(worldConfig.const);
|
||||
const maxLevel = Math.max(0, Math.floor(asNumber(worldConstants.maxLevel, 255)));
|
||||
const maxDedLevel = Math.max(0, Math.floor(asNumber(worldConstants.maxDedLevel, 30)));
|
||||
const personalityKeys = generals.map((general) => normalizeTraitKey(general.personalCode));
|
||||
const domesticKeys = generals.map((general) => normalizeTraitKey(general.specialCode));
|
||||
const warKeys = generals.map((general) => normalizeTraitKey(general.special2Code));
|
||||
const [personalityMap, domesticMap, warMap] = await Promise.all([
|
||||
loadTraitNames(personalityKeys, 'personality'),
|
||||
loadTraitNames(domesticKeys, 'domestic'),
|
||||
loadTraitNames(warKeys, 'war'),
|
||||
]);
|
||||
const nationMap = new Map(nations.map((nation) => [nation.id, nation]));
|
||||
const worldConfig = asRecord(worldState?.config);
|
||||
const worldConstants = asRecord(worldConfig.const);
|
||||
const maxLevel = Math.max(0, Math.floor(asNumber(worldConstants.maxLevel, 255)));
|
||||
const maxDedLevel = Math.max(0, Math.floor(asNumber(worldConstants.maxDedLevel, 30)));
|
||||
|
||||
// Legacy a_npcList.php shows select_pool humans first and possessed npc=1 rows.
|
||||
// Unpossessed npc=2 candidates belong only to the token-aware selection screen.
|
||||
// selection list instead consumes the raw id-ordered full list before its own comparator.
|
||||
const sourceRows = includeAllWithToken
|
||||
? generals
|
||||
: [
|
||||
...generals.filter((general) => general.npcState === 0),
|
||||
...generals.filter((general) => general.npcState === 1),
|
||||
];
|
||||
const rows = sourceRows.map((general) => {
|
||||
const meta = asRecord(general.meta);
|
||||
const personalityKey = normalizeTraitKey(general.personalCode);
|
||||
const domesticKey = normalizeTraitKey(general.specialCode);
|
||||
const warKey = normalizeTraitKey(general.special2Code);
|
||||
const ownerName =
|
||||
general.npcState === 1
|
||||
? typeof meta.owner_name === 'string'
|
||||
? meta.owner_name
|
||||
: typeof meta.ownerName === 'string'
|
||||
? meta.ownerName
|
||||
: ''
|
||||
: '';
|
||||
|
||||
return {
|
||||
id: general.id,
|
||||
name: general.name,
|
||||
picture: general.picture,
|
||||
imageServer: general.imageServer,
|
||||
npcState: general.npcState,
|
||||
ownerName,
|
||||
age: general.age,
|
||||
level: includeAllWithToken
|
||||
? resolveExperienceLevel(general.experience, maxLevel)
|
||||
: readFiniteMetaNumber(meta, 'explevel'),
|
||||
officerLevel: general.officerLevel,
|
||||
killturn: readFiniteMetaNumber(meta, 'killturn'),
|
||||
nationId: general.nationId,
|
||||
nationName: nationMap.get(general.nationId)?.name ?? '-',
|
||||
nationLevel: nationMap.get(general.nationId)?.level ?? 0,
|
||||
personality: personalityKey
|
||||
? {
|
||||
key: personalityKey,
|
||||
name: personalityMap.get(personalityKey)?.name ?? personalityKey,
|
||||
info: personalityMap.get(personalityKey)?.info ?? '',
|
||||
}
|
||||
: null,
|
||||
specialDomestic: domesticKey
|
||||
? {
|
||||
key: domesticKey,
|
||||
name: domesticMap.get(domesticKey)?.name ?? domesticKey,
|
||||
info: domesticMap.get(domesticKey)?.info ?? '',
|
||||
}
|
||||
: null,
|
||||
specialWar: warKey
|
||||
? {
|
||||
key: warKey,
|
||||
name: warMap.get(warKey)?.name ?? warKey,
|
||||
info: warMap.get(warKey)?.info ?? '',
|
||||
}
|
||||
: null,
|
||||
statTotal: general.leadership + general.strength + general.intel,
|
||||
leadership: general.leadership,
|
||||
strength: general.strength,
|
||||
intelligence: general.intel,
|
||||
experience: general.experience,
|
||||
experienceText: resolveHonorText(general.experience),
|
||||
dedication: general.dedication,
|
||||
dedicationText: resolveDedicationText(general.dedication, maxDedLevel),
|
||||
};
|
||||
});
|
||||
const tokenKeepCounts = Object.fromEntries(
|
||||
activeTokens.flatMap((token) =>
|
||||
Object.entries(asRecord(token.pickResult)).flatMap(([generalId, value]) => {
|
||||
const keepCount = asNumber(asRecord(value).keepCount, Number.NaN);
|
||||
return Number.isFinite(keepCount) ? [[generalId, Math.max(0, Math.floor(keepCount))]] : [];
|
||||
})
|
||||
)
|
||||
);
|
||||
// Legacy a_npcList.php shows select_pool humans first and possessed npc=1 rows.
|
||||
// Unpossessed npc=2 candidates belong only to the token-aware selection screen.
|
||||
// selection list instead consumes the raw id-ordered full list before its own comparator.
|
||||
const sourceRows = includeAllWithToken
|
||||
? generals
|
||||
: [
|
||||
...generals.filter((general) => general.npcState === 0),
|
||||
...generals.filter((general) => general.npcState === 1),
|
||||
];
|
||||
const rows = sourceRows.map((general) => {
|
||||
const meta = asRecord(general.meta);
|
||||
const personalityKey = normalizeTraitKey(general.personalCode);
|
||||
const domesticKey = normalizeTraitKey(general.specialCode);
|
||||
const warKey = normalizeTraitKey(general.special2Code);
|
||||
const ownerName =
|
||||
general.npcState === 1
|
||||
? typeof meta.owner_name === 'string'
|
||||
? meta.owner_name
|
||||
: typeof meta.ownerName === 'string'
|
||||
? meta.ownerName
|
||||
: ''
|
||||
: '';
|
||||
|
||||
return {
|
||||
sort,
|
||||
generals: includeAllWithToken ? rows : sortNpcList(rows, sort),
|
||||
tokenKeepCounts,
|
||||
id: general.id,
|
||||
name: general.name,
|
||||
picture: general.picture,
|
||||
imageServer: general.imageServer,
|
||||
npcState: general.npcState,
|
||||
ownerName,
|
||||
age: general.age,
|
||||
level: includeAllWithToken
|
||||
? resolveExperienceLevel(general.experience, maxLevel)
|
||||
: readFiniteMetaNumber(meta, 'explevel'),
|
||||
officerLevel: general.officerLevel,
|
||||
killturn: readFiniteMetaNumber(meta, 'killturn'),
|
||||
nationId: general.nationId,
|
||||
nationName: nationMap.get(general.nationId)?.name ?? '-',
|
||||
nationLevel: nationMap.get(general.nationId)?.level ?? 0,
|
||||
personality: personalityKey
|
||||
? {
|
||||
key: personalityKey,
|
||||
name: personalityMap.get(personalityKey)?.name ?? sanitizeInternalDisplayCode(personalityKey),
|
||||
info: personalityMap.get(personalityKey)?.info ?? '',
|
||||
}
|
||||
: null,
|
||||
specialDomestic: domesticKey
|
||||
? {
|
||||
key: domesticKey,
|
||||
name: domesticMap.get(domesticKey)?.name ?? sanitizeInternalDisplayCode(domesticKey),
|
||||
info: domesticMap.get(domesticKey)?.info ?? '',
|
||||
}
|
||||
: null,
|
||||
specialWar: warKey
|
||||
? {
|
||||
key: warKey,
|
||||
name: warMap.get(warKey)?.name ?? sanitizeInternalDisplayCode(warKey),
|
||||
info: warMap.get(warKey)?.info ?? '',
|
||||
}
|
||||
: null,
|
||||
statTotal: general.leadership + general.strength + general.intel,
|
||||
leadership: general.leadership,
|
||||
strength: general.strength,
|
||||
intelligence: general.intel,
|
||||
experience: general.experience,
|
||||
experienceText: resolveHonorText(general.experience),
|
||||
dedication: general.dedication,
|
||||
dedicationText: resolveDedicationText(general.dedication, maxDedLevel),
|
||||
};
|
||||
}),
|
||||
});
|
||||
const tokenKeepCounts = Object.fromEntries(
|
||||
activeTokens.flatMap((token) =>
|
||||
Object.entries(asRecord(token.pickResult)).flatMap(([generalId, value]) => {
|
||||
const keepCount = asNumber(asRecord(value).keepCount, Number.NaN);
|
||||
return Number.isFinite(keepCount) ? [[generalId, Math.max(0, Math.floor(keepCount))]] : [];
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
sort,
|
||||
generals: includeAllWithToken ? rows : sortNpcList(rows, sort),
|
||||
tokenKeepCounts,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -2,6 +2,7 @@ import { asRecord } from '@sammo-ts/common';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { accessAuthedInputProcedure, authedProcedure } from '../../trpc.js';
|
||||
import { sanitizeInternalDisplayCode } from '../../services/gameDisplayNames.js';
|
||||
import { loadTraitNames } from '../nation/shared.js';
|
||||
import { getMyGeneral } from '../shared/general.js';
|
||||
import { resolveSecretPermission } from '../shared/secretPermission.js';
|
||||
@@ -172,7 +173,7 @@ export const getNationDirectory = authedProcedure.query(async ({ ctx }) => {
|
||||
level: nation.level,
|
||||
type: {
|
||||
key: nation.typeCode,
|
||||
name: nationTypeNames.get(nation.typeCode)?.name ?? nation.typeCode,
|
||||
name: nationTypeNames.get(nation.typeCode)?.name ?? sanitizeInternalDisplayCode(nation.typeCode),
|
||||
},
|
||||
power: readMetaNumber(nation.meta, 'power'),
|
||||
capitalCityId: nation.capitalCityId ?? 0,
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
import { isItemKey, ItemLoader } from '@sammo-ts/logic';
|
||||
import { loadUnitSetDefinitionByName } from '@sammo-ts/game-engine/scenario/unitSetLoader.js';
|
||||
|
||||
import type { WorldStateRow } from '../context.js';
|
||||
|
||||
const NATION_LEVEL_NAMES: Record<number, string> = {
|
||||
0: '방랑군',
|
||||
1: '호족',
|
||||
2: '군벌',
|
||||
3: '주자사',
|
||||
4: '주목',
|
||||
5: '공',
|
||||
6: '왕',
|
||||
7: '황제',
|
||||
};
|
||||
|
||||
const CITY_LEVEL_NAMES: Record<number, string> = {
|
||||
1: '수',
|
||||
2: '진',
|
||||
3: '관',
|
||||
4: '이',
|
||||
5: '소',
|
||||
6: '중',
|
||||
7: '대',
|
||||
8: '특',
|
||||
};
|
||||
|
||||
const REGION_NAMES: Record<number, string> = {
|
||||
1: '하북',
|
||||
2: '중원',
|
||||
3: '서북',
|
||||
4: '서촉',
|
||||
5: '남중',
|
||||
6: '초',
|
||||
7: '오월',
|
||||
8: '동이',
|
||||
};
|
||||
|
||||
const OFFICER_LEVEL_NAMES: Record<number, string> = {
|
||||
12: '군주',
|
||||
11: '참모',
|
||||
10: '제1장군',
|
||||
9: '제1모사',
|
||||
8: '제2장군',
|
||||
7: '제2모사',
|
||||
6: '제3장군',
|
||||
5: '제3모사',
|
||||
4: '태수',
|
||||
3: '군사',
|
||||
2: '종사',
|
||||
1: '일반',
|
||||
0: '재야',
|
||||
};
|
||||
|
||||
const OFFICER_LEVEL_NAMES_BY_NATION_LEVEL: Record<number, Record<number, string>> = {
|
||||
7: { 12: '황제', 11: '승상', 10: '표기장군', 9: '사공', 8: '거기장군', 7: '태위', 6: '위장군', 5: '사도' },
|
||||
6: { 12: '왕', 11: '광록훈', 10: '좌장군', 9: '상서령', 8: '우장군', 7: '중서령', 6: '전장군', 5: '비서령' },
|
||||
5: { 12: '공', 11: '광록대부', 10: '안국장군', 9: '집금오', 8: '파로장군', 7: '소부' },
|
||||
4: { 12: '주목', 11: '태사령', 10: '아문장군', 9: '낭중', 8: '호군', 7: '종사중랑' },
|
||||
3: { 12: '주자사', 11: '주부', 10: '편장군', 9: '간의대부' },
|
||||
2: { 12: '군벌', 11: '참모', 10: '비장군', 9: '부참모' },
|
||||
1: { 12: '영주', 11: '참모' },
|
||||
0: { 12: '두목', 11: '부두목' },
|
||||
};
|
||||
|
||||
export const sanitizeInternalDisplayCode = (value: string | null | undefined): string => {
|
||||
if (!value || value === 'None') {
|
||||
return '-';
|
||||
}
|
||||
if (/^\d+$/u.test(value)) {
|
||||
return '-';
|
||||
}
|
||||
return value.replace(/^che_(?:event_)?/u, '');
|
||||
};
|
||||
|
||||
export const resolveNationLevelName = (level: number): string => NATION_LEVEL_NAMES[level] ?? '-';
|
||||
|
||||
export const resolveCityLevelName = (level: number): string => CITY_LEVEL_NAMES[level] ?? '-';
|
||||
|
||||
export const resolveRegionName = (region: number): string => REGION_NAMES[region] ?? '-';
|
||||
|
||||
export const resolveOfficerLevelName = (officerLevel: number, nationLevel?: number): string => {
|
||||
if (officerLevel < 5) {
|
||||
return OFFICER_LEVEL_NAMES[officerLevel] ?? '-';
|
||||
}
|
||||
if (nationLevel === undefined) {
|
||||
return OFFICER_LEVEL_NAMES[officerLevel] ?? '-';
|
||||
}
|
||||
return OFFICER_LEVEL_NAMES_BY_NATION_LEVEL[nationLevel]?.[officerLevel] ?? '-';
|
||||
};
|
||||
|
||||
export const resolveDedicationLevelName = (dedicationLevel: number, maxDedicationLevel: number): string => {
|
||||
if (dedicationLevel <= 0) {
|
||||
return '무품관';
|
||||
}
|
||||
return `${Math.max(1, maxDedicationLevel - dedicationLevel + 1)}품관`;
|
||||
};
|
||||
|
||||
const resolveUnitSetName = (world: Pick<WorldStateRow, 'config'> | null, fallback: string): string => {
|
||||
const config = asRecord(world?.config);
|
||||
const environment = asRecord(config.environment ?? config.map);
|
||||
return typeof environment.unitSet === 'string' && environment.unitSet.trim() ? environment.unitSet : fallback;
|
||||
};
|
||||
|
||||
const crewTypeNameCache = new Map<string, Promise<Map<number, string>>>();
|
||||
|
||||
export const loadCrewTypeDisplayNames = (
|
||||
world: Pick<WorldStateRow, 'config'> | null,
|
||||
fallback: string
|
||||
): Promise<Map<number, string>> => {
|
||||
const unitSetName = resolveUnitSetName(world, fallback);
|
||||
const cached = crewTypeNameCache.get(unitSetName);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
const pending = loadUnitSetDefinitionByName(unitSetName)
|
||||
.then((definition) => new Map((definition.crewTypes ?? []).map((crewType) => [crewType.id, crewType.name])))
|
||||
.catch(() => new Map<number, string>());
|
||||
crewTypeNameCache.set(unitSetName, pending);
|
||||
return pending;
|
||||
};
|
||||
|
||||
const itemLoader = new ItemLoader();
|
||||
|
||||
export const loadItemDisplayNames = async (values: Array<string | null | undefined>): Promise<Map<string, string>> => {
|
||||
const keys = Array.from(new Set(values.filter((value): value is string => Boolean(value) && value !== 'None')));
|
||||
const entries = await Promise.all(
|
||||
keys.map(async (key) => {
|
||||
if (!isItemKey(key)) {
|
||||
return [key, sanitizeInternalDisplayCode(key)] as const;
|
||||
}
|
||||
const item = await itemLoader.load(key).catch(() => null);
|
||||
return [key, item?.name ?? sanitizeInternalDisplayCode(key)] as const;
|
||||
})
|
||||
);
|
||||
return new Map(entries);
|
||||
};
|
||||
@@ -156,7 +156,8 @@ describe('archive.myPastPlays', () => {
|
||||
leadership: 80,
|
||||
strength: 70,
|
||||
officerLevel: 8,
|
||||
personal: '3',
|
||||
officerLevelText: '제2장군',
|
||||
personal: '-',
|
||||
historyCount: 2,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
@@ -166,9 +167,10 @@ describe('archive.myPastPlays', () => {
|
||||
strength: 71,
|
||||
intel: 61,
|
||||
officerLevel: 7,
|
||||
personal: 'che_의리',
|
||||
special: 'che_상재',
|
||||
special2: 'che_신산',
|
||||
officerLevelText: '제2모사',
|
||||
personal: '의리',
|
||||
special: '상재',
|
||||
special2: '신산',
|
||||
historyCount: 1,
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
resolveCityLevelName,
|
||||
resolveDedicationLevelName,
|
||||
resolveNationLevelName,
|
||||
resolveOfficerLevelName,
|
||||
resolveRegionName,
|
||||
sanitizeInternalDisplayCode,
|
||||
} from '../src/services/gameDisplayNames.js';
|
||||
|
||||
describe('Ref GUI display names', () => {
|
||||
it('maps nation, city, region, office, and dedication levels to Ref labels', () => {
|
||||
expect(resolveNationLevelName(3)).toBe('주자사');
|
||||
expect(resolveCityLevelName(8)).toBe('특');
|
||||
expect(resolveRegionName(2)).toBe('중원');
|
||||
expect(resolveOfficerLevelName(9, 3)).toBe('간의대부');
|
||||
expect(resolveOfficerLevelName(5, 3)).toBe('-');
|
||||
expect(resolveOfficerLevelName(5)).toBe('제3모사');
|
||||
expect(resolveDedicationLevelName(2, 30)).toBe('29품관');
|
||||
expect(resolveDedicationLevelName(0, 30)).toBe('무품관');
|
||||
});
|
||||
|
||||
it('never exposes internal prefixes or numeric fallback codes', () => {
|
||||
expect(sanitizeInternalDisplayCode('che_event_의병')).toBe('의병');
|
||||
expect(sanitizeInternalDisplayCode('che_법가')).toBe('법가');
|
||||
expect(sanitizeInternalDisplayCode('3')).toBe('-');
|
||||
expect(resolveNationLevelName(99)).toBe('-');
|
||||
expect(resolveCityLevelName(99)).toBe('-');
|
||||
expect(resolveRegionName(99)).toBe('-');
|
||||
});
|
||||
});
|
||||
@@ -248,6 +248,55 @@ describe('in-game my information ownership', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('returns Ref display names instead of numeric levels and internal codes for the main GUI', async () => {
|
||||
const fixture = createContext({
|
||||
me: buildGeneral({
|
||||
officerLevel: 9,
|
||||
crewTypeId: 1100,
|
||||
horseCode: 'che_명마_03_노새',
|
||||
meta: { explevel: 4, dedlevel: 2 },
|
||||
}),
|
||||
city: {
|
||||
id: 1,
|
||||
name: '업',
|
||||
level: 8,
|
||||
nationId: 1,
|
||||
population: 1_000,
|
||||
populationMax: 2_000,
|
||||
agriculture: 100,
|
||||
agricultureMax: 200,
|
||||
commerce: 100,
|
||||
commerceMax: 200,
|
||||
security: 100,
|
||||
securityMax: 200,
|
||||
trust: 70,
|
||||
trade: 100,
|
||||
defence: 100,
|
||||
defenceMax: 200,
|
||||
wall: 100,
|
||||
wallMax: 200,
|
||||
region: 2,
|
||||
supplyState: 1,
|
||||
frontState: 0,
|
||||
},
|
||||
});
|
||||
|
||||
await expect(appRouter.createCaller(fixture.context).general.me()).resolves.toMatchObject({
|
||||
general: {
|
||||
officerLevelText: '간의대부',
|
||||
crewTypeName: '보병',
|
||||
progression: { experienceLevel: 4, dedicationLevel: 2, dedicationText: '29품관' },
|
||||
itemNames: { horse: '노새(+3)' },
|
||||
},
|
||||
city: { levelName: '특', regionName: '중원', nationName: '위' },
|
||||
nation: {
|
||||
levelName: '주자사',
|
||||
typeName: '법가',
|
||||
capitalCityName: '업',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the Ref-style neutral nation frame and trait display names on the main read model', async () => {
|
||||
const fixture = createContext({
|
||||
me: buildGeneral({
|
||||
@@ -542,8 +591,14 @@ describe('battle-center general and user permissions', () => {
|
||||
id: 7,
|
||||
picture: 'default.jpg',
|
||||
imageServer: 0,
|
||||
officerLevelText: '일반',
|
||||
crewTypeName: '-',
|
||||
equipmentNames: { weapon: '-', book: '-', horse: '-', item: '-' },
|
||||
traits: { personal: '-', specialDomestic: '-', specialWar: '-' },
|
||||
progression: {
|
||||
experienceLevel: 0,
|
||||
dedicationLevel: 1,
|
||||
dedicationText: '30품관',
|
||||
statExperience: { leadership: 0, strength: 0, intelligence: 0 },
|
||||
statUpgradeLimit: 20,
|
||||
dex: [0, 0, 0, 0, 0],
|
||||
|
||||
@@ -117,6 +117,9 @@ describe('nation general and secret office permissions', () => {
|
||||
cityName: null,
|
||||
troopName: null,
|
||||
refreshScoreTotal: 10,
|
||||
dedicationLevel: 1,
|
||||
dedicationText: '30품관',
|
||||
bill: 600,
|
||||
});
|
||||
expect(result.generals[0]).not.toHaveProperty('crew');
|
||||
await expect(caller.nation.getSecretGeneralList()).rejects.toMatchObject({ code: 'FORBIDDEN' });
|
||||
|
||||
Reference in New Issue
Block a user