feat(game): align in-game GUI with ref captures
This commit is contained in:
@@ -492,9 +492,26 @@ export const publicRouter = router({
|
||||
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: { npcState: { gt: 0 } } }),
|
||||
...(includeAllWithToken
|
||||
? {}
|
||||
: {
|
||||
where: {
|
||||
OR: [
|
||||
{ npcState: 1 },
|
||||
{ npcState: 0, id: { in: poolGeneralIds } },
|
||||
],
|
||||
},
|
||||
}),
|
||||
select: {
|
||||
id: true,
|
||||
name: true,
|
||||
@@ -546,12 +563,13 @@ export const publicRouter = router({
|
||||
const maxLevel = Math.max(0, Math.floor(asNumber(worldConstants.maxLevel, 255)));
|
||||
const maxDedLevel = Math.max(0, Math.floor(asNumber(worldConstants.maxDedLevel, 30)));
|
||||
|
||||
// Legacy public NPC list put pool rows before possessed rows. The token-aware
|
||||
// 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 >= 2),
|
||||
...generals.filter((general) => general.npcState === 0),
|
||||
...generals.filter((general) => general.npcState === 1),
|
||||
];
|
||||
const rows = sourceRows.map((general) => {
|
||||
|
||||
@@ -20,6 +20,7 @@ type YearbookNation = {
|
||||
color: string;
|
||||
level: number;
|
||||
power: number;
|
||||
generalCount: number;
|
||||
cities: string[];
|
||||
};
|
||||
|
||||
@@ -52,13 +53,19 @@ const parseYearbookNations = (value: unknown): YearbookNation[] => {
|
||||
const color = typeof item.color === 'string' ? item.color : null;
|
||||
const level = typeof item.level === 'number' ? item.level : null;
|
||||
const power = typeof item.power === 'number' ? item.power : null;
|
||||
const generalCount =
|
||||
typeof item.generalCount === 'number'
|
||||
? item.generalCount
|
||||
: typeof item.gennum === 'number'
|
||||
? item.gennum
|
||||
: 0;
|
||||
const cities = Array.isArray(item.cities)
|
||||
? item.cities.filter((city): city is string => typeof city === 'string')
|
||||
: null;
|
||||
if (id === null || name === null || color === null || level === null || power === null || !cities) {
|
||||
continue;
|
||||
}
|
||||
output.push({ id, name, color, level, power, cities });
|
||||
output.push({ id, name, color, level, power, generalCount, cities });
|
||||
}
|
||||
return output;
|
||||
};
|
||||
@@ -155,9 +162,18 @@ const buildNationSnapshot = async (ctx: GameApiContext) => {
|
||||
cityNamesByNation.set(city.nationId, cityNames);
|
||||
}
|
||||
|
||||
const generalStatsByNation = new Map<number, { goldRice: number; statPower: number; expDed: number }>();
|
||||
const generalStatsByNation = new Map<
|
||||
number,
|
||||
{ goldRice: number; statPower: number; expDed: number; generalCount: number }
|
||||
>();
|
||||
for (const general of generalRows) {
|
||||
const entry = generalStatsByNation.get(general.nationId) ?? { goldRice: 0, statPower: 0, expDed: 0 };
|
||||
const entry = generalStatsByNation.get(general.nationId) ?? {
|
||||
goldRice: 0,
|
||||
statPower: 0,
|
||||
expDed: 0,
|
||||
generalCount: 0,
|
||||
};
|
||||
entry.generalCount += 1;
|
||||
entry.goldRice += general.gold + general.rice;
|
||||
const leadership = general.leadership;
|
||||
const strength = general.strength;
|
||||
@@ -170,7 +186,12 @@ const buildNationSnapshot = async (ctx: GameApiContext) => {
|
||||
}
|
||||
|
||||
return nationRows.map<YearbookNation>((nation) => {
|
||||
const generalStats = generalStatsByNation.get(nation.id) ?? { goldRice: 0, statPower: 0, expDed: 0 };
|
||||
const generalStats = generalStatsByNation.get(nation.id) ?? {
|
||||
goldRice: 0,
|
||||
statPower: 0,
|
||||
expDed: 0,
|
||||
generalCount: 0,
|
||||
};
|
||||
const cityStats = cityStatsByNation.get(nation.id) ?? { popSum: 0, valueSum: 0, maxSum: 0 };
|
||||
const resource = Math.round(((nation.gold ?? 0) + (nation.rice ?? 0) + generalStats.goldRice) / 100);
|
||||
const tech = nation.tech ?? 0;
|
||||
@@ -187,6 +208,7 @@ const buildNationSnapshot = async (ctx: GameApiContext) => {
|
||||
color: nation.color,
|
||||
level: nation.level,
|
||||
power,
|
||||
generalCount: generalStats.generalCount,
|
||||
cities: cityNamesByNation.get(nation.id) ?? [],
|
||||
};
|
||||
});
|
||||
|
||||
@@ -218,6 +218,13 @@ export const createGameApiServer = async () => {
|
||||
}
|
||||
|
||||
reply.hijack();
|
||||
const requestOrigin = request.headers.origin;
|
||||
if (typeof requestOrigin === 'string' && requestOrigin.length > 0) {
|
||||
// Hijacked SSE responses bypass Fastify's normal CORS response hook.
|
||||
reply.raw.setHeader('Access-Control-Allow-Origin', requestOrigin);
|
||||
reply.raw.setHeader('Access-Control-Allow-Credentials', 'true');
|
||||
reply.raw.setHeader('Vary', 'Origin');
|
||||
}
|
||||
reply.raw.setHeader('Content-Type', 'text/event-stream');
|
||||
reply.raw.setHeader('Cache-Control', 'no-cache');
|
||||
reply.raw.setHeader('Connection', 'keep-alive');
|
||||
|
||||
@@ -65,9 +65,9 @@ const buildContext = (auth: GameSessionTokenPayload | null = null): GameApiConte
|
||||
age: 44,
|
||||
officerLevel: 12,
|
||||
nationId: 1,
|
||||
leadership: 80,
|
||||
strength: 70,
|
||||
intel: 85,
|
||||
leadership: 90,
|
||||
strength: 95,
|
||||
intel: 75,
|
||||
experience: 16_000,
|
||||
dedication: 10_000,
|
||||
personalCode: 'None',
|
||||
@@ -78,14 +78,21 @@ const buildContext = (auth: GameSessionTokenPayload | null = null): GameApiConte
|
||||
];
|
||||
const db = {
|
||||
general: {
|
||||
findMany: async (args: { where?: { npcState: { gt: number } } }) => {
|
||||
findMany: async (args: {
|
||||
where?: { OR: [{ npcState: number }, { npcState: number; id: { in: number[] } }] };
|
||||
}) => {
|
||||
if (args.where) {
|
||||
expect(args.where).toEqual({ npcState: { gt: 0 } });
|
||||
return generalRows.filter((general) => general.npcState > 0);
|
||||
expect(args.where).toEqual({
|
||||
OR: [{ npcState: 1 }, { npcState: 0, id: { in: [30] } }],
|
||||
});
|
||||
return generalRows.filter((general) => general.npcState === 1 || general.id === 30);
|
||||
}
|
||||
return generalRows;
|
||||
},
|
||||
},
|
||||
selectPoolEntry: {
|
||||
findMany: async () => [{ generalId: 30 }],
|
||||
},
|
||||
nation: {
|
||||
findMany: async () => [{ id: 1, name: '촉', level: 5 }],
|
||||
},
|
||||
@@ -165,32 +172,7 @@ describe('public.getNpcList', () => {
|
||||
dedication: 700,
|
||||
dedicationText: '28품관',
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
name: '조운',
|
||||
picture: '20.jpg',
|
||||
imageServer: 1,
|
||||
npcState: 2,
|
||||
ownerName: '',
|
||||
age: 35,
|
||||
level: 5,
|
||||
officerLevel: 0,
|
||||
killturn: 0,
|
||||
nationId: 0,
|
||||
nationName: '-',
|
||||
nationLevel: 0,
|
||||
personality: null,
|
||||
specialDomestic: null,
|
||||
specialWar: null,
|
||||
statTotal: 260,
|
||||
leadership: 90,
|
||||
strength: 95,
|
||||
intelligence: 75,
|
||||
experience: 900,
|
||||
experienceText: '무명',
|
||||
dedication: 600,
|
||||
dedicationText: '28품관',
|
||||
},
|
||||
expect.objectContaining({ id: 30, name: '유비', npcState: 0, ownerName: '' }),
|
||||
]);
|
||||
expect(result.tokenKeepCounts).toEqual({});
|
||||
expect(JSON.stringify(result)).not.toContain('노출 금지');
|
||||
@@ -237,7 +219,7 @@ describe('public.getNpcList', () => {
|
||||
it('keeps pool rows before possessed NPCs when the selected value is tied', async () => {
|
||||
const result = await appRouter.createCaller(buildContext()).public.getNpcList({ sort: 3 });
|
||||
|
||||
expect(result.generals.map((general) => general.id)).toEqual([20, 10]);
|
||||
expect(result.generals.map((general) => general.id)).toEqual([30, 10]);
|
||||
});
|
||||
|
||||
it('falls an invalid legacy sort value back to name order', async () => {
|
||||
@@ -245,6 +227,6 @@ describe('public.getNpcList', () => {
|
||||
const result = await caller.public.getNpcList({ sort: 99 } as unknown as { sort: 1 });
|
||||
|
||||
expect(result.sort).toBe(1);
|
||||
expect(result.generals.map((general) => general.name)).toEqual(['관우', '조운']);
|
||||
expect(result.generals.map((general) => general.name)).toEqual(['관우', '유비']);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,7 @@ const archiveRows = [
|
||||
year: 200,
|
||||
month: 1,
|
||||
map: { year: 200, month: 1, startYear: 190, cityList: [], nationList: [] },
|
||||
nations: [{ id: 1, name: '촉', color: '#FF0000', level: 7, power: 1000, cities: ['성도'] }],
|
||||
nations: [{ id: 1, name: '촉', color: '#FF0000', level: 7, power: 1000, generalCount: 8, cities: ['성도'] }],
|
||||
globalHistory: ['<C>●</>1월: 기록 없음'],
|
||||
globalAction: ['<C>●</>1월: 기록 없음'],
|
||||
hash: 'archive-1',
|
||||
@@ -39,7 +39,7 @@ const archiveRows = [
|
||||
year: 200,
|
||||
month: 2,
|
||||
map: { year: 200, month: 2, startYear: 190, cityList: [], nationList: [] },
|
||||
nations: [{ id: 1, name: '촉', color: '#FF0000', level: 7, power: 1200, cities: ['성도'] }],
|
||||
nations: [{ id: 1, name: '촉', color: '#FF0000', level: 7, power: 1200, generalCount: 9, cities: ['성도'] }],
|
||||
globalHistory: ['<C>●</>2월: 기록 없음'],
|
||||
globalAction: ['<C>●</>2월: 기록 없음'],
|
||||
hash: 'archive-2',
|
||||
@@ -52,7 +52,7 @@ 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, cities: ['낙양'] }],
|
||||
nations: [{ id: 1, name: '현재기수국', color: '#00FF00', level: 7, power: 1300, generalCount: 10, cities: ['낙양'] }],
|
||||
globalHistory: ['저장된 현재 기수 과거 기록'],
|
||||
globalAction: ['저장된 현재 기수 과거 행동'],
|
||||
hash: 'current-archive',
|
||||
@@ -151,7 +151,7 @@ describe('historical yearbook access from dynasty', () => {
|
||||
data: {
|
||||
year: 200,
|
||||
month: 2,
|
||||
nations: [{ id: 1, name: '촉', cities: ['성도'] }],
|
||||
nations: [{ id: 1, name: '촉', generalCount: 9, cities: ['성도'] }],
|
||||
globalHistory: ['<C>●</>2월: 기록 없음'],
|
||||
globalAction: ['<C>●</>2월: 기록 없음'],
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user