fix(api): align scenario 2601 reference data
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
import { TRPCError } from '@trpc/server';
|
import { TRPCError } from '@trpc/server';
|
||||||
|
|
||||||
|
import { asRecord } from '@sammo-ts/common';
|
||||||
|
|
||||||
import { zWorldStateConfig, zWorldStateMeta } from '../../context.js';
|
import { zWorldStateConfig, zWorldStateMeta } from '../../context.js';
|
||||||
import { isSelectionPoolWorld, resolveSelectionMaxGeneral } from '../../services/selectPool.js';
|
import { isSelectionPoolWorld, resolveSelectionMaxGeneral } from '../../services/selectPool.js';
|
||||||
import { procedure, router } from '../../trpc.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 userCnt = await ctx.db.general.count({ where: { npcState: { lt: 2 } } });
|
||||||
const npcCnt = await ctx.db.general.count({ where: { npcState: { gte: 2 } } });
|
const npcCnt = await ctx.db.general.count({ where: { npcState: { gte: 2 } } });
|
||||||
const nationCnt = await ctx.db.nation.count({ where: { level: { gt: 0 } } });
|
const nationCnt = await ctx.db.nation.count({ where: { level: { gt: 0 } } });
|
||||||
|
const scenarioTitle = asRecord(asRecord(rawWorldState.meta).scenarioMeta).title;
|
||||||
|
|
||||||
let myGeneral = null;
|
let myGeneral = null;
|
||||||
if (ctx.auth?.user.id) {
|
if (ctx.auth?.user.id) {
|
||||||
@@ -55,6 +58,7 @@ export const lobbyRouter = router({
|
|||||||
isUnited: worldState.meta.isunited ?? worldState.meta.isUnited ?? 0,
|
isUnited: worldState.meta.isunited ?? worldState.meta.isUnited ?? 0,
|
||||||
selectionPoolEnabled: isSelectionPoolWorld(rawWorldState),
|
selectionPoolEnabled: isSelectionPoolWorld(rawWorldState),
|
||||||
npcPossessionEnabled: worldState.config.npcMode === 1,
|
npcPossessionEnabled: worldState.config.npcMode === 1,
|
||||||
|
scenarioTitle: typeof scenarioTitle === 'string' ? scenarioTitle : '',
|
||||||
myGeneral,
|
myGeneral,
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
|
|||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
name: true,
|
name: true,
|
||||||
|
picture: true,
|
||||||
|
imageServer: true,
|
||||||
npcState: true,
|
npcState: true,
|
||||||
officerLevel: true,
|
officerLevel: true,
|
||||||
cityId: true,
|
cityId: true,
|
||||||
@@ -43,6 +45,16 @@ export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
|
|||||||
crew: true,
|
crew: true,
|
||||||
train: true,
|
train: true,
|
||||||
atmos: 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' },
|
orderBy: { id: 'asc' },
|
||||||
}),
|
}),
|
||||||
@@ -79,29 +91,62 @@ export const getBattleCenter = accessAuthedProcedure.query(async ({ ctx }) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const generals = generalRows.map((general) => ({
|
const generals = generalRows.map((general) => {
|
||||||
id: general.id,
|
const meta =
|
||||||
name: general.name,
|
general.meta && typeof general.meta === 'object' && !Array.isArray(general.meta)
|
||||||
npcState: general.npcState,
|
? (general.meta as Record<string, unknown>)
|
||||||
officerLevel: general.officerLevel,
|
: {};
|
||||||
cityId: general.cityId,
|
const metaNumber = (key: string): number => {
|
||||||
turnTime: formatDateTime(general.turnTime),
|
const value = meta[key];
|
||||||
recentWar: formatDateTime(general.recentWarTime),
|
return typeof value === 'number' && Number.isFinite(value) ? value : 0;
|
||||||
warnum: battleCountMap.get(general.id) ?? 0,
|
};
|
||||||
stats: {
|
return {
|
||||||
leadership: general.leadership,
|
id: general.id,
|
||||||
strength: general.strength,
|
name: general.name,
|
||||||
intelligence: general.intel,
|
picture: general.picture,
|
||||||
},
|
imageServer: general.imageServer,
|
||||||
experience: general.experience,
|
npcState: general.npcState,
|
||||||
dedication: general.dedication,
|
officerLevel: general.officerLevel,
|
||||||
injury: general.injury,
|
cityId: general.cityId,
|
||||||
gold: general.gold,
|
turnTime: formatDateTime(general.turnTime),
|
||||||
rice: general.rice,
|
recentWar: formatDateTime(general.recentWarTime),
|
||||||
crew: general.crew,
|
warnum: battleCountMap.get(general.id) ?? 0,
|
||||||
train: general.train,
|
stats: {
|
||||||
atmos: general.atmos,
|
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 {
|
return {
|
||||||
me: {
|
me: {
|
||||||
|
|||||||
@@ -5,7 +5,13 @@ import { LogCategory, LogScope } from '@sammo-ts/infra';
|
|||||||
|
|
||||||
import { authedProcedure } from '../../../trpc.js';
|
import { authedProcedure } from '../../../trpc.js';
|
||||||
import { getMyGeneral } from '../../shared/general.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
|
export const getGeneralLog = authedProcedure
|
||||||
.input(
|
.input(
|
||||||
@@ -75,6 +81,9 @@ export const getGeneralLog = authedProcedure
|
|||||||
logs: logs.map((entry) => ({
|
logs: logs.map((entry) => ({
|
||||||
id: entry.id,
|
id: entry.id,
|
||||||
text: entry.text,
|
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,
|
nationId: me.nationId,
|
||||||
},
|
},
|
||||||
select: { id: true, year: true, month: true, text: true },
|
select: { id: true, year: true, month: true, text: true },
|
||||||
orderBy: { id: 'asc' },
|
orderBy: { id: 'desc' },
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
if (!nation) {
|
if (!nation) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { TRPCError } from '@trpc/server';
|
|||||||
|
|
||||||
import { asRecord } from '@sammo-ts/common';
|
import { asRecord } from '@sammo-ts/common';
|
||||||
|
|
||||||
|
import { loadUnitSetDefinitionByName } from '../../../battleSim/unitSetLoader.js';
|
||||||
import { accessAuthedProcedure } from '../../../trpc.js';
|
import { accessAuthedProcedure } from '../../../trpc.js';
|
||||||
import { getMyGeneral } from '../../shared/general.js';
|
import { getMyGeneral } from '../../shared/general.js';
|
||||||
import { assertNationAccess, resolveNationPermission } from '../shared.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.city.findMany({ select: { id: true, name: true } }),
|
||||||
ctx.db.troop.findMany({
|
ctx.db.troop.findMany({
|
||||||
where: { nationId: me.nationId },
|
where: { nationId: me.nationId },
|
||||||
@@ -51,7 +52,14 @@ export const getSecretGeneralList = accessAuthedProcedure.query(async ({ ctx })
|
|||||||
where: { nationId: me.nationId },
|
where: { nationId: me.nationId },
|
||||||
orderBy: [{ turnTime: 'asc' }, { id: 'asc' }],
|
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 generalIds = generalRows.map((general) => general.id);
|
||||||
const turns = generalIds.length
|
const turns = generalIds.length
|
||||||
? await ctx.db.generalTurn.findMany({
|
? await ctx.db.generalTurn.findMany({
|
||||||
@@ -92,6 +100,7 @@ export const getSecretGeneralList = accessAuthedProcedure.query(async ({ ctx })
|
|||||||
defenceTrain,
|
defenceTrain,
|
||||||
defenceTrainText: defenceTrainText(defenceTrain),
|
defenceTrainText: defenceTrainText(defenceTrain),
|
||||||
crewTypeId: general.crewTypeId,
|
crewTypeId: general.crewTypeId,
|
||||||
|
crewTypeName: crewTypeNames.get(general.crewTypeId) ?? '-',
|
||||||
crew: general.crew,
|
crew: general.crew,
|
||||||
train: general.train,
|
train: general.train,
|
||||||
atmos: general.atmos,
|
atmos: general.atmos,
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
import { TRPCError } from '@trpc/server';
|
import { TRPCError } from '@trpc/server';
|
||||||
|
|
||||||
import { asRecord } from '@sammo-ts/common';
|
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 { accessAuthedProcedure } from '../../../trpc.js';
|
||||||
import { getMyGeneral } from '../../shared/general.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 }>();
|
const cityStatsByNation = new Map<number, { popSum: number; valueSum: number; maxSum: number }>();
|
||||||
for (const city of cityRows) {
|
for (const city of cityRows) {
|
||||||
const entry = cityStatsByNation.get(city.nationId) ?? { popSum: 0, valueSum: 0, maxSum: 0 };
|
const entry = cityStatsByNation.get(city.nationId) ?? { popSum: 0, valueSum: 0, maxSum: 0 };
|
||||||
const valueSum =
|
const valueSum = city.population + city.agriculture + city.commerce + city.security + city.wall + city.defence;
|
||||||
city.population +
|
|
||||||
city.agriculture +
|
|
||||||
city.commerce +
|
|
||||||
city.security +
|
|
||||||
city.wall +
|
|
||||||
city.defence;
|
|
||||||
const maxSum =
|
const maxSum =
|
||||||
city.populationMax +
|
city.populationMax +
|
||||||
city.agricultureMax +
|
city.agricultureMax +
|
||||||
@@ -222,22 +223,24 @@ export const getStratFinan = accessAuthedProcedure.query(async ({ ctx }) => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const nationsList = nationRows.map((nationItem) => {
|
const nationsList = nationRows
|
||||||
const diplomacy =
|
.filter((nationItem) => nationItem.id > 0)
|
||||||
nationItem.id === nation.id
|
.map((nationItem) => {
|
||||||
? { state: 7, term: null }
|
const diplomacy =
|
||||||
: diplomacyMap.get(nationItem.id) ?? { state: 2, term: 0 };
|
nationItem.id === nation.id
|
||||||
return {
|
? { state: 7, term: null }
|
||||||
id: nationItem.id,
|
: (diplomacyMap.get(nationItem.id) ?? { state: 2, term: 0 });
|
||||||
name: nationItem.name,
|
return {
|
||||||
color: nationItem.color,
|
id: nationItem.id,
|
||||||
level: nationItem.level,
|
name: nationItem.name,
|
||||||
power: powerByNation.get(nationItem.id) ?? 0,
|
color: nationItem.color,
|
||||||
generalCount: generalCountMap.get(nationItem.id) ?? 0,
|
level: nationItem.level,
|
||||||
cityCount: cityCountMap.get(nationItem.id) ?? 0,
|
power: powerByNation.get(nationItem.id) ?? 0,
|
||||||
diplomacy,
|
generalCount: generalCountMap.get(nationItem.id) ?? 0,
|
||||||
};
|
cityCount: cityCountMap.get(nationItem.id) ?? 0,
|
||||||
});
|
diplomacy,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const nationCities = cityRows.filter((city) => city.nationId === nation.id);
|
const nationCities = cityRows.filter((city) => city.nationId === nation.id);
|
||||||
const nationGenerals = generalRows.filter((general) => general.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 nationGenerals = generalsByNation.get(nation.id) ?? [];
|
||||||
const nationCities = citiesByNation.get(nation.id) ?? [];
|
const nationCities = citiesByNation.get(nation.id) ?? [];
|
||||||
const officers = Array.from({ length: 8 }, (_, index) => 12 - index).map((officerLevel) => {
|
const officers = Array.from({ length: 8 }, (_, index) => 12 - index).map((officerLevel) => {
|
||||||
const general = nationGenerals
|
const general = nationGenerals.filter((candidate) => candidate.officerLevel === officerLevel).at(-1);
|
||||||
.filter((candidate) => candidate.officerLevel === officerLevel)
|
|
||||||
.at(-1);
|
|
||||||
return {
|
return {
|
||||||
officerLevel,
|
officerLevel,
|
||||||
general: general
|
general: general
|
||||||
@@ -178,7 +176,7 @@ export const getNationDirectory = authedProcedure.query(async ({ ctx }) => {
|
|||||||
},
|
},
|
||||||
power: readMetaNumber(nation.meta, 'power'),
|
power: readMetaNumber(nation.meta, 'power'),
|
||||||
capitalCityId: nation.capitalCityId ?? 0,
|
capitalCityId: nation.capitalCityId ?? 0,
|
||||||
generalCount: nationGenerals.length,
|
generalCount: readMetaNumber(nation.meta, 'gennum', nationGenerals.length),
|
||||||
cityCount: nationCities.length,
|
cityCount: nationCities.length,
|
||||||
officers,
|
officers,
|
||||||
ambassadorNames: secretPermissions
|
ambassadorNames: secretPermissions
|
||||||
@@ -204,8 +202,8 @@ export const getNationDirectory = authedProcedure.query(async ({ ctx }) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getGeneralDirectory = accessAuthedInputProcedure(z.object({ sort: zDirectorySort }).optional())
|
export const getGeneralDirectory = accessAuthedInputProcedure(z.object({ sort: zDirectorySort }).optional()).query(
|
||||||
.query(async ({ ctx, input }) => {
|
async ({ ctx, input }) => {
|
||||||
await getMyGeneral(ctx);
|
await getMyGeneral(ctx);
|
||||||
const sort = input?.sort ?? 9;
|
const sort = input?.sort ?? 9;
|
||||||
const [generals, nations, accessLogs, worldState] = await Promise.all([
|
const [generals, nations, accessLogs, worldState] = await Promise.all([
|
||||||
@@ -365,4 +363,5 @@ export const getGeneralDirectory = accessAuthedInputProcedure(z.object({ sort: z
|
|||||||
});
|
});
|
||||||
|
|
||||||
return { sort, generals: rows };
|
return { sort, generals: rows };
|
||||||
});
|
}
|
||||||
|
);
|
||||||
|
|||||||
@@ -7,10 +7,7 @@ import { LogCategory, LogScope } from '@sammo-ts/infra';
|
|||||||
|
|
||||||
import type { GameApiContext } from '../../context.js';
|
import type { GameApiContext } from '../../context.js';
|
||||||
import { loadPublicMap, type BaseMapResult } from '../../maps/worldMap.js';
|
import { loadPublicMap, type BaseMapResult } from '../../maps/worldMap.js';
|
||||||
import {
|
import { generalAccessEndpointWeights, recordGeneralAccessWeight } from '../../services/generalAccess.js';
|
||||||
generalAccessEndpointWeights,
|
|
||||||
recordGeneralAccessWeight,
|
|
||||||
} from '../../services/generalAccess.js';
|
|
||||||
import { authedProcedure, router } from '../../trpc.js';
|
import { authedProcedure, router } from '../../trpc.js';
|
||||||
import { getMyGeneral } from '../shared/general.js';
|
import { getMyGeneral } from '../shared/general.js';
|
||||||
|
|
||||||
@@ -72,6 +69,7 @@ const parseYearbookNations = (value: unknown): YearbookNation[] => {
|
|||||||
|
|
||||||
const resolveArchiveTarget = (
|
const resolveArchiveTarget = (
|
||||||
worldMeta: unknown,
|
worldMeta: unknown,
|
||||||
|
profileId: string,
|
||||||
profileName: string,
|
profileName: string,
|
||||||
requestedServerId?: string
|
requestedServerId?: string
|
||||||
): { archiveKey: string; legacyAlias: string | null; isCurrentProfile: boolean } => {
|
): { archiveKey: string; legacyAlias: string | null; isCurrentProfile: boolean } => {
|
||||||
@@ -81,7 +79,12 @@ const resolveArchiveTarget = (
|
|||||||
const isCurrentProfile = requested === profileName || requested === canonicalServerId;
|
const isCurrentProfile = requested === profileName || requested === canonicalServerId;
|
||||||
return {
|
return {
|
||||||
archiveKey: isCurrentProfile ? canonicalServerId : requested,
|
archiveKey: isCurrentProfile ? canonicalServerId : requested,
|
||||||
legacyAlias: isCurrentProfile && canonicalServerId !== profileName ? profileName : null,
|
legacyAlias:
|
||||||
|
isCurrentProfile && canonicalServerId !== profileName
|
||||||
|
? profileName
|
||||||
|
: isCurrentProfile && profileId !== canonicalServerId
|
||||||
|
? profileId
|
||||||
|
: null,
|
||||||
isCurrentProfile,
|
isCurrentProfile,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -263,7 +266,7 @@ export const yearbookRouter = router({
|
|||||||
message: 'World state is not initialized.',
|
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) =>
|
const findRange = async (profileName: string) =>
|
||||||
Promise.all([
|
Promise.all([
|
||||||
@@ -278,10 +281,19 @@ export const yearbookRouter = router({
|
|||||||
orderBy: [{ year: 'desc' as const }, { month: 'desc' as const }],
|
orderBy: [{ year: 'desc' as const }, { month: 'desc' as const }],
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
let [firstRow, lastRow] = await findRange(target.archiveKey);
|
const ranges = await Promise.all(
|
||||||
if ((!firstRow || !lastRow) && target.legacyAlias) {
|
[target.archiveKey, target.legacyAlias]
|
||||||
[firstRow, lastRow] = await findRange(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)) {
|
if (!target.isCurrentProfile && (!firstRow || !lastRow)) {
|
||||||
throw new TRPCError({ code: 'NOT_FOUND', message: '연감 범위를 찾을 수 없습니다.' });
|
throw new TRPCError({ code: 'NOT_FOUND', message: '연감 범위를 찾을 수 없습니다.' });
|
||||||
@@ -314,7 +326,7 @@ export const yearbookRouter = router({
|
|||||||
if (!worldState) {
|
if (!worldState) {
|
||||||
throw new TRPCError({ code: 'PRECONDITION_FAILED', message: 'World state is not initialized.' });
|
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);
|
const shouldRecordAfterHashCheck = target.isCurrentProfile && Boolean(input.hash);
|
||||||
if (target.isCurrentProfile && !shouldRecordAfterHashCheck) {
|
if (target.isCurrentProfile && !shouldRecordAfterHashCheck) {
|
||||||
await recordHistoryAccess(ctx);
|
await recordHistoryAccess(ctx);
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ const createContext = (options: {
|
|||||||
nationMeta?: Record<string, unknown>;
|
nationMeta?: Record<string, unknown>;
|
||||||
requestCommand?: ReturnType<typeof vi.fn>;
|
requestCommand?: ReturnType<typeof vi.fn>;
|
||||||
accessToken?: string;
|
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 me = options.me === undefined ? buildGeneral() : options.me;
|
||||||
const targets = options.targets ?? (me ? [me] : []);
|
const targets = options.targets ?? (me ? [me] : []);
|
||||||
@@ -119,12 +119,24 @@ const createContext = (options: {
|
|||||||
},
|
},
|
||||||
logEntry: {
|
logEntry: {
|
||||||
groupBy: vi.fn(async () => []),
|
groupBy: vi.fn(async () => []),
|
||||||
findMany: vi.fn(async (query?: { where?: { id?: { lt?: number } }; take?: number }) => {
|
findMany: vi.fn(
|
||||||
const source = options.logs ?? [{ id: 1, text: '기록' }];
|
async (query?: {
|
||||||
const beforeId = query?.where?.id?.lt;
|
where?: { id?: { lt?: number } };
|
||||||
const filtered = beforeId ? source.filter((entry) => entry.id < beforeId) : source;
|
take?: number;
|
||||||
return query?.take ? filtered.slice(0, query.take) : filtered;
|
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 };
|
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({
|
await expect(appRouter.createCaller(tenured.context).nation.getBattleCenter()).resolves.toMatchObject({
|
||||||
me: { id: 7, permissionLevel: 1 },
|
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({
|
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({
|
await expect(member.nation.getGeneralLog({ generalId: me.id, type: 'generalAction' })).resolves.toMatchObject({
|
||||||
generalId: me.id,
|
generalId: me.id,
|
||||||
|
logs: [{ id: 1, year: 185, month: 1, createdAt: '2026-01-01 00:00:00' }],
|
||||||
});
|
});
|
||||||
await expect(
|
await expect(
|
||||||
member.nation.getGeneralLog({ generalId: otherUser.id, type: 'generalAction' })
|
member.nation.getGeneralLog({ generalId: otherUser.id, type: 'generalAction' })
|
||||||
|
|||||||
@@ -52,12 +52,27 @@ const archiveRows = [
|
|||||||
year: 219,
|
year: 219,
|
||||||
month: 12,
|
month: 12,
|
||||||
map: { year: 219, month: 12, startYear: 190, cityList: [], nationList: [] },
|
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: ['저장된 현재 기수 과거 기록'],
|
globalHistory: ['저장된 현재 기수 과거 기록'],
|
||||||
globalAction: ['저장된 현재 기수 과거 행동'],
|
globalAction: ['저장된 현재 기수 과거 행동'],
|
||||||
hash: 'current-archive',
|
hash: 'current-archive',
|
||||||
createdAt: new Date('2026-07-31T00:00:00.000Z'),
|
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 => ({
|
const authFor = (userId: string): GameSessionTokenPayload => ({
|
||||||
@@ -75,14 +90,21 @@ const authFor = (userId: string): GameSessionTokenPayload => ({
|
|||||||
sanctions: {},
|
sanctions: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const buildContext = (auth: GameSessionTokenPayload | null, options: { hasGeneral?: boolean } = {}): GameApiContext => {
|
const buildContext = (
|
||||||
|
auth: GameSessionTokenPayload | null,
|
||||||
|
options: { hasGeneral?: boolean; worldMeta?: unknown } = {}
|
||||||
|
): GameApiContext => {
|
||||||
const db = {
|
const db = {
|
||||||
general: {
|
general: {
|
||||||
findFirst: async ({ where }: { where: { userId: string } }) =>
|
findFirst: async ({ where }: { where: { userId: string } }) =>
|
||||||
options.hasGeneral === false ? null : { id: where.userId === 'owner-a' ? 1 : 2, userId: where.userId },
|
options.hasGeneral === false ? null : { id: where.userId === 'owner-a' ? 1 : 2, userId: where.userId },
|
||||||
},
|
},
|
||||||
worldState: {
|
worldState: {
|
||||||
findFirst: async () => ({ currentYear: 220, currentMonth: 1, meta: { serverId: currentServerId } }),
|
findFirst: async () => ({
|
||||||
|
currentYear: 220,
|
||||||
|
currentMonth: 1,
|
||||||
|
meta: options.worldMeta ?? { serverId: currentServerId },
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
yearbookHistory: {
|
yearbookHistory: {
|
||||||
findFirst: async (args: {
|
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 () => {
|
it('uses stored logs for a past month of the current generation', async () => {
|
||||||
const caller = appRouter.createCaller(buildContext(authFor('owner-a')));
|
const caller = appRouter.createCaller(buildContext(authFor('owner-a')));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user