import { type Ability, type Blueprint, type Item, type Method, type Resource, type Rule, type Species, parseAbility, parseBlueprint, parseItem, parseMethod, parseResource, parseRule, parseSpecies, } from "@proscenium/playbill"; import { z } from "zod"; const Base = z.object({ $define: z.string(), $hidden: z.boolean().default(false), id: z.string(), // TODO: Validate ID shapes name: z.string().default("Unnamed Component"), description: z.string().default("No description provided"), categories: z.array(z.string()).default([]), }); const AbilitySchema = Base.extend({ $define: z.literal("ability"), type: z.string().default("action"), ap: z.number().int().default(0), hp: z.number().int().default(0), ep: z.number().int().default(0), xp: z.number().int().default(1), damage: z.number().int().default(0), damageType: z.string().default("phy"), roll: z.string().default("none"), }); const BlueprintSchema = Base.extend({ $define: z.literal("blueprint"), species: z.string(), items: z.array(z.string()).default([]), abilities: z.array(z.string()).default([]), ap: z.number().int().default(4), hp: z.number().int().default(5), ep: z.number().int().default(1), xp: z.number().int().default(0), muscle: z.string().default("novice"), focus: z.string().default("novice"), knowledge: z.string().default("novice"), charm: z.string().default("novice"), cunning: z.string().default("novice"), spark: z.string().default("novice"), }); const GlossarySchema = Base.extend({ $define: z.literal("glossary"), terms: z.record(z.string(), z.string()), }); const BaseItem = Base.extend({ $define: z.literal("item"), rarity: z.string().default("common"), affinity: z.string().default("none"), damage: z.number().int().default(0), damageType: z.string().default("phy"), abilities: z.array(z.string()).default([]), }); const ItemSchema = z.discriminatedUnion("type", [ BaseItem.extend({ type: z.literal("wielded"), hands: z.number().int().default(1), }), BaseItem.extend({ type: z.literal("worn"), slot: z.string().default("unique"), }), BaseItem.extend({ type: z.literal("trinket"), }), ]); const MethodSchema = Base.extend({ $define: z.literal("method"), curator: z.string().default("Someone"), abilities: z.array(z.array(z.string())).default([]), rank1: z.array(z.string()).default([]), rank2: z.array(z.string()).default([]), rank3: z.array(z.string()).default([]), rank4: z.array(z.string()).default([]), rank5: z.array(z.string()).default([]), rank6: z.array(z.string()).default([]), rank7: z.array(z.string()).default([]), rank8: z.array(z.string()).default([]), rank9: z.array(z.string()).default([]), }); const ResourceSchema = z.discriminatedUnion("type", [ Base.extend({ $define: z.literal("resource"), type: z.literal("text"), }), Base.extend({ $define: z.literal("resource"), type: z.literal("image"), url: z.string(), }), Base.extend({ $define: z.literal("resource"), type: z.literal("table"), data: z.array(z.array(z.string())), }), ]); const RuleSchema = Base.extend({ $define: z.literal("rule"), overrule: z.nullable(z.string()).default(null), order: z.number().int().default(Number.POSITIVE_INFINITY), }); const SpeciesSchema = Base.extend({ $define: z.literal("species"), hands: z.number().int().default(2), abilities: z.array(z.string()).default([]), ap: z.number().int().default(0), hp: z.number().int().default(0), ep: z.number().int().default(0), xp: z.number().int().default(0), muscle: z.string().default("novice"), focus: z.string().default("novice"), knowledge: z.string().default("novice"), charm: z.string().default("novice"), cunning: z.string().default("novice"), spark: z.string().default("novice"), }); const SchemaMapping = { ability: AbilitySchema, blueprint: BlueprintSchema, glossary: GlossarySchema, item: ItemSchema, method: MethodSchema, resource: ResourceSchema, rule: RuleSchema, species: SpeciesSchema, } as const; type ComponentType = keyof typeof SchemaMapping; const parseComponentType = (val: unknown): ComponentType => { if (typeof val !== "string") { throw new Error("Component type must be a string"); } if (!(val in SchemaMapping)) { throw new Error(`Unknown component type: ${val}`); } return val as ComponentType; }; export type ParsedComponent = | { type: "ability"; component: Ability; } | { type: "blueprint"; component: Blueprint; } | { type: "item"; component: Item; } | { type: "method"; component: Method; } | { type: "resource"; component: Resource; } | { type: "rule"; component: Rule; } | { type: "species"; component: Species; }; function parseAbilityDefinition(obj: unknown): ParsedComponent { const parsed = AbilitySchema.parse(obj); const ability = parseAbility({ id: parsed.id, name: parsed.name, description: parsed.description, categories: parsed.categories, type: parsed.type, ap: parsed.ap, ep: parsed.ep, hp: parsed.hp, xp: parsed.xp, damage: parsed.damage > 0 ? { amount: parsed.damage, type: parsed.damageType, } : null, roll: parsed.roll, }); return { type: "ability", component: ability, }; } function parseBlueprintDefinition(obj: unknown): ParsedComponent { const parsed = BlueprintSchema.parse(obj); const blueprint = parseBlueprint({ id: parsed.id, name: parsed.name, description: parsed.description, categories: parsed.categories, species: parsed.species, items: parsed.items, abilities: parsed.abilities, baggage: [], ap: parsed.ap, hp: parsed.hp, ep: parsed.ep, xp: parsed.xp, muscle: parsed.muscle, focus: parsed.focus, knowledge: parsed.knowledge, charm: parsed.charm, cunning: parsed.cunning, spark: parsed.spark, }); return { type: "blueprint", component: blueprint, }; } function parseItemDefinition(obj: unknown): ParsedComponent { const parsed = ItemSchema.parse(obj); return { type: "item", component: parseItem({ ...parsed, damage: parsed.damage > 0 ? { amount: parsed.damage, type: parsed.damageType, } : null, abilities: parsed.abilities, tweak: "", temper: "", }), }; } function parseMethodDefinition(obj: unknown): ParsedComponent { const parsed = MethodSchema.parse(obj); // Prefer `abilities` if defined, otherwise // construct `abilities` using the rankX properties const abilities = parsed.abilities; if (abilities.length === 0) { const allRanks = [ parsed.rank1, parsed.rank2, parsed.rank3, parsed.rank4, parsed.rank5, parsed.rank6, parsed.rank7, parsed.rank8, parsed.rank9, ]; // Add all ranks until an empty rank is found for (const rank of allRanks) { if (rank.length > 0) { abilities.push(rank); } else { break; } } } const method = { id: parsed.id, name: parsed.name, description: parsed.description, categories: parsed.categories, curator: parsed.curator, abilities: abilities, }; return { type: "method", component: parseMethod(method), }; } function parseResourceDefinition(obj: unknown): ParsedComponent { const parsed = ResourceSchema.parse(obj); return { type: "resource", component: parseResource(parsed), }; } function parseRuleDefinition(obj: unknown): ParsedComponent { const parsed = RuleSchema.parse(obj); return { type: "rule", component: parseRule(parsed), }; } function parseSpeciesDefinition(obj: unknown): ParsedComponent { const parsed = SpeciesSchema.parse(obj); return { type: "species", component: parseSpecies(parsed), }; } export function parsePlaybillComponent(obj: unknown): ParsedComponent | null { const baseParse = Base.parse(obj); if (baseParse.$hidden) { return null; } const type = parseComponentType(baseParse.$define); const schema = SchemaMapping[type]; const component = schema.parse(obj); switch (type) { case "ability": return parseAbilityDefinition(component); case "blueprint": return parseBlueprintDefinition(component); case "item": return parseItemDefinition(component); case "method": return parseMethodDefinition(component); case "resource": return parseResourceDefinition(component); case "rule": return parseRuleDefinition(component); case "species": return parseSpeciesDefinition(component); default: throw new Error(`Unknown component type: ${type}`); } }