375 lines
8.7 KiB
TypeScript
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}`);
|
|
}
|
|
}
|