Files
OpenFrontIO/src/core/ClanApiSchemas.ts
T
Ryan a14cf0edc1 Clan Game History (#3988)
## Description:

Adds 
<img width="1046" height="901" alt="image"
src="https://github.com/user-attachments/assets/930b0d27-4707-4836-b068-620346e7e3a7"
/>

continuation of infra https://github.com/openfrontio/infra/pull/345
## Please complete the following:

- [x] I have added screenshots for all UI updates
- [x] I process any text displayed to the user through translateText()
and I've added it to the en.json file
- [x] I have added relevant tests to the test directory
- [x] I confirm I have thoroughly tested these changes and take full
responsibility for any bugs introduced

## Please put your Discord username so you can be contacted if a bug or
regression is found:

w.o.n
2026-05-22 22:30:16 +01:00

185 lines
5.7 KiB
TypeScript

import { z } from "zod";
import { ClanTagSchema } from "./Schemas";
const RequiredClanTagSchema = ClanTagSchema.unwrap();
export const ClanLeaderboardEntrySchema = z.object({
clanTag: RequiredClanTagSchema,
games: z.number(),
wins: z.number(),
losses: z.number(),
playerSessions: z.number(),
weightedWins: z.number(),
weightedLosses: z.number(),
weightedWLRatio: z.number(),
});
export type ClanLeaderboardEntry = z.infer<typeof ClanLeaderboardEntrySchema>;
export const ClanLeaderboardResponseSchema = z.object({
start: z.iso.datetime(),
end: z.iso.datetime(),
clans: ClanLeaderboardEntrySchema.array(),
total: z.number().optional(),
limit: z.number().optional(),
});
export type ClanLeaderboardResponse = z.infer<
typeof ClanLeaderboardResponseSchema
>;
export const ClanInfoSchema = z.object({
name: z.string().max(35),
tag: RequiredClanTagSchema,
description: z.string().max(200),
isOpen: z.boolean(),
createdAt: z.iso.datetime().optional(),
memberCount: z.number().optional(),
});
export type ClanInfo = z.infer<typeof ClanInfoSchema>;
export const ClanBrowseResponseSchema = z.object({
results: ClanInfoSchema.array(),
total: z.number(),
page: z.number(),
limit: z.number(),
});
export type ClanBrowseResponse = z.infer<typeof ClanBrowseResponseSchema>;
export const ClanMemberWLSchema = z.object({
wins: z.number(),
losses: z.number(),
});
export type ClanMemberWL = z.infer<typeof ClanMemberWLSchema>;
export const ClanMemberStatsSchema = z.object({
total: ClanMemberWLSchema,
ffa: ClanMemberWLSchema,
team: ClanMemberWLSchema,
hvn: ClanMemberWLSchema,
duos: ClanMemberWLSchema,
trios: ClanMemberWLSchema,
quads: ClanMemberWLSchema,
"2": ClanMemberWLSchema,
"3": ClanMemberWLSchema,
"4": ClanMemberWLSchema,
"5": ClanMemberWLSchema,
"6": ClanMemberWLSchema,
"7": ClanMemberWLSchema,
ranked: ClanMemberWLSchema,
"1v1": ClanMemberWLSchema,
});
export type ClanMemberStats = z.infer<typeof ClanMemberStatsSchema>;
export const TEAM_BREAKDOWN_KEYS = [
"duos",
"trios",
"quads",
"2",
"3",
"4",
"5",
"6",
"7",
] as const satisfies readonly (keyof ClanMemberStats)[];
export const RANKED_BREAKDOWN_KEYS = [
"1v1",
] as const satisfies readonly (keyof ClanMemberStats)[];
export const ClanMemberSchema = z.object({
role: z.enum(["leader", "officer", "member"]),
joinedAt: z.iso.datetime(),
publicId: z.string(),
stats: ClanMemberStatsSchema.optional(),
});
export type ClanMember = z.infer<typeof ClanMemberSchema>;
export const ClanMembersResponseSchema = z.object({
results: ClanMemberSchema.array(),
total: z.number(),
page: z.number(),
limit: z.number(),
pendingRequests: z.number().optional(),
});
export type ClanMembersResponse = z.infer<typeof ClanMembersResponseSchema>;
export const ClanJoinRequestSchema = z.object({
publicId: z.string(),
createdAt: z.iso.datetime(),
});
export type ClanJoinRequest = z.infer<typeof ClanJoinRequestSchema>;
export const ClanRequestsResponseSchema = z.object({
results: ClanJoinRequestSchema.array(),
total: z.number(),
page: z.number(),
limit: z.number(),
});
export type ClanRequestsResponse = z.infer<typeof ClanRequestsResponseSchema>;
export const ClanBanSchema = z.object({
publicId: z.string(),
bannedBy: z.string(),
reason: z.string().max(200).nullable(),
createdAt: z.iso.datetime(),
});
export type ClanBan = z.infer<typeof ClanBanSchema>;
export const ClanBansResponseSchema = z.object({
results: ClanBanSchema.array(),
total: z.number(),
page: z.number(),
limit: z.number(),
});
export type ClanBansResponse = z.infer<typeof ClanBansResponseSchema>;
export const JoinClanResponseSchema = z.object({
status: z.enum(["joined", "requested"]),
});
export type JoinClanResponse = z.infer<typeof JoinClanResponseSchema>;
export const ClanGamePlayerSchema = z.object({
publicId: z.string(),
username: z.string(),
won: z.boolean(),
});
export type ClanGamePlayer = z.infer<typeof ClanGamePlayerSchema>;
// "incomplete" covers games with no recorded winner.
// The server stamps this when winnerType IS NULL,
// so we have to accept it on the wire even if the UI collapses it back
// into the defeat-styled badge.
export const ClanGameResultSchema = z.enum(["victory", "defeat", "incomplete"]);
export type ClanGameResult = z.infer<typeof ClanGameResultSchema>;
export const ClanGameFilters = ["ffa", "team", "hvn", "ranked"] as const;
export const ClanGameFilterSchema = z.enum(ClanGameFilters);
export type ClanGameFilter = z.infer<typeof ClanGameFilterSchema>;
export const ClanGameSchema = z.object({
gameId: z.string(),
start: z.iso.datetime(),
durationSeconds: z.number().int().nonnegative(),
map: z.string().optional(),
mode: z.string().optional(),
// playerTeams is `null` (not absent) for FFA / non-team games — use
// `.nullish()` so the wire `null` doesn't fail the parse.
playerTeams: z.string().nullish(),
rankedType: z.string().optional(),
result: ClanGameResultSchema.optional(),
// Mirrors games.num_players nullability — historical rows may not
// carry a value. Use `.nullish()` so wire `null` parses cleanly.
totalPlayers: z.number().int().nonnegative().nullish(),
clanPlayers: ClanGamePlayerSchema.array(),
});
export type ClanGame = z.infer<typeof ClanGameSchema>;
export const ClanGamesResponseSchema = z.object({
results: ClanGameSchema.array(),
// Opaque continuation token. Round-trip verbatim as the `cursor` query
// parameter to fetch the next page; never construct or parse it.
// `null` means the server has no more rows to serve. Page size is
// fixed server-side, so the client never sends a limit.
nextCursor: z.string().nullable(),
});
export type ClanGamesResponse = z.infer<typeof ClanGamesResponseSchema>;