Files
OpenFrontIO/src/core/ApiSchemas.ts
T
Evan 97d0a05d58 Rewarded videos ads to test a skin (#3120)
## Description:

Added rewarded video ads for skin trials via Playwire's
manuallyCreateRewardUi API. Users can now click "Try me" to watch a
video ad and receive a temporary skin trial. Upon completion a temporary
flare is granted to the player so they have ~5 minutes to use the skin.

added getPlayerCosmeticsRefs and getPlayerCosmetics to Cosmetics.ts to
centralize cosmetic retrieval & validation.

<img width="801" height="534" alt="Screenshot 2026-02-10 at 7 58 14 PM"
src="https://github.com/user-attachments/assets/51cc378c-2feb-4692-8cf2-20ee54cea3b8"
/>

## 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:

evan
2026-02-11 20:52:48 -08:00

184 lines
4.9 KiB
TypeScript

import { z } from "zod";
import { base64urlToUuid } from "./Base64";
import { BigIntStringSchema, PlayerStatsSchema } from "./StatsSchemas";
import { Difficulty, GameMapType, GameMode, GameType } from "./game/Game";
export const RefreshResponseSchema = z.object({
token: z.string(),
});
export type RefreshResponse = z.infer<typeof RefreshResponseSchema>;
export const TokenPayloadSchema = z.object({
jti: z.string(),
sub: z
.string()
.refine(
(val) => {
const uuid = base64urlToUuid(val);
return !!uuid;
},
{
message: "Invalid base64-encoded UUID",
},
)
.transform((val) => {
const uuid = base64urlToUuid(val);
if (!uuid) throw new Error("Invalid base64 UUID");
return uuid;
}),
iat: z.number(),
iss: z.string(),
aud: z.string(),
exp: z.number(),
});
export type TokenPayload = z.infer<typeof TokenPayloadSchema>;
export const DiscordUserSchema = z.object({
id: z.string(),
avatar: z.string().nullable(),
username: z.string(),
global_name: z.string().nullable(),
discriminator: z.string(),
});
export type DiscordUser = z.infer<typeof DiscordUserSchema>;
const SingleplayerMapAchievementSchema = z.object({
mapName: z.enum(GameMapType),
difficulty: z.enum(Difficulty),
});
export const UserMeResponseSchema = z.object({
user: z.object({
discord: DiscordUserSchema.optional(),
email: z.string().optional(),
}),
player: z.object({
publicId: z.string(),
roles: z.string().array().optional(),
flares: z.string().array().optional(),
flareExpiration: z.record(z.string(), z.number()).optional(),
tempFlaresCooldown: z.boolean(),
achievements: z
.array(
z.object({
type: z.literal("singleplayer-map"), // TODO: change the shape to be more flexible when we have more achievements
data: z.array(SingleplayerMapAchievementSchema),
}),
)
.optional(),
leaderboard: z
.object({
oneVone: z
.object({
elo: z.number().optional(),
})
.optional(),
})
.optional(),
}),
});
export type UserMeResponse = z.infer<typeof UserMeResponseSchema>;
export const PlayerStatsLeafSchema = z.object({
wins: BigIntStringSchema,
losses: BigIntStringSchema,
total: BigIntStringSchema,
stats: PlayerStatsSchema,
});
export type PlayerStatsLeaf = z.infer<typeof PlayerStatsLeafSchema>;
export const PlayerStatsTreeSchema = z.partialRecord(
z.enum(GameType),
z.partialRecord(
z.enum(GameMode),
z.partialRecord(z.enum(Difficulty), PlayerStatsLeafSchema),
),
);
export type PlayerStatsTree = z.infer<typeof PlayerStatsTreeSchema>;
export const PlayerGameSchema = z.object({
gameId: z.string(),
start: z.iso.datetime(),
mode: z.enum(GameMode),
type: z.enum(GameType),
map: z.enum(GameMapType),
difficulty: z.enum(Difficulty),
clientId: z.string().optional(),
});
export type PlayerGame = z.infer<typeof PlayerGameSchema>;
export const PlayerProfileSchema = z.object({
createdAt: z.iso.datetime(),
user: DiscordUserSchema.optional(),
games: PlayerGameSchema.array(),
stats: PlayerStatsTreeSchema,
});
export type PlayerProfile = z.infer<typeof PlayerProfileSchema>;
export const ClanLeaderboardEntrySchema = z.object({
clanTag: z.string(),
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(),
});
export type ClanLeaderboardResponse = z.infer<
typeof ClanLeaderboardResponseSchema
>;
export const PlayerLeaderboardEntrySchema = z.object({
rank: z.number(),
playerId: z.string(),
username: z.string(),
clanTag: z.string().optional(),
flag: z.string().optional(),
elo: z.number(),
games: z.number(),
wins: z.number(),
losses: z.number(),
winRate: z.number(),
});
export type PlayerLeaderboardEntry = z.infer<
typeof PlayerLeaderboardEntrySchema
>;
export const PlayerLeaderboardResponseSchema = z.object({
players: PlayerLeaderboardEntrySchema.array(),
});
export type PlayerLeaderboardResponse = z.infer<
typeof PlayerLeaderboardResponseSchema
>;
export const RankedLeaderboardEntrySchema = z.object({
rank: z.number(),
elo: z.number(),
peakElo: z.number().nullable(),
wins: z.number(),
losses: z.number(),
total: z.number(),
public_id: z.string(),
user: DiscordUserSchema.nullable().optional(),
username: z.string(),
clanTag: z.string().nullable().optional(),
});
export type RankedLeaderboardEntry = z.infer<
typeof RankedLeaderboardEntrySchema
>;
export const RankedLeaderboardResponseSchema = z.object({
"1v1": RankedLeaderboardEntrySchema.array(),
});
export type RankedLeaderboardResponse = z.infer<
typeof RankedLeaderboardResponseSchema
>;