Exact source used by this build. SHA-256: 020991ef10f56f25cd5f8ac85a9ae46b0553ae97bc824c213595feb8d58fb90d.

1 import { z } from "zod";
2 import { SeasonSchema } from "../seasons/schema";
3 const position = { x: z.number().nonnegative(), y: z.number().nonnegative() };
4 const poi = { ...position, poiId: z.string().min(1) };
5 const visual = { manifestKey: z.string().min(1) };
6 export const FestivalSchema = z.object({
7   id: z.string().regex(/^festival_\d{2}$/), name: z.literal("TBD"), description: z.literal("TBD"), ...visual,
8   season: SeasonSchema, day: z.number().int().min(1).max(28), startMinute: z.number().int().min(360).max(1550),
9   endMinute: z.number().int().min(360).max(1560), returnMinute: z.literal(1320), areaId: z.string().min(1), entrance: z.object(poi),
10   layout: z.array(z.object({ id: z.string().min(1), kind: z.enum(["decor", "stall", "game"]), ...poi, ...visual })).min(3),
11   shop: z.object({ ...poi, stock: z.array(z.object({ id: z.string().min(1), itemId: z.string().min(1), price: z.number().int().nonnegative(), quantity: z.number().int().positive() })).min(1) }),
12   game: z.object({
13     kind: z.enum(["collect", "fishing", "display", "targets"]), durationMs: z.number().int().nonnegative(), winScore: z.number().int().positive(), multiplayerWinScore: z.number().int().positive(), rewardGold: z.number().int().nonnegative(),
14     collectibles: z.array(z.object({ id: z.string().min(1), ...position, ...visual })),
15     targets: z.array(z.object({ id: z.string().min(1), appearsMs: z.number().int().nonnegative(), expiresMs: z.number().int().positive(), points: z.number().int().positive(), ...visual })),
16     fishingBiteDelayMs: z.number().int().positive(), fishingBiteWindowMs: z.number().int().positive(), fishingCastCooldownMs: z.number().int().positive(), displaySlots: z.literal(9), shotCooldownMs: z.number().int().positive(),
17   }),
18 }).superRefine((festival, ctx) => {
19   if (festival.endMinute <= festival.startMinute || festival.endMinute > festival.returnMinute) ctx.addIssue({ code: "custom", message: "Invalid festival entry window" });
20   for (const key of ["layout"] as const) if (new Set(festival[key].map((v) => v.id)).size !== festival[key].length) ctx.addIssue({ code: "custom", message: "Duplicate festival layout id" });
21   if (new Set(festival.shop.stock.map((v) => v.id)).size !== festival.shop.stock.length) ctx.addIssue({ code: "custom", message: "Duplicate festival stock id" });
22   if (festival.game.kind !== "display" && festival.game.durationMs <= 0) ctx.addIssue({ code: "custom", message: "Timed games need a duration" });
23   for (const entries of [festival.game.targets, festival.game.collectibles]) if (new Set(entries.map((v) => v.id)).size !== entries.length) ctx.addIssue({ code: "custom", message: "Duplicate game target id" });
24   if (festival.game.kind === "collect" && festival.game.collectibles.length < festival.game.winScore) ctx.addIssue({ code: "custom", message: "Collect game cannot be won" });
25   if (festival.game.kind === "targets" && !festival.game.targets.length) ctx.addIssue({ code: "custom", message: "Target game needs targets" });
26   for (const target of festival.game.targets) if (target.expiresMs <= target.appearsMs || target.expiresMs > festival.game.durationMs) ctx.addIssue({ code: "custom", message: "Invalid target time window" });
27 });
28 const points = z.array(z.number().int().positive()).length(6);
29 export const FestivalsContentSchema = z.object({
30   festivals: z.array(FestivalSchema).length(4),
31   closedBuildings: z.array(z.object({ ...poi, areaId: z.string().min(1), width: z.number().positive(), height: z.number().positive() })),
32   displayScoring: z.object({ basePoints: z.number().int(), itemCountMultiplier: z.number().int(), emptySlotPenalty: z.number().int(), categoryPoints: z.number().int(), maxCategories: z.number().int().positive(), priceThresholds: z.array(z.number().int().nonnegative()).length(6), points: z.object({ regular: points, silver: points, gold: points, iridium: points }), placements: z.array(z.object({ score: z.number().int().nonnegative(), rewardGold: z.number().int().nonnegative() })).min(1) }),
33 });
34 export type FestivalsContent = z.infer<typeof FestivalsContentSchema>;
35