fix(api): align scenario 2601 reference data
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
|
||||
import { zWorldStateConfig, zWorldStateMeta } from '../../context.js';
|
||||
import { isSelectionPoolWorld, resolveSelectionMaxGeneral } from '../../services/selectPool.js';
|
||||
import { procedure, router } from '../../trpc.js';
|
||||
@@ -23,6 +25,7 @@ export const lobbyRouter = router({
|
||||
const userCnt = await ctx.db.general.count({ where: { npcState: { lt: 2 } } });
|
||||
const npcCnt = await ctx.db.general.count({ where: { npcState: { gte: 2 } } });
|
||||
const nationCnt = await ctx.db.nation.count({ where: { level: { gt: 0 } } });
|
||||
const scenarioTitle = asRecord(asRecord(rawWorldState.meta).scenarioMeta).title;
|
||||
|
||||
let myGeneral = null;
|
||||
if (ctx.auth?.user.id) {
|
||||
@@ -55,6 +58,7 @@ export const lobbyRouter = router({
|
||||
isUnited: worldState.meta.isunited ?? worldState.meta.isUnited ?? 0,
|
||||
selectionPoolEnabled: isSelectionPoolWorld(rawWorldState),
|
||||
npcPossessionEnabled: worldState.config.npcMode === 1,
|
||||
scenarioTitle: typeof scenarioTitle === 'string' ? scenarioTitle : '',
|
||||
myGeneral,
|
||||
};
|
||||
}),
|
||||
|
||||
@@ -27,6 +27,8 @@ export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
picture: true,
|
||||
imageServer: true,
|
||||
npcState: true,
|
||||
officerLevel: true,
|
||||
cityId: true,
|
||||
@@ -43,6 +45,16 @@ export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
crew: true,
|
||||
train: true,
|
||||
atmos: true,
|
||||
age: true,
|
||||
crewTypeId: true,
|
||||
weaponCode: true,
|
||||
bookCode: true,
|
||||
horseCode: true,
|
||||
itemCode: true,
|
||||
personalCode: true,
|
||||
specialCode: true,
|
||||
special2Code: true,
|
||||
meta: true,
|
||||
},
|
||||
orderBy: { id: 'asc' },
|
||||
}),
|
||||
@@ -79,29 +91,62 @@ export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const generals = generalRows.map((general) => ({
|
||||
id: general.id,
|
||||
name: general.name,
|
||||
npcState: general.npcState,
|
||||
officerLevel: general.officerLevel,
|
||||
cityId: general.cityId,
|
||||
turnTime: formatDateTime(general.turnTime),
|
||||
recentWar: formatDateTime(general.recentWarTime),
|
||||
warnum: battleCountMap.get(general.id) ?? 0,
|
||||
stats: {
|
||||
leadership: general.leadership,
|
||||
strength: general.strength,
|
||||
intelligence: general.intel,
|
||||
},
|
||||
experience: general.experience,
|
||||
dedication: general.dedication,
|
||||
injury: general.injury,
|
||||
gold: general.gold,
|
||||
rice: general.rice,
|
||||
crew: general.crew,
|
||||
train: general.train,
|
||||
atmos: general.atmos,
|
||||
}));
|
||||
const generals = generalRows.map((general) => {
|
||||
const meta =
|
||||
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;
|
||||
};
|
||||
return {
|
||||
id: general.id,
|
||||
name: general.name,
|
||||
picture: general.picture,
|
||||
imageServer: general.imageServer,
|
||||
npcState: general.npcState,
|
||||
officerLevel: general.officerLevel,
|
||||
cityId: general.cityId,
|
||||
turnTime: formatDateTime(general.turnTime),
|
||||
recentWar: formatDateTime(general.recentWarTime),
|
||||
warnum: battleCountMap.get(general.id) ?? 0,
|
||||
stats: {
|
||||
leadership: general.leadership,
|
||||
strength: general.strength,
|
||||
intelligence: general.intel,
|
||||
},
|
||||
experience: general.experience,
|
||||
dedication: general.dedication,
|
||||
injury: general.injury,
|
||||
gold: general.gold,
|
||||
rice: general.rice,
|
||||
crew: general.crew,
|
||||
train: general.train,
|
||||
atmos: general.atmos,
|
||||
age: general.age,
|
||||
crewTypeId: general.crewTypeId,
|
||||
equipment: {
|
||||
weapon: general.weaponCode,
|
||||
book: general.bookCode,
|
||||
horse: general.horseCode,
|
||||
item: general.itemCode,
|
||||
},
|
||||
traits: {
|
||||
personal: general.personalCode,
|
||||
specialDomestic: general.specialCode,
|
||||
specialWar: general.special2Code,
|
||||
},
|
||||
battleStats: {
|
||||
kills: metaNumber('rank_killnum') || metaNumber('killnum'),
|
||||
deaths: metaNumber('deathnum'),
|
||||
fire: metaNumber('firenum'),
|
||||
killCrew: metaNumber('killcrew'),
|
||||
deathCrew: metaNumber('deathcrew'),
|
||||
dex: [1, 2, 3, 4, 5].map((index) => metaNumber(`dex${index}`)),
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
me: {
|
||||
|
||||
@@ -5,7 +5,13 @@ import { LogCategory, LogScope } from '@sammo-ts/infra';
|
||||
|
||||
import { authedProcedure } from '../../../trpc.js';
|
||||
import { getMyGeneral } from '../../shared/general.js';
|
||||
import { assertNationAccess, resolveNationPermission, zGeneralLogType, type GeneralLogType } from '../shared.js';
|
||||
import {
|
||||
assertNationAccess,
|
||||
formatDateTime,
|
||||
resolveNationPermission,
|
||||
zGeneralLogType,
|
||||
type GeneralLogType,
|
||||
} from '../shared.js';
|
||||
|
||||
export const getGeneralLog = authedProcedure
|
||||
.input(
|
||||
@@ -75,6 +81,9 @@ export const getGeneralLog = authedProcedure
|
||||
logs: logs.map((entry) => ({
|
||||
id: entry.id,
|
||||
text: entry.text,
|
||||
year: entry.year,
|
||||
month: entry.month,
|
||||
createdAt: formatDateTime(entry.createdAt),
|
||||
})),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ export const getNationInfo = authedProcedure.query(async ({ ctx }) => {
|
||||
nationId: me.nationId,
|
||||
},
|
||||
select: { id: true, year: true, month: true, text: true },
|
||||
orderBy: { id: 'asc' },
|
||||
orderBy: { id: 'desc' },
|
||||
}),
|
||||
]);
|
||||
if (!nation) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
|
||||
import { loadUnitSetDefinitionByName } from '../../../battleSim/unitSetLoader.js';
|
||||
import { accessAuthedProcedure } from '../../../trpc.js';
|
||||
import { getMyGeneral } from '../../shared/general.js';
|
||||
import { assertNationAccess, resolveNationPermission } from '../shared.js';
|
||||
@@ -41,7 +42,7 @@ export const getSecretGeneralList = accessAuthedProcedure.query(async ({ ctx })
|
||||
});
|
||||
}
|
||||
|
||||
const [cities, troops, generalRows] = await Promise.all([
|
||||
const [cities, troops, generalRows, worldState] = await Promise.all([
|
||||
ctx.db.city.findMany({ select: { id: true, name: true } }),
|
||||
ctx.db.troop.findMany({
|
||||
where: { nationId: me.nationId },
|
||||
@@ -51,7 +52,14 @@ export const getSecretGeneralList = accessAuthedProcedure.query(async ({ ctx })
|
||||
where: { nationId: me.nationId },
|
||||
orderBy: [{ turnTime: 'asc' }, { id: 'asc' }],
|
||||
}),
|
||||
ctx.db.worldState.findFirst({ select: { config: true } }),
|
||||
]);
|
||||
const worldConfig = asRecord(worldState?.config);
|
||||
const environment = asRecord(worldConfig.environment ?? worldConfig.map);
|
||||
const unitSetName =
|
||||
typeof environment.unitSet === 'string' && environment.unitSet.trim() ? environment.unitSet : ctx.profile.id;
|
||||
const unitSet = await loadUnitSetDefinitionByName(unitSetName);
|
||||
const crewTypeNames = new Map((unitSet.crewTypes ?? []).map((crewType) => [crewType.id, crewType.name]));
|
||||
const generalIds = generalRows.map((general) => general.id);
|
||||
const turns = generalIds.length
|
||||
? await ctx.db.generalTurn.findMany({
|
||||
@@ -92,6 +100,7 @@ export const getSecretGeneralList = accessAuthedProcedure.query(async ({ ctx })
|
||||
defenceTrain,
|
||||
defenceTrainText: defenceTrainText(defenceTrain),
|
||||
crewTypeId: general.crewTypeId,
|
||||
crewTypeName: crewTypeNames.get(general.crewTypeId) ?? '-',
|
||||
crew: general.crew,
|
||||
train: general.train,
|
||||
atmos: general.atmos,
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
import { getGoldIncome, getOutcome, getRiceIncome, getWallIncome, getWarGoldIncome, type NationIncomeContext } from '@sammo-ts/logic';
|
||||
import {
|
||||
getGoldIncome,
|
||||
getOutcome,
|
||||
getRiceIncome,
|
||||
getWallIncome,
|
||||
getWarGoldIncome,
|
||||
type NationIncomeContext,
|
||||
} from '@sammo-ts/logic';
|
||||
|
||||
import { accessAuthedProcedure } from '../../../trpc.js';
|
||||
import { getMyGeneral } from '../../shared/general.js';
|
||||
@@ -171,13 +178,7 @@ export const getStratFinan = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
const cityStatsByNation = new Map<number, { popSum: number; valueSum: number; maxSum: number }>();
|
||||
for (const city of cityRows) {
|
||||
const entry = cityStatsByNation.get(city.nationId) ?? { popSum: 0, valueSum: 0, maxSum: 0 };
|
||||
const valueSum =
|
||||
city.population +
|
||||
city.agriculture +
|
||||
city.commerce +
|
||||
city.security +
|
||||
city.wall +
|
||||
city.defence;
|
||||
const valueSum = city.population + city.agriculture + city.commerce + city.security + city.wall + city.defence;
|
||||
const maxSum =
|
||||
city.populationMax +
|
||||
city.agricultureMax +
|
||||
@@ -222,22 +223,24 @@ export const getStratFinan = accessAuthedProcedure.query(async ({ ctx }) => {
|
||||
);
|
||||
}
|
||||
|
||||
const nationsList = nationRows.map((nationItem) => {
|
||||
const diplomacy =
|
||||
nationItem.id === nation.id
|
||||
? { state: 7, term: null }
|
||||
: diplomacyMap.get(nationItem.id) ?? { state: 2, term: 0 };
|
||||
return {
|
||||
id: nationItem.id,
|
||||
name: nationItem.name,
|
||||
color: nationItem.color,
|
||||
level: nationItem.level,
|
||||
power: powerByNation.get(nationItem.id) ?? 0,
|
||||
generalCount: generalCountMap.get(nationItem.id) ?? 0,
|
||||
cityCount: cityCountMap.get(nationItem.id) ?? 0,
|
||||
diplomacy,
|
||||
};
|
||||
});
|
||||
const nationsList = nationRows
|
||||
.filter((nationItem) => nationItem.id > 0)
|
||||
.map((nationItem) => {
|
||||
const diplomacy =
|
||||
nationItem.id === nation.id
|
||||
? { state: 7, term: null }
|
||||
: (diplomacyMap.get(nationItem.id) ?? { state: 2, term: 0 });
|
||||
return {
|
||||
id: nationItem.id,
|
||||
name: nationItem.name,
|
||||
color: nationItem.color,
|
||||
level: nationItem.level,
|
||||
power: powerByNation.get(nationItem.id) ?? 0,
|
||||
generalCount: generalCountMap.get(nationItem.id) ?? 0,
|
||||
cityCount: cityCountMap.get(nationItem.id) ?? 0,
|
||||
diplomacy,
|
||||
};
|
||||
});
|
||||
|
||||
const nationCities = cityRows.filter((city) => city.nationId === nation.id);
|
||||
const nationGenerals = generalRows.filter((general) => general.nationId === nation.id);
|
||||
|
||||
@@ -143,9 +143,7 @@ export const getNationDirectory = authedProcedure.query(async ({ ctx }) => {
|
||||
const nationGenerals = generalsByNation.get(nation.id) ?? [];
|
||||
const nationCities = citiesByNation.get(nation.id) ?? [];
|
||||
const officers = Array.from({ length: 8 }, (_, index) => 12 - index).map((officerLevel) => {
|
||||
const general = nationGenerals
|
||||
.filter((candidate) => candidate.officerLevel === officerLevel)
|
||||
.at(-1);
|
||||
const general = nationGenerals.filter((candidate) => candidate.officerLevel === officerLevel).at(-1);
|
||||
return {
|
||||
officerLevel,
|
||||
general: general
|
||||
@@ -178,7 +176,7 @@ export const getNationDirectory = authedProcedure.query(async ({ ctx }) => {
|
||||
},
|
||||
power: readMetaNumber(nation.meta, 'power'),
|
||||
capitalCityId: nation.capitalCityId ?? 0,
|
||||
generalCount: nationGenerals.length,
|
||||
generalCount: readMetaNumber(nation.meta, 'gennum', nationGenerals.length),
|
||||
cityCount: nationCities.length,
|
||||
officers,
|
||||
ambassadorNames: secretPermissions
|
||||
@@ -204,8 +202,8 @@ export const getNationDirectory = authedProcedure.query(async ({ ctx }) => {
|
||||
});
|
||||
});
|
||||
|
||||
export const getGeneralDirectory = accessAuthedInputProcedure(z.object({ sort: zDirectorySort }).optional())
|
||||
.query(async ({ ctx, input }) => {
|
||||
export const getGeneralDirectory = accessAuthedInputProcedure(z.object({ sort: zDirectorySort }).optional()).query(
|
||||
async ({ ctx, input }) => {
|
||||
await getMyGeneral(ctx);
|
||||
const sort = input?.sort ?? 9;
|
||||
const [generals, nations, accessLogs, worldState] = await Promise.all([
|
||||
@@ -365,4 +363,5 @@ export const getGeneralDirectory = accessAuthedInputProcedure(z.object({ sort: z
|
||||
});
|
||||
|
||||
return { sort, generals: rows };
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
@@ -7,10 +7,7 @@ import { LogCategory, LogScope } from '@sammo-ts/infra';
|
||||
|
||||
import type { GameApiContext } from '../../context.js';
|
||||
import { loadPublicMap, type BaseMapResult } from '../../maps/worldMap.js';
|
||||
import {
|
||||
generalAccessEndpointWeights,
|
||||
recordGeneralAccessWeight,
|
||||
} from '../../services/generalAccess.js';
|
||||
import { generalAccessEndpointWeights, recordGeneralAccessWeight } from '../../services/generalAccess.js';
|
||||
import { authedProcedure, router } from '../../trpc.js';
|
||||
import { getMyGeneral } from '../shared/general.js';
|
||||
|
||||
@@ -72,6 +69,7 @@ const parseYearbookNations = (value: unknown): YearbookNation[] => {
|
||||
|
||||
const resolveArchiveTarget = (
|
||||
worldMeta: unknown,
|
||||
profileId: string,
|
||||
profileName: string,
|
||||
requestedServerId?: string
|
||||
): { archiveKey: string; legacyAlias: string | null; isCurrentProfile: boolean } => {
|
||||
@@ -81,7 +79,12 @@ const resolveArchiveTarget = (
|
||||
const isCurrentProfile = requested === profileName || requested === canonicalServerId;
|
||||
return {
|
||||
archiveKey: isCurrentProfile ? canonicalServerId : requested,
|
||||
legacyAlias: isCurrentProfile && canonicalServerId !== profileName ? profileName : null,
|
||||
legacyAlias:
|
||||
isCurrentProfile && canonicalServerId !== profileName
|
||||
? profileName
|
||||
: isCurrentProfile && profileId !== canonicalServerId
|
||||
? profileId
|
||||
: null,
|
||||
isCurrentProfile,
|
||||
};
|
||||
};
|
||||
@@ -263,7 +266,7 @@ export const yearbookRouter = router({
|
||||
message: 'World state is not initialized.',
|
||||
});
|
||||
}
|
||||
const target = resolveArchiveTarget(worldState.meta, ctx.profile.name, input?.serverID);
|
||||
const target = resolveArchiveTarget(worldState.meta, ctx.profile.id, ctx.profile.name, input?.serverID);
|
||||
|
||||
const findRange = async (profileName: string) =>
|
||||
Promise.all([
|
||||
@@ -278,10 +281,19 @@ export const yearbookRouter = router({
|
||||
orderBy: [{ year: 'desc' as const }, { month: 'desc' as const }],
|
||||
}),
|
||||
]);
|
||||
let [firstRow, lastRow] = await findRange(target.archiveKey);
|
||||
if ((!firstRow || !lastRow) && target.legacyAlias) {
|
||||
[firstRow, lastRow] = await findRange(target.legacyAlias);
|
||||
}
|
||||
const ranges = await Promise.all(
|
||||
[target.archiveKey, target.legacyAlias]
|
||||
.filter((value): value is string => Boolean(value))
|
||||
.map(findRange)
|
||||
);
|
||||
const firstRow = ranges
|
||||
.map(([first]) => first)
|
||||
.filter((row): row is NonNullable<typeof row> => Boolean(row))
|
||||
.sort((a, b) => joinYearMonth(a.year, a.month) - joinYearMonth(b.year, b.month))[0];
|
||||
const lastRow = ranges
|
||||
.map(([, last]) => last)
|
||||
.filter((row): row is NonNullable<typeof row> => Boolean(row))
|
||||
.sort((a, b) => joinYearMonth(b.year, b.month) - joinYearMonth(a.year, a.month))[0];
|
||||
|
||||
if (!target.isCurrentProfile && (!firstRow || !lastRow)) {
|
||||
throw new TRPCError({ code: 'NOT_FOUND', message: '연감 범위를 찾을 수 없습니다.' });
|
||||
@@ -314,7 +326,7 @@ export const yearbookRouter = router({
|
||||
if (!worldState) {
|
||||
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'World state is not initialized.' });
|
||||
}
|
||||
const target = resolveArchiveTarget(worldState.meta, ctx.profile.name, input.serverID);
|
||||
const target = resolveArchiveTarget(worldState.meta, ctx.profile.id, ctx.profile.name, input.serverID);
|
||||
const shouldRecordAfterHashCheck = target.isCurrentProfile && Boolean(input.hash);
|
||||
if (target.isCurrentProfile && !shouldRecordAfterHashCheck) {
|
||||
await recordHistoryAccess(ctx);
|
||||
|
||||
@@ -79,7 +79,7 @@ const createContext = (options: {
|
||||
nationMeta?: Record<string, unknown>;
|
||||
requestCommand?: ReturnType<typeof vi.fn>;
|
||||
accessToken?: string;
|
||||
logs?: Array<{ id: number; text: string }>;
|
||||
logs?: Array<{ id: number; text: string; year?: number; month?: number; createdAt?: Date }>;
|
||||
}) => {
|
||||
const me = options.me === undefined ? buildGeneral() : options.me;
|
||||
const targets = options.targets ?? (me ? [me] : []);
|
||||
@@ -119,12 +119,24 @@ const createContext = (options: {
|
||||
},
|
||||
logEntry: {
|
||||
groupBy: vi.fn(async () => []),
|
||||
findMany: vi.fn(async (query?: { where?: { id?: { lt?: number } }; take?: number }) => {
|
||||
const source = options.logs ?? [{ id: 1, text: '기록' }];
|
||||
const beforeId = query?.where?.id?.lt;
|
||||
const filtered = beforeId ? source.filter((entry) => entry.id < beforeId) : source;
|
||||
return query?.take ? filtered.slice(0, query.take) : filtered;
|
||||
}),
|
||||
findMany: vi.fn(
|
||||
async (query?: {
|
||||
where?: { id?: { lt?: number } };
|
||||
take?: number;
|
||||
select?: { id?: boolean; text?: boolean };
|
||||
}) => {
|
||||
const source = (options.logs ?? [{ id: 1, text: '기록' }]).map((entry) => ({
|
||||
year: 185,
|
||||
month: 1,
|
||||
createdAt: now,
|
||||
...entry,
|
||||
}));
|
||||
const beforeId = query?.where?.id?.lt;
|
||||
const filtered = beforeId ? source.filter((entry) => entry.id < beforeId) : source;
|
||||
const selected = query?.select ? filtered.map(({ id, text }) => ({ id, text })) : filtered;
|
||||
return query?.take ? selected.slice(0, query.take) : selected;
|
||||
}
|
||||
),
|
||||
},
|
||||
};
|
||||
const redisClient = { get: async () => null, set: async () => null };
|
||||
@@ -405,6 +417,14 @@ describe('battle-center general and user permissions', () => {
|
||||
});
|
||||
await expect(appRouter.createCaller(tenured.context).nation.getBattleCenter()).resolves.toMatchObject({
|
||||
me: { id: 7, permissionLevel: 1 },
|
||||
generals: [
|
||||
{
|
||||
id: 7,
|
||||
picture: 'default.jpg',
|
||||
imageServer: 0,
|
||||
battleStats: { kills: 0, deaths: 0, fire: 0, killCrew: 0, deathCrew: 0, dex: [0, 0, 0, 0, 0] },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const auditor = createContext({
|
||||
@@ -430,6 +450,7 @@ describe('battle-center general and user permissions', () => {
|
||||
|
||||
await expect(member.nation.getGeneralLog({ generalId: me.id, type: 'generalAction' })).resolves.toMatchObject({
|
||||
generalId: me.id,
|
||||
logs: [{ id: 1, year: 185, month: 1, createdAt: '2026-01-01 00:00:00' }],
|
||||
});
|
||||
await expect(
|
||||
member.nation.getGeneralLog({ generalId: otherUser.id, type: 'generalAction' })
|
||||
|
||||
@@ -52,12 +52,27 @@ const archiveRows = [
|
||||
year: 219,
|
||||
month: 12,
|
||||
map: { year: 219, month: 12, startYear: 190, cityList: [], nationList: [] },
|
||||
nations: [{ id: 1, name: '현재기수국', color: '#00FF00', level: 7, power: 1300, generalCount: 10, cities: ['낙양'] }],
|
||||
nations: [
|
||||
{ id: 1, name: '현재기수국', color: '#00FF00', level: 7, power: 1300, generalCount: 10, cities: ['낙양'] },
|
||||
],
|
||||
globalHistory: ['저장된 현재 기수 과거 기록'],
|
||||
globalAction: ['저장된 현재 기수 과거 행동'],
|
||||
hash: 'current-archive',
|
||||
createdAt: new Date('2026-07-31T00:00:00.000Z'),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
profileName: profile.id,
|
||||
sourceId: 201,
|
||||
year: 219,
|
||||
month: 11,
|
||||
map: { year: 219, month: 11, startYear: 190, cityList: [], nationList: [] },
|
||||
nations: [],
|
||||
globalHistory: ['레거시 프로필 별칭 기록'],
|
||||
globalAction: [],
|
||||
hash: 'legacy-profile-alias',
|
||||
createdAt: new Date('2026-07-30T00:00:00.000Z'),
|
||||
},
|
||||
];
|
||||
|
||||
const authFor = (userId: string): GameSessionTokenPayload => ({
|
||||
@@ -75,14 +90,21 @@ const authFor = (userId: string): GameSessionTokenPayload => ({
|
||||
sanctions: {},
|
||||
});
|
||||
|
||||
const buildContext = (auth: GameSessionTokenPayload | null, options: { hasGeneral?: boolean } = {}): GameApiContext => {
|
||||
const buildContext = (
|
||||
auth: GameSessionTokenPayload | null,
|
||||
options: { hasGeneral?: boolean; worldMeta?: unknown } = {}
|
||||
): GameApiContext => {
|
||||
const db = {
|
||||
general: {
|
||||
findFirst: async ({ where }: { where: { userId: string } }) =>
|
||||
options.hasGeneral === false ? null : { id: where.userId === 'owner-a' ? 1 : 2, userId: where.userId },
|
||||
},
|
||||
worldState: {
|
||||
findFirst: async () => ({ currentYear: 220, currentMonth: 1, meta: { serverId: currentServerId } }),
|
||||
findFirst: async () => ({
|
||||
currentYear: 220,
|
||||
currentMonth: 1,
|
||||
meta: options.worldMeta ?? { serverId: currentServerId },
|
||||
}),
|
||||
},
|
||||
yearbookHistory: {
|
||||
findFirst: async (args: {
|
||||
@@ -198,6 +220,16 @@ describe('historical yearbook access from dynasty', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('reads imported history under the short profile ID when world metadata has no server ID', async () => {
|
||||
const caller = appRouter.createCaller(buildContext(authFor('owner-a'), { worldMeta: {} }));
|
||||
|
||||
await expect(caller.yearbook.getRange()).resolves.toEqual({
|
||||
firstYearMonth: 219 * 12 + 10,
|
||||
lastYearMonth: 219 * 12 + 10,
|
||||
currentYearMonth: 220 * 12,
|
||||
});
|
||||
});
|
||||
|
||||
it('uses stored logs for a past month of the current generation', async () => {
|
||||
const caller = appRouter.createCaller(buildContext(authFor('owner-a')));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user