muse/src/define/index.ts
2025-03-21 12:20:51 -04:00

375 lines
8.7 KiB
TypeScript

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}`);
}
}