feat(game): align in-game GUI with ref captures

This commit is contained in:
2026-08-01 22:03:03 +00:00
parent 23cdc2adf2
commit 43ec211ff8
30 changed files with 1306 additions and 776 deletions
+21 -3
View File
@@ -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) => {
+26 -4
View File
@@ -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) ?? [],
};
});
+7
View File
@@ -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');