Exact source used by this build. SHA-256: e289465a84ab0d7cfa3cb3b5b5266670fa7091662d3e75bd29a1615327fe2106.

1 import { z } from "zod";
2 import { SeasonSchema } from "../seasons/schema";
3 
4 export const VillagerIdSchema = z.string().regex(/^villager_\d{2}$/);
5 export const VillagerFacingSchema = z.enum(["up", "down", "left", "right"]);
6 export const GiftTasteSchema = z.enum(["love", "like", "neutral", "dislike", "hate"]);
7 export const HeartTierSchema = z.enum(["blue", "green", "yellow", "red", "purple", "star"]);
8 const PoiIdSchema = z.string().regex(/^poi\.[a-z][a-z0-9_]*\.[a-z][a-z0-9_.]*$/);
9 const MinuteSchema = z.number().int().min(360).max(1560);
10 export const GiftTasteListSchema = z.object({ itemIds: z.array(z.string().min(1)), tags: z.array(z.string().min(1)) });
11 export const GiftTastesSchema = z.object({ love: GiftTasteListSchema, like: GiftTasteListSchema, neutral: GiftTasteListSchema, dislike: GiftTasteListSchema, hate: GiftTasteListSchema });
12 export const VillagerPoiSchema = z.object({ id: PoiIdSchema, areaId: z.string().min(1), x: z.number().nonnegative(), y: z.number().nonnegative(), facing: VillagerFacingSchema });
13 export const ScheduleEntrySchema = z.object({ time: MinuteSchema, poiId: PoiIdSchema, facing: VillagerFacingSchema });
14 export const ScheduleSchema = z.array(ScheduleEntrySchema).min(1).superRefine((entries, context) => {
15   if (entries[0]?.time !== 360) context.addIssue({ code: "custom", message: "Schedule must start at 06:00" });
16   for (let index = 1; index < entries.length; index++) if (entries[index]!.time <= entries[index - 1]!.time) context.addIssue({ code: "custom", message: "Schedule times must increase" });
17 });
18 export const VillagerDefinitionSchema = z.object({
19   id: VillagerIdSchema, name: z.literal("TBD"), description: z.literal("TBD"),
20   homePoiId: PoiIdSchema, workplacePoiId: PoiIdSchema,
21   birthday: z.object({ season: SeasonSchema, day: z.number().int().min(1).max(28) }), marriageCandidate: z.boolean(),
22   giftTastes: GiftTastesSchema,
23   relationships: z.array(z.object({ villagerId: VillagerIdSchema, type: z.enum(["unspecified", "parent", "child", "sibling", "partner", "friend", "coworker"]), description: z.literal("TBD") })),
24   voiceSheet: z.string().regex(/^voices\/villager_\d{2}\.json$/), spriteKey: z.string().min(1), portraitKey: z.string().min(1), iconKey: z.string().min(1),
25   schedules: z.object({ weekday: ScheduleSchema, weekdays: z.record(z.string().regex(/^[0-6]$/), ScheduleSchema), rain: ScheduleSchema, seasons: z.partialRecord(SeasonSchema, ScheduleSchema), festivals: z.record(z.string(), ScheduleSchema) }),
26   shopId: z.string().min(1).optional(), counterPoiId: PoiIdSchema.optional(),
27 }).refine((villager) => !!villager.shopId === !!villager.counterPoiId, "Shopkeeper needs both shop and counter");
28 export const DialogueConditionSchema = z.object({
29   heartTier: HeartTierSchema.optional(), season: SeasonSchema.optional(), weekday: z.number().int().min(0).max(6).optional(),
30   weather: z.enum(["sunny", "rain", "storm", "wind", "snow"]).optional(), location: z.string().min(1).optional(),
31   firstMeeting: z.boolean().optional(), birthday: z.boolean().optional(), giftTaste: GiftTasteSchema.optional(),
32 });
33 export const DialogueEntrySchema = z.object({ key: z.string().min(1), villagerId: VillagerIdSchema.optional(), text: z.literal("TBD"), conditions: DialogueConditionSchema });
34 export const HeartEventStepSchema = z.discriminatedUnion("kind", [
35   z.object({ kind: z.literal("move"), actorId: z.string().min(1), poiId: PoiIdSchema }),
36   z.object({ kind: z.literal("face"), actorId: z.string().min(1), facing: VillagerFacingSchema }),
37   z.object({ kind: z.literal("dialogue"), actorId: z.string().min(1), key: z.string().min(1) }),
38   z.object({ kind: z.literal("choice"), key: z.string().min(1), choices: z.array(z.object({ id: z.string().min(1), key: z.string().min(1), friendshipDelta: z.number().int().min(-1500).max(250) })).min(2) }),
39   z.object({ kind: z.literal("fade"), direction: z.enum(["in", "out"]), durationMs: z.number().int().nonnegative() }),
40 ]);
41 export const HeartEventSchema = z.object({
42   id: z.string().min(1), villagerId: VillagerIdSchema,
43   trigger: z.object({ minHearts: z.number().int().min(0).max(14), areaId: z.string().min(1), startMinute: MinuteSchema, endMinute: MinuteSchema, weather: z.array(z.enum(["sunny", "rain", "storm", "wind", "snow"])).min(1) }).refine((trigger) => trigger.endMinute > trigger.startMinute, "Event time window must be increasing"),
44   actors: z.array(z.object({ id: z.string().min(1), poiId: PoiIdSchema, facing: VillagerFacingSchema })).min(1), script: z.array(HeartEventStepSchema).min(1),
45 });
46 export const VillagerMailSchema = z.object({ id: z.string().min(1), villagerId: VillagerIdSchema, key: z.string().min(1), trigger: z.discriminatedUnion("kind", [z.object({ kind: z.literal("friendship"), minHearts: z.number().int().min(1).max(14) }), z.object({ kind: z.literal("quest"), questId: z.string().min(1) })]), recipeId: z.string().regex(/^recipe_(cook|craft)_\d{2}$/).optional(), manifestKey: z.string().min(1) });
47 export const HelpWantedQuestSchema = z.object({ id: z.string().min(1), kind: z.enum(["delivery", "gathering", "slay", "fishing"]), villagerId: VillagerIdSchema, key: z.string().min(1), itemId: z.string().min(1).optional(), itemTag: z.string().min(1).optional(), monsterTag: z.string().min(1).optional(), quantity: z.number().int().positive(), durationDays: z.number().int().positive(), rewardItemId: z.string().min(1), manifestKey: z.string().min(1) });
48 export const VillagersConfigSchema = z.object({
49   bulletinPoiId: PoiIdSchema, mailboxPoiId: PoiIdSchema,
50   npcTilesPerSecond: z.number().positive(), updateIntervalMs: z.number().int().min(100), pointsPerHeart: z.number().int().positive(), friendshipCap: z.number().int().positive(), candidateCap: z.number().int().positive(), candidateDecayStop: z.number().int().positive(), spouseCap: z.number().int().positive(),
51   talkPoints: z.number().int(), giftsPerDay: z.number().int().positive(), giftsPerWeek: z.number().int().positive(), weeklyGiftBonus: z.number().int().nonnegative(), weeklyResetDay: z.number().int().min(0).max(6),
52   tastePoints: z.object({ love: z.number().int(), like: z.number().int(), neutral: z.number().int(), dislike: z.number().int(), hate: z.number().int() }),
53   giftQualityMultipliers: z.object({ regular: z.number().positive(), silver: z.number().positive(), gold: z.number().positive(), iridium: z.number().positive() }),
54   birthdayMultiplier: z.number().positive(), decay: z.object({ friend: z.number().int().nonnegative(), dating: z.number().int().nonnegative(), spouse: z.number().int().nonnegative() }), interactionRange: z.number().positive(),
55   questPriceMultiplier: z.number().positive(), questFriendship: z.number().int().positive(), bouquetItemId: z.string().min(1), pendantItemId: z.string().min(1), bouquetPrice: z.number().int().positive(), pendantPrice: z.number().int().positive(), bouquetHearts: z.number().int().positive(), bouquetFriendshipPoints: z.number().int().nonnegative(), pendantHearts: z.number().int().positive(), weddingDelayDays: z.number().int().positive(), spouseChores: z.array(z.enum(["water_crops", "feed_animals", "repair_fences"])),
56   dialogueCharactersPerSecond: z.number().positive(), dialogueFallbackOrder: z.array(z.enum(["giftTaste", "firstMeeting", "birthday", "location", "weather", "season", "weekday", "heartTier", "default"])),
57   heartTiers: z.array(z.object({ id: HeartTierSchema, minHearts: z.number().int().nonnegative(), color: z.string().regex(/^#[0-9a-f]{6}$/), iconKey: z.string().min(1) })).min(1),
58 });
59 export const VillagersContentSchema = z.object({
60   config: VillagersConfigSchema, villagers: z.array(VillagerDefinitionSchema).min(1), pois: z.array(VillagerPoiSchema), universalTastes: GiftTastesSchema,
61   dialogue: z.array(DialogueEntrySchema), events: z.array(HeartEventSchema), mail: z.array(VillagerMailSchema), quests: z.array(HelpWantedQuestSchema),
62   festivalDays: z.array(z.object({ id: z.string().min(1), season: SeasonSchema, day: z.number().int().min(1).max(28) })),
63   manifest: z.record(z.string().min(1), z.object({ kind: z.enum(["sprite", "portrait", "icon", "panel"]), placeholderColor: z.string().regex(/^#[0-9a-f]{6}$/), text: z.literal("TBD") })),
64 });
65 export const VoiceSheetSchema = z.object({ tone: z.literal("TBD").optional(), vocabulary: z.array(z.literal("TBD")).optional(), notes: z.literal("TBD").optional() }).strict();
66 export type VillagersContent = z.infer<typeof VillagersContentSchema>;
67 export type VillagerDefinition = z.infer<typeof VillagerDefinitionSchema>;
68 export type VillagerPoi = z.infer<typeof VillagerPoiSchema>;
69 export type ScheduleEntry = z.infer<typeof ScheduleEntrySchema>;
70 export type GiftTaste = z.infer<typeof GiftTasteSchema>;
71 export type DialogueEntry = z.infer<typeof DialogueEntrySchema>;
72 export type HeartEvent = z.infer<typeof HeartEventSchema>;
73 export type HeartEventStep = z.infer<typeof HeartEventStepSchema>;
74 export type VillagerMail = z.infer<typeof VillagerMailSchema>;
75 export type HelpWantedQuest = z.infer<typeof HelpWantedQuestSchema>;
76