fix: preserve best-general rank data parity
This commit is contained in:
@@ -8,8 +8,29 @@ import { resolveUniqueConfig } from '@sammo-ts/logic/rewards/uniqueLottery.js';
|
||||
|
||||
import { authedProcedure, procedure, router } from '../../trpc.js';
|
||||
|
||||
const DEFAULT_BG_COLOR = '#2b2b2b';
|
||||
const DEFAULT_BG_COLOR = '#330000';
|
||||
const DEFAULT_FG_COLOR = '#ffffff';
|
||||
const NEUTRAL_BG_COLOR = '#000000';
|
||||
const LEGACY_WHITE_TEXT_COLORS = new Set([
|
||||
'',
|
||||
'#330000',
|
||||
'#FF0000',
|
||||
'#800000',
|
||||
'#A0522D',
|
||||
'#FF6347',
|
||||
'#808000',
|
||||
'#008000',
|
||||
'#2E8B57',
|
||||
'#008080',
|
||||
'#6495ED',
|
||||
'#0000FF',
|
||||
'#000080',
|
||||
'#483D8B',
|
||||
'#7B68EE',
|
||||
'#800080',
|
||||
'#A9A9A9',
|
||||
'#000000',
|
||||
]);
|
||||
|
||||
const readMetaNumber = (value: unknown): number => {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
@@ -24,7 +45,17 @@ const readMetaNumber = (value: unknown): number => {
|
||||
return 0;
|
||||
};
|
||||
|
||||
const percentText = (value: number): string => `${(value * 100).toFixed(2)}%`;
|
||||
export const formatLegacyRankingNumber = (value: number, fractionDigits = 0): string =>
|
||||
new Intl.NumberFormat('en-US', {
|
||||
minimumFractionDigits: fractionDigits,
|
||||
maximumFractionDigits: fractionDigits,
|
||||
useGrouping: true,
|
||||
}).format(value);
|
||||
|
||||
export const resolveLegacyTextColor = (backgroundColor: string): string =>
|
||||
LEGACY_WHITE_TEXT_COLORS.has(backgroundColor.toUpperCase()) ? '#ffffff' : '#000000';
|
||||
|
||||
const percentText = (value: number): string => `${formatLegacyRankingNumber(value * 100, 2)}%`;
|
||||
|
||||
const readOwnerDisplayName = (value: unknown): string | null => {
|
||||
const meta = asRecord(value);
|
||||
@@ -72,6 +103,7 @@ export const rankingRouter = router({
|
||||
ctx.db.nation.findMany({ select: { id: true, name: true, color: true } }),
|
||||
ctx.db.general.findMany({
|
||||
where: { npcState: npcFilter },
|
||||
orderBy: { id: 'asc' },
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
@@ -133,11 +165,11 @@ export const rankingRouter = router({
|
||||
}
|
||||
return (r.killcrew_person ?? 0) / Math.max(1, r.deathcrew_person ?? 0);
|
||||
}],
|
||||
['보 병 숙 련 도', 'int', (_g, r) => r.dex1 ?? 0],
|
||||
['궁 병 숙 련 도', 'int', (_g, r) => r.dex2 ?? 0],
|
||||
['기 병 숙 련 도', 'int', (_g, r) => r.dex3 ?? 0],
|
||||
['귀 병 숙 련 도', 'int', (_g, r) => r.dex4 ?? 0],
|
||||
['차 병 숙 련 도', 'int', (_g, r) => r.dex5 ?? 0],
|
||||
['보 병 숙 련 도', 'int', (g) => readMetaNumber(asRecord(g.meta).dex1)],
|
||||
['궁 병 숙 련 도', 'int', (g) => readMetaNumber(asRecord(g.meta).dex2)],
|
||||
['기 병 숙 련 도', 'int', (g) => readMetaNumber(asRecord(g.meta).dex3)],
|
||||
['귀 병 숙 련 도', 'int', (g) => readMetaNumber(asRecord(g.meta).dex4)],
|
||||
['차 병 숙 련 도', 'int', (g) => readMetaNumber(asRecord(g.meta).dex5)],
|
||||
['전 력 전 승 률', 'percent', (_g, r) => {
|
||||
const total = (r.ttw ?? 0) + (r.ttd ?? 0) + (r.ttl ?? 0);
|
||||
if (total < 50) {
|
||||
@@ -186,17 +218,20 @@ export const rankingRouter = router({
|
||||
const ranks = rankMap.get(general.id) ?? {};
|
||||
const value = valueFn(general, ranks);
|
||||
const nation = nationMap.get(general.nationId) ?? null;
|
||||
const bgColor =
|
||||
nation?.color ?? (general.nationId === 0 ? NEUTRAL_BG_COLOR : DEFAULT_BG_COLOR);
|
||||
let display = {
|
||||
id: general.id,
|
||||
name: general.name,
|
||||
ownerName: isUnited ? readOwnerDisplayName(general.meta) : null,
|
||||
nationName: nation?.name ?? '재야',
|
||||
bgColor: nation?.color ?? DEFAULT_BG_COLOR,
|
||||
fgColor: DEFAULT_FG_COLOR,
|
||||
bgColor,
|
||||
fgColor: resolveLegacyTextColor(bgColor),
|
||||
picture: general.picture ?? null,
|
||||
imageServer: general.imageServer ?? 0,
|
||||
value,
|
||||
printValue: valueType === 'percent' ? percentText(value) : Math.floor(value).toLocaleString('ko-KR'),
|
||||
printValue:
|
||||
valueType === 'percent' ? percentText(value) : formatLegacyRankingNumber(value),
|
||||
};
|
||||
|
||||
if (!isUnited && (title === '계 략 성 공' || title === '유 산 소 모 량' || title === '유 산 획 득 량')) {
|
||||
@@ -206,7 +241,7 @@ export const rankingRouter = router({
|
||||
ownerName: null,
|
||||
nationName: '???',
|
||||
bgColor: DEFAULT_BG_COLOR,
|
||||
fgColor: DEFAULT_FG_COLOR,
|
||||
fgColor: resolveLegacyTextColor(DEFAULT_BG_COLOR),
|
||||
picture: null,
|
||||
imageServer: 0,
|
||||
};
|
||||
@@ -268,12 +303,14 @@ export const rankingRouter = router({
|
||||
})
|
||||
.map((general) => {
|
||||
const nation = nationMap.get(general.nationId) ?? null;
|
||||
const bgColor =
|
||||
nation?.color ?? (general.nationId === 0 ? NEUTRAL_BG_COLOR : DEFAULT_BG_COLOR);
|
||||
return {
|
||||
id: general.id,
|
||||
name: general.name,
|
||||
nationName: nation?.name ?? '재야',
|
||||
bgColor: nation?.color ?? DEFAULT_BG_COLOR,
|
||||
fgColor: DEFAULT_FG_COLOR,
|
||||
bgColor,
|
||||
fgColor: resolveLegacyTextColor(bgColor),
|
||||
picture: general.picture ?? null,
|
||||
imageServer: general.imageServer ?? 0,
|
||||
};
|
||||
@@ -299,7 +336,7 @@ export const rankingRouter = router({
|
||||
name: '미발견',
|
||||
nationName: '-',
|
||||
bgColor: DEFAULT_BG_COLOR,
|
||||
fgColor: DEFAULT_FG_COLOR,
|
||||
fgColor: resolveLegacyTextColor(DEFAULT_BG_COLOR),
|
||||
picture: null,
|
||||
imageServer: 0,
|
||||
},
|
||||
@@ -398,7 +435,10 @@ export const rankingRouter = router({
|
||||
picture: typeof aux.picture === 'string' ? aux.picture : null,
|
||||
imageServer: readMetaNumber(aux.imgsvr),
|
||||
value: row.value,
|
||||
printValue: type.type === 'percent' ? percentText(row.value) : Math.floor(row.value).toLocaleString('ko-KR'),
|
||||
printValue:
|
||||
type.type === 'percent'
|
||||
? percentText(row.value)
|
||||
: formatLegacyRankingNumber(row.value),
|
||||
serverName: String(aux.serverName ?? ''),
|
||||
serverIdx: readMetaNumber(aux.serverIdx),
|
||||
scenarioName: String(aux.scenarioName ?? ''),
|
||||
|
||||
@@ -9,6 +9,7 @@ import { InMemoryBattleSimTransport } from '../src/battleSim/inMemoryTransport.j
|
||||
import type { DatabaseClient, GameApiContext, GameProfile } from '../src/context.js';
|
||||
import { InMemoryTurnDaemonTransport } from '../src/daemon/inMemoryTransport.js';
|
||||
import { appRouter } from '../src/router.js';
|
||||
import { formatLegacyRankingNumber, resolveLegacyTextColor } from '../src/router/ranking/index.js';
|
||||
|
||||
const profile: GameProfile = {
|
||||
id: 'che',
|
||||
@@ -40,7 +41,7 @@ const generalRows = [
|
||||
npcState: 0,
|
||||
picture: '1.jpg',
|
||||
imageServer: 0,
|
||||
meta: { ownerName: '공개소유자' },
|
||||
meta: { ownerName: '공개소유자', dex1: 120 },
|
||||
experience: 1200,
|
||||
dedication: 900,
|
||||
horseCode: 'che_명마_15_적토마',
|
||||
@@ -56,7 +57,7 @@ const generalRows = [
|
||||
npcState: 1,
|
||||
picture: null,
|
||||
imageServer: 0,
|
||||
meta: { owner_name: '빙의소유자' },
|
||||
meta: { owner_name: '빙의소유자', dex1: 80 },
|
||||
experience: 1100,
|
||||
dedication: 800,
|
||||
horseCode: 'None',
|
||||
@@ -72,7 +73,7 @@ const generalRows = [
|
||||
npcState: 2,
|
||||
picture: null,
|
||||
imageServer: 0,
|
||||
meta: {},
|
||||
meta: { dex1: 200 },
|
||||
experience: 1300,
|
||||
dedication: 1000,
|
||||
horseCode: 'None',
|
||||
@@ -122,6 +123,9 @@ const buildContext = (options?: {
|
||||
{ generalId: 1, type: 'firenum', value: 10 },
|
||||
{ generalId: 2, type: 'firenum', value: 20 },
|
||||
{ generalId: 3, type: 'firenum', value: 30 },
|
||||
{ generalId: 1, type: 'dex1', value: 999 },
|
||||
{ generalId: 2, type: 'dex1', value: 999 },
|
||||
{ generalId: 3, type: 'dex1', value: 999 },
|
||||
],
|
||||
},
|
||||
auction: {
|
||||
@@ -218,6 +222,27 @@ describe('ranking.getBestGeneral', () => {
|
||||
const result = await appRouter.createCaller(buildContext()).ranking.getBestGeneral({ view: 'npc' });
|
||||
expect(result.sections[0]?.entries.map((entry) => entry.id)).toEqual([3]);
|
||||
});
|
||||
|
||||
it('uses the general dex columns as the legacy source of truth instead of mirrored rank rows', async () => {
|
||||
const result = await appRouter.createCaller(buildContext()).ranking.getBestGeneral({ view: 'user' });
|
||||
const dex = result.sections.find((section) => section.title === '보 병 숙 련 도');
|
||||
|
||||
expect(dex?.entries.map((entry) => [entry.id, entry.value, entry.printValue])).toEqual([
|
||||
[1, 120, '120'],
|
||||
[2, 80, '80'],
|
||||
]);
|
||||
expect(dex?.entries[0]).toMatchObject({
|
||||
bgColor: '#006400',
|
||||
fgColor: '#000000',
|
||||
});
|
||||
});
|
||||
|
||||
it('matches PHP number_format rounding and the legacy fixed color table', () => {
|
||||
expect(formatLegacyRankingNumber(1.005, 2)).toBe('1.01');
|
||||
expect(formatLegacyRankingNumber(12345.6, 2)).toBe('12,345.60');
|
||||
expect(resolveLegacyTextColor('#006400')).toBe('#000000');
|
||||
expect(resolveLegacyTextColor('#330000')).toBe('#ffffff');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ranking hall of fame', () => {
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
type LogEntryDraft,
|
||||
type MessageRecordDraft,
|
||||
} from '@sammo-ts/logic';
|
||||
import { asRecord, type RankDataType } from '@sammo-ts/common';
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
|
||||
import type { TurnDaemonCommandResult, TurnDaemonHooks } from '../lifecycle/types.js';
|
||||
import type { InMemoryTurnWorld } from './inMemoryWorld.js';
|
||||
@@ -33,6 +33,7 @@ import { persistGeneralLifecycleEvents } from './generalTurnLifecyclePersistence
|
||||
import type { DatabaseTurnDaemonLease } from '../lifecycle/databaseTurnDaemonLease.js';
|
||||
import { calculateNationBettingRewards } from '../betting/nationBettingSettlement.js';
|
||||
import type { NationBettingCandidate, PendingNationBettingFinish, PendingNationBettingOpen } from './types.js';
|
||||
import { buildPersistedRankRows } from './rankData.js';
|
||||
|
||||
export interface DatabaseTurnHooks {
|
||||
hooks: TurnDaemonHooks;
|
||||
@@ -270,20 +271,6 @@ const toLegacyDatabaseInt = (value: number): number => {
|
||||
return value >= 0 ? Math.floor(value + 0.5) : Math.ceil(value - 0.5);
|
||||
};
|
||||
|
||||
const readRankMetaNumber = (meta: Record<string, unknown>, key: string): number => {
|
||||
const value = meta[key];
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return toLegacyDatabaseInt(value);
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const parsed = Number(value);
|
||||
if (Number.isFinite(parsed)) {
|
||||
return toLegacyDatabaseInt(parsed);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const LEGACY_INTEGER_GENERAL_META_KEYS = [
|
||||
'leadership_exp',
|
||||
'strength_exp',
|
||||
@@ -312,72 +299,10 @@ const buildPersistedGeneralMeta = (
|
||||
return asJson(meta);
|
||||
};
|
||||
|
||||
const buildRankRows = (
|
||||
general: ReturnType<InMemoryTurnWorld['consumeDirtyState']>['generals'][number]
|
||||
): Array<{ generalId: number; nationId: number; type: string; value: number }> => {
|
||||
const meta = asRecord(general.meta);
|
||||
const readMeta = (key: string) => readRankMetaNumber(meta, key);
|
||||
const readRank = (key: string) => readRankMetaNumber(meta, `rank_${key}`);
|
||||
|
||||
const entries: Array<[RankDataType, number]> = [
|
||||
['experience', toLegacyDatabaseInt(general.experience)],
|
||||
['dedication', toLegacyDatabaseInt(general.dedication)],
|
||||
['firenum', readMeta('firenum')],
|
||||
['warnum', readRank('warnum')],
|
||||
['killnum', readRank('killnum')],
|
||||
['deathnum', readRank('deathnum')],
|
||||
['occupied', readRank('occupied')],
|
||||
['killcrew', readRank('killcrew')],
|
||||
['deathcrew', readRank('deathcrew')],
|
||||
['killcrew_person', readRank('killcrew_person')],
|
||||
['deathcrew_person', readRank('deathcrew_person')],
|
||||
['dex1', readMeta('dex1')],
|
||||
['dex2', readMeta('dex2')],
|
||||
['dex3', readMeta('dex3')],
|
||||
['dex4', readMeta('dex4')],
|
||||
['dex5', readMeta('dex5')],
|
||||
['ttw', readMeta('ttw')],
|
||||
['ttd', readMeta('ttd')],
|
||||
['ttl', readMeta('ttl')],
|
||||
['ttg', readMeta('ttg')],
|
||||
['ttp', readMeta('ttp')],
|
||||
['tlw', readMeta('tlw')],
|
||||
['tld', readMeta('tld')],
|
||||
['tll', readMeta('tll')],
|
||||
['tlg', readMeta('tlg')],
|
||||
['tlp', readMeta('tlp')],
|
||||
['tsw', readMeta('tsw')],
|
||||
['tsd', readMeta('tsd')],
|
||||
['tsl', readMeta('tsl')],
|
||||
['tsg', readMeta('tsg')],
|
||||
['tsp', readMeta('tsp')],
|
||||
['tiw', readMeta('tiw')],
|
||||
['tid', readMeta('tid')],
|
||||
['til', readMeta('til')],
|
||||
['tig', readMeta('tig')],
|
||||
['tip', readMeta('tip')],
|
||||
['betgold', readMeta('betgold')],
|
||||
['betwin', readMeta('betwin')],
|
||||
['betwingold', readMeta('betwingold')],
|
||||
['inherit_earned', readMeta('inherit_earned')],
|
||||
['inherit_spent', readMeta('inherit_spent')],
|
||||
['inherit_earned_dyn', readMeta('inherit_earned_dyn')],
|
||||
['inherit_earned_act', readMeta('inherit_earned_act')],
|
||||
['inherit_spent_dyn', readMeta('inherit_spent_dyn')],
|
||||
];
|
||||
|
||||
return entries.map(([type, value]) => ({
|
||||
generalId: general.id,
|
||||
nationId: general.nationId,
|
||||
type,
|
||||
value,
|
||||
}));
|
||||
};
|
||||
|
||||
const buildInitialRankRows = (
|
||||
general: ReturnType<InMemoryTurnWorld['consumeDirtyState']>['generals'][number]
|
||||
): Array<{ generalId: number; nationId: number; type: string; value: number }> =>
|
||||
buildRankRows(general).map((row) => ({ ...row, nationId: 0, value: 0 }));
|
||||
buildPersistedRankRows(general).map((row) => ({ ...row, nationId: 0, value: 0 }));
|
||||
|
||||
const buildGeneralUpdate = (
|
||||
general: ReturnType<InMemoryTurnWorld['consumeDirtyState']>['generals'][number]
|
||||
@@ -953,7 +878,7 @@ export const createDatabaseTurnHooks = async (
|
||||
if (createdGenerals.length > 0 || rankTargets.length > 0) {
|
||||
const rankRows = [
|
||||
...createdGenerals.flatMap(buildInitialRankRows),
|
||||
...rankTargets.flatMap(buildRankRows),
|
||||
...rankTargets.flatMap(buildPersistedRankRows),
|
||||
];
|
||||
await Promise.all(
|
||||
rankRows.map((row) =>
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
LEGACY_RANK_DATA_TYPES,
|
||||
RANK_DATA_TYPES,
|
||||
rankDataMetaKey,
|
||||
type LegacyRankDataType,
|
||||
type RankDataType,
|
||||
} from '@sammo-ts/common';
|
||||
|
||||
export interface RankedGeneralState {
|
||||
id: number;
|
||||
nationId: number;
|
||||
experience: number;
|
||||
dedication: number;
|
||||
meta: unknown;
|
||||
}
|
||||
|
||||
export interface PersistedRankRow {
|
||||
generalId: number;
|
||||
nationId: number;
|
||||
type: RankDataType;
|
||||
value: number;
|
||||
}
|
||||
|
||||
const asRecord = (value: unknown): Record<string, unknown> =>
|
||||
typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
? { ...(value as Record<string, unknown>) }
|
||||
: {};
|
||||
const toLegacyDatabaseInt = (value: number): number => {
|
||||
if (!Number.isFinite(value)) {
|
||||
return 0;
|
||||
}
|
||||
return value >= 0 ? Math.floor(value + 0.5) : Math.ceil(value - 0.5);
|
||||
};
|
||||
|
||||
const readMetaNumber = (meta: Record<string, unknown>, key: string): number => {
|
||||
const value = meta[key];
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return toLegacyDatabaseInt(value);
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const parsed = Number(value);
|
||||
return Number.isFinite(parsed) ? toLegacyDatabaseInt(parsed) : 0;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
export const rankMetaKey = rankDataMetaKey;
|
||||
|
||||
export const buildPersistedRankRows = (general: RankedGeneralState): PersistedRankRow[] => {
|
||||
const meta = asRecord(general.meta);
|
||||
return RANK_DATA_TYPES.map((type) => {
|
||||
const value =
|
||||
type === 'experience'
|
||||
? toLegacyDatabaseInt(general.experience)
|
||||
: type === 'dedication'
|
||||
? toLegacyDatabaseInt(general.dedication)
|
||||
: readMetaNumber(meta, rankMetaKey(type));
|
||||
return {
|
||||
generalId: general.id,
|
||||
nationId: general.nationId,
|
||||
type,
|
||||
value,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export const buildLegacyComparableRankRows = (
|
||||
general: RankedGeneralState
|
||||
): Array<PersistedRankRow & { type: LegacyRankDataType }> => {
|
||||
const legacyTypes = new Set<RankDataType>(LEGACY_RANK_DATA_TYPES);
|
||||
return buildPersistedRankRows(general).filter(
|
||||
(row): row is PersistedRankRow & { type: LegacyRankDataType } => legacyTypes.has(row.type)
|
||||
);
|
||||
};
|
||||
|
||||
export const applyPersistedRankRowsToMeta = (
|
||||
rawMeta: Record<string, unknown>,
|
||||
rows: ReadonlyArray<{ type: string; value: number }>
|
||||
): void => {
|
||||
const supportedTypes = new Set<string>(RANK_DATA_TYPES);
|
||||
for (const row of rows) {
|
||||
if (row.type === 'experience' || row.type === 'dedication' || !supportedTypes.has(row.type)) {
|
||||
continue;
|
||||
}
|
||||
rawMeta[rankMetaKey(row.type as RankDataType)] = row.value;
|
||||
}
|
||||
};
|
||||
@@ -37,7 +37,7 @@ import {
|
||||
} from '@sammo-ts/logic';
|
||||
import { buildLegacyDefaultUniqueItemPool } from '@sammo-ts/logic/rewards/legacyUniqueItemPool.js';
|
||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic';
|
||||
import { asRecord, LiteHashDRBG, RandUtil } from '@sammo-ts/common';
|
||||
import { asRecord, LEGACY_RANK_DATA_TYPES, LiteHashDRBG, RandUtil } from '@sammo-ts/common';
|
||||
|
||||
import type { ConstraintContext, StateView } from '@sammo-ts/logic';
|
||||
|
||||
@@ -57,6 +57,7 @@ import { buildFrontStatePatches } from './frontStateHandler.js';
|
||||
import { buildActionContext } from './reservedTurnActionContext.js';
|
||||
import { GeneralAI, shouldUseAi } from './ai/generalAi.js';
|
||||
import type { AiReservedTurnProvider } from './ai/types.js';
|
||||
import { rankMetaKey } from './rankData.js';
|
||||
|
||||
const DEFAULT_ACTION = '휴식';
|
||||
|
||||
@@ -227,44 +228,11 @@ const cloneTurnGeneral = (general: TurnGeneral): TurnGeneral => ({
|
||||
|
||||
const resetRetiredGeneral = (general: TurnGeneral): TurnGeneral => {
|
||||
const meta = { ...general.meta };
|
||||
for (const key of [
|
||||
'firenum',
|
||||
'rank_warnum',
|
||||
'rank_killnum',
|
||||
'rank_deathnum',
|
||||
'rank_occupied',
|
||||
'rank_killcrew',
|
||||
'rank_deathcrew',
|
||||
'rank_killcrew_person',
|
||||
'rank_deathcrew_person',
|
||||
'rank_ttw',
|
||||
'rank_ttd',
|
||||
'rank_ttl',
|
||||
'rank_ttg',
|
||||
'rank_ttp',
|
||||
'rank_tlw',
|
||||
'rank_tld',
|
||||
'rank_tll',
|
||||
'rank_tlg',
|
||||
'rank_tlp',
|
||||
'rank_tsw',
|
||||
'rank_tsd',
|
||||
'rank_tsl',
|
||||
'rank_tsg',
|
||||
'rank_tsp',
|
||||
'rank_tiw',
|
||||
'rank_tid',
|
||||
'rank_til',
|
||||
'rank_tig',
|
||||
'rank_tip',
|
||||
'rank_betgold',
|
||||
'rank_betwin',
|
||||
'rank_betwingold',
|
||||
'specage',
|
||||
'specage2',
|
||||
]) {
|
||||
meta[key] = 0;
|
||||
for (const type of LEGACY_RANK_DATA_TYPES) {
|
||||
meta[rankMetaKey(type)] = 0;
|
||||
}
|
||||
meta.specage = 0;
|
||||
meta.specage2 = 0;
|
||||
for (let dex = 1; dex <= 5; dex += 1) {
|
||||
const key = `dex${dex}`;
|
||||
meta[key] = Math.round(readMetaNumber(meta, key, 0) * 0.5);
|
||||
|
||||
@@ -32,6 +32,7 @@ import type { UnitSetLoaderOptions } from '../scenario/unitSetLoader.js';
|
||||
import { loadUnitSetDefinitionByName } from '../scenario/unitSetLoader.js';
|
||||
import type { TurnDiplomacy, TurnEvent, TurnGeneral, TurnWorldLoadResult } from './types.js';
|
||||
import { readDiplomacyMeta } from '@sammo-ts/logic';
|
||||
import { applyPersistedRankRowsToMeta } from './rankData.js';
|
||||
|
||||
interface TurnWorldLoaderOptions {
|
||||
databaseUrl: string;
|
||||
@@ -157,17 +158,6 @@ const mapScenarioConfig = (raw: JsonValue): ScenarioConfig => {
|
||||
return parsed.data;
|
||||
};
|
||||
|
||||
const GENERAL_RANK_META_PREFIX_TYPES = new Set([
|
||||
'warnum',
|
||||
'killnum',
|
||||
'deathnum',
|
||||
'occupied',
|
||||
'killcrew',
|
||||
'deathcrew',
|
||||
'killcrew_person',
|
||||
'deathcrew_person',
|
||||
]);
|
||||
|
||||
const mapGeneralRow = (
|
||||
row: TurnEngineGeneralRow,
|
||||
rankRows: readonly TurnEngineRankDataRow[],
|
||||
@@ -181,12 +171,7 @@ const mapGeneralRow = (
|
||||
item: normalizeCode(row.itemCode),
|
||||
};
|
||||
const rawMeta = { ...(asTriggerRecord(row.meta) as Record<string, unknown>) };
|
||||
for (const rank of rankRows) {
|
||||
if (rank.type === 'experience' || rank.type === 'dedication') {
|
||||
continue;
|
||||
}
|
||||
rawMeta[GENERAL_RANK_META_PREFIX_TYPES.has(rank.type) ? `rank_${rank.type}` : rank.type] = rank.value;
|
||||
}
|
||||
applyPersistedRankRowsToMeta(rawMeta, rankRows);
|
||||
const inheritancePoints = Object.fromEntries(inheritanceRows.map((entry) => [entry.key, entry.value]));
|
||||
const itemInventory = readItemInventoryFromMeta(rawMeta, legacySlots);
|
||||
return {
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { LEGACY_RANK_DATA_TYPES } from '@sammo-ts/common';
|
||||
import type { TurnSchedule } from '@sammo-ts/logic';
|
||||
|
||||
import { rankMetaKey } from '../src/turn/rankData.js';
|
||||
import type { TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
|
||||
import { createTurnTestHarness } from './helpers/turnTestHarness.js';
|
||||
|
||||
@@ -294,6 +296,9 @@ describe('legacy general turn lifecycle', () => {
|
||||
});
|
||||
|
||||
it('retires a player general and resets inherited stats and rank state', async () => {
|
||||
const legacyRankMeta = Object.fromEntries(
|
||||
LEGACY_RANK_DATA_TYPES.map((type, index) => [rankMetaKey(type), index + 1])
|
||||
);
|
||||
const harness = await createTurnTestHarness({
|
||||
snapshot: makeSnapshot([
|
||||
makeGeneral({
|
||||
@@ -302,11 +307,11 @@ describe('legacy general turn lifecycle', () => {
|
||||
experience: 101,
|
||||
dedication: 81,
|
||||
meta: {
|
||||
...legacyRankMeta,
|
||||
killturn: 24,
|
||||
dex1: 101,
|
||||
inherit_lived_month: 10,
|
||||
inherit_active_action: 4,
|
||||
rank_warnum: 12,
|
||||
},
|
||||
}),
|
||||
]),
|
||||
@@ -325,7 +330,9 @@ describe('legacy general turn lifecycle', () => {
|
||||
expect(updated.meta.dex1).toBe(51);
|
||||
expect(updated.meta.inherit_lived_month).toBe(0);
|
||||
expect(updated.meta.inherit_active_action).toBe(0);
|
||||
expect(updated.meta.rank_warnum).toBe(0);
|
||||
for (const type of LEGACY_RANK_DATA_TYPES) {
|
||||
expect(updated.meta[rankMetaKey(type)], type).toBe(0);
|
||||
}
|
||||
expect(harness.world.peekDirtyState().lifecycleEvents[0]?.outcome).toBe('retired');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { LEGACY_RANK_DATA_TYPES, RANK_DATA_TYPES } from '@sammo-ts/common';
|
||||
|
||||
import {
|
||||
applyPersistedRankRowsToMeta,
|
||||
buildLegacyComparableRankRows,
|
||||
buildPersistedRankRows,
|
||||
rankMetaKey,
|
||||
} from '../src/turn/rankData.js';
|
||||
|
||||
describe('rank data projection', () => {
|
||||
it('projects every core row while preserving legacy key and integer semantics', () => {
|
||||
const rows = buildPersistedRankRows({
|
||||
id: 7,
|
||||
nationId: 2,
|
||||
experience: 10.5,
|
||||
dedication: 20.49,
|
||||
meta: {
|
||||
rank_warnum: '3.5',
|
||||
ttw: 4.5,
|
||||
inherit_earned: '9',
|
||||
dex1: 12,
|
||||
},
|
||||
});
|
||||
|
||||
expect(rows).toHaveLength(RANK_DATA_TYPES.length);
|
||||
expect(rows).toEqual(
|
||||
expect.arrayContaining([
|
||||
{ generalId: 7, nationId: 2, type: 'experience', value: 11 },
|
||||
{ generalId: 7, nationId: 2, type: 'dedication', value: 20 },
|
||||
{ generalId: 7, nationId: 2, type: 'warnum', value: 4 },
|
||||
{ generalId: 7, nationId: 2, type: 'ttw', value: 5 },
|
||||
{ generalId: 7, nationId: 2, type: 'inherit_earned', value: 9 },
|
||||
{ generalId: 7, nationId: 2, type: 'dex1', value: 12 },
|
||||
])
|
||||
);
|
||||
expect(buildLegacyComparableRankRows({
|
||||
id: 7,
|
||||
nationId: 2,
|
||||
experience: 10.5,
|
||||
dedication: 20.49,
|
||||
meta: {},
|
||||
})).toHaveLength(LEGACY_RANK_DATA_TYPES.length);
|
||||
});
|
||||
|
||||
it('loads persisted rows into the same raw and prefixed meta keys used by commands', () => {
|
||||
const meta: Record<string, unknown> = {};
|
||||
applyPersistedRankRowsToMeta(meta, [
|
||||
{ type: 'warnum', value: 3 },
|
||||
{ type: 'ttw', value: 4 },
|
||||
{ type: 'inherit_spent', value: 5 },
|
||||
{ type: 'dex1', value: 6 },
|
||||
{ type: 'unknown', value: 99 },
|
||||
]);
|
||||
|
||||
expect(meta).toEqual({
|
||||
rank_warnum: 3,
|
||||
ttw: 4,
|
||||
inherit_spent: 5,
|
||||
dex1: 6,
|
||||
});
|
||||
expect(rankMetaKey('warnum')).toBe('rank_warnum');
|
||||
expect(rankMetaKey('betgold')).toBe('betgold');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user