Move playbill to its own repo

This commit is contained in:
Endeavorance 2025-02-22 14:30:43 -05:00
parent 1c64a941cd
commit 879e508d95
8 changed files with 20 additions and 490 deletions

View file

@ -1,12 +1,13 @@
import { parseArgs } from "node:util";
import chalk from "chalk";
import { loadBindingFile } from "./binding";
import { DirectiveError, processLang } from "./lang";
import {
type AnyResource,
DirectiveError,
ValidatedPlaybillSchema,
compileScript,
getEmptyPlaybill,
} from "./playbill-schema";
} from "@proscenium/playbill";
import chalk from "chalk";
import { loadBindingFile } from "./binding";
import { loadResourceFile } from "./resource";
import { usage } from "./usage";
@ -148,7 +149,7 @@ async function main(): Promise<boolean> {
verboseLog("Processing descriptions");
for (const resource of loadedResources) {
try {
resource.description = await processLang(resource.description, playbill);
resource.description = compileScript(resource.description, playbill);
} catch (error) {
if (error instanceof DirectiveError) {
console.error(

View file

@ -1,107 +0,0 @@
import { z } from "zod";
import { StatsSchema, type Playbill } from "./playbill-schema";
const DirectiveSchema = z.enum(["dmg", "ability", "stat", "rule"] as const);
type DirectiveName = z.infer<typeof DirectiveSchema>;
type DirectiveHandler = (playbill: Playbill, ...args: string[]) => string;
export class DirectiveError extends Error { }
const ProsceniumLangDirectives: Record<DirectiveName, DirectiveHandler> = {
dmg: (_, amount = "1", type = "phy") => {
const DAMAGE_TYPES = ["phy", "arc", "physical", "arcane"];
if (!DAMAGE_TYPES.includes(type)) {
throw new DirectiveError(`Unknown damage type: ${type}`);
}
const finalizedType = type.slice(0, 3);
return `<em class='dmg ${finalizedType}'>${amount}</em>`;
},
ability: (playbill: Playbill, ref: string) => {
if (typeof ref !== "string") {
throw new DirectiveError("Invalid ability reference");
}
const foundAbility = playbill.abilities.find((a) => a.id === ref);
if (foundAbility === undefined) {
throw new DirectiveError(`Unknown ability reference: ${ref}`);
}
return `<em class="ref-ability">${foundAbility.name}</em>`;
},
rule: (playbill: Playbill, ref: string) => {
if (typeof ref !== "string") {
throw new DirectiveError("Invalid rule reference");
}
const foundRule = playbill.rules.find((r) => r.id === ref);
if (foundRule === undefined) {
throw new DirectiveError(`Unknown ability reference: ${ref}`);
}
return `<em class="ref-rule">${foundRule.name}</em>`;
},
stat: (_, stat: string, amount = "") => {
const lower = stat.toLowerCase();
const parsed = StatsSchema.safeParse(lower);
if (!parsed.success) {
throw new DirectiveError(`Unknown stat: ${stat}`);
}
if (amount !== "") {
const amountAsNum = Number.parseInt(amount, 10);
if (Number.isNaN(amountAsNum)) {
throw new DirectiveError(`Invalid amount: ${amount}`);
}
return `<em class="stat">${amount} ${stat.toUpperCase()}</em>`;
}
return `<em class="stat">${stat.toUpperCase()}</em>`;
},
};
function processCommands(text: string, playbill: Playbill): string {
const commandPattern = /\{(\w+):([^}]+)\}/g;
return text.replace(commandPattern, (_match, command, args) => {
const directive = DirectiveSchema.parse(command);
const handler = ProsceniumLangDirectives[directive];
const parsedArgs = [];
let currentArg = "";
let inQuotes = false;
for (let i = 0; i < args.length; i++) {
const char = args[i];
if (char === '"' && (i === 0 || args[i - 1] !== "\\")) {
inQuotes = !inQuotes;
} else if (char === "," && !inQuotes) {
parsedArgs.push(currentArg.trim().replace(/^"|"$/g, ""));
currentArg = "";
} else {
currentArg += char;
}
}
parsedArgs.push(currentArg.trim().replace(/^"|"$/g, ""));
return handler(playbill, ...parsedArgs);
});
}
export async function processLang(
input: string,
playbill: Playbill,
): Promise<string> {
const lines = input.split("\n");
const processedLines: string[] = [];
for (const line of lines) {
const processed = processCommands(line, playbill);
processedLines.push(processed);
}
return processedLines.join("\n");
}

View file

@ -1,365 +0,0 @@
import { z } from "zod";
/*
* TYPE REFERENCES
* ------------------------
* Ability N/A
* Tag N/A
* Lore N/A
* Rule N/A
* Method Ability
* Item Tag
* Species Ability
* Entity Ability, Species
*/
// String shapes
export const IDSchema = z.string().regex(/[a-z\-]+/);
export const NameSchema = z.string().max(40).default("Unnamed");
export const DescriptionSchema = z.string().default("No description");
// Constants
export const STAT_ABBREVIATIONS = ["ap", "ep", "hp", "xp"] as const;
export const ITEM_TYPES = ["trinket", "worn", "wielded"] as const;
export const ROLL_TYPES = ["none", "attack", "fate"] as const;
export const TALENTS = [
"muscle",
"knowledge",
"focus",
"charm",
"cunning",
"spark",
] as const;
export const ABILITY_TYPES = ["action", "cue", "trait"] as const;
export const TALENT_PROWESS = ["novice", "adept", "master"] as const;
// Enums
export const StatsSchema = z.enum(STAT_ABBREVIATIONS);
export const ItemTypeSchema = z.enum(ITEM_TYPES);
export const RollTypeSchema = z.enum(ROLL_TYPES);
export const AbilityTypeSchema = z.enum(ABILITY_TYPES);
export const TalentSchema = z.enum(TALENTS);
export const TalentProwessSchema = z.enum(TALENT_PROWESS);
const ResourceTypes = [
"ability",
"species",
"entity",
"item",
"tag",
"lore",
"method",
"rule",
"playbill",
] as const;
const ResourceTypeSchema = z.enum(ResourceTypes);
type ResourceType = z.infer<typeof ResourceTypeSchema>;
// Inner utility shapes
export const StatSpreadSchema = z.object({
ap: z.number().default(0),
ep: z.number().default(0),
hp: z.number().default(0),
xp: z.number().default(0),
});
export const TalentSpreadSchema = z.object({
muscle: TalentProwessSchema.default("novice"),
knowledge: TalentProwessSchema.default("novice"),
focus: TalentProwessSchema.default("novice"),
charm: TalentProwessSchema.default("novice"),
cunning: TalentProwessSchema.default("novice"),
spark: TalentProwessSchema.default("novice"),
});
export const PlaybillResourceSchema = z.object({
$define: ResourceTypeSchema,
id: IDSchema,
name: NameSchema,
description: DescriptionSchema,
});
// An object with optional properties to define passive effects
// offered from an ability. These are aggregated by the game
// engine at runtime to determine the final stats of a character.
export const AbilityEffectSchema = z.object({
bonusMaxHp: z.number().optional(),
bonusMaxAp: z.number().optional(),
bonusMaxEp: z.number().optional(),
muscleProwess: TalentProwessSchema.optional(),
knowledgeProwess: TalentProwessSchema.optional(),
focusProwess: TalentProwessSchema.optional(),
charmProwess: TalentProwessSchema.optional(),
cunningProwess: TalentProwessSchema.optional(),
sparkProwess: TalentProwessSchema.optional(),
});
export const BaggageSchema = z.object({
title: z.string(),
description: z.string(),
});
// Playbill component shapes
export const AbilitySchema = PlaybillResourceSchema.extend({
$define: z.literal("ability"),
name: NameSchema.default("Unnamed Ability"),
type: AbilityTypeSchema,
costs: StatSpreadSchema.default({}),
roll: RollTypeSchema.or(TalentSchema).default("none"),
banes: z.array(z.string()).default([]),
boons: z.array(z.string()).default([]),
effects: AbilityEffectSchema.default({}),
});
export type PlaybillAbility = z.infer<typeof AbilitySchema>;
export const EntitySchema = PlaybillResourceSchema.extend({
$define: z.literal("entity"),
name: NameSchema.default("Unnamed Species"),
species: z.string(IDSchema),
abilities: z.array(IDSchema),
stats: StatSpreadSchema,
talents: TalentSpreadSchema,
damage: z.number().default(0),
baggage: z.array(BaggageSchema).default([]),
});
export const ItemTagSchema = PlaybillResourceSchema.extend({
$define: z.literal("tag"),
name: NameSchema.default("Unnamed Item Tag"),
icon: z.string().url(),
});
export const ItemSchema = PlaybillResourceSchema.extend({
$define: z.literal("item"),
name: NameSchema.default("Unnamed Item"),
type: ItemTypeSchema,
slots: z.number().min(0).max(5).default(0),
tags: z.array(IDSchema).default([]),
});
export const LoreSchema = PlaybillResourceSchema.extend({
$define: z.literal("lore"),
name: NameSchema.default("Unnamed Lore Entry"),
categories: z.array(z.string()).default([]),
});
export const MethodSchema = PlaybillResourceSchema.extend({
$define: z.literal("method"),
name: NameSchema.default("Unnamed Method"),
curator: NameSchema,
abilities: z.array(z.array(IDSchema)).default([]),
});
export const SpeciesSchema = PlaybillResourceSchema.extend({
$define: z.literal("species"),
name: NameSchema.default("Unnamed Species"),
abilities: z.array(IDSchema).default([]),
bonuses: AbilityEffectSchema.default({}),
});
export const RuleSchema = PlaybillResourceSchema.extend({
$define: z.literal("rule"),
overrule: z.string().optional(),
});
// Full Playbill Schema
export const PlaybillSchema = PlaybillResourceSchema.extend({
$define: z.literal("playbill"),
version: z.string().default("0"),
name: NameSchema.default("Unnamed Playbill"),
author: z.string().default("Anonymous"),
abilities: z.array(AbilitySchema).default([]),
entities: z.array(EntitySchema).default([]),
items: z.array(ItemSchema).default([]),
tags: z.array(ItemTagSchema).default([]),
lore: z.array(LoreSchema).default([]),
methods: z.array(MethodSchema).default([]),
species: z.array(SpeciesSchema).default([]),
rules: z.array(RuleSchema).default([]),
});
export const ValidatedPlaybillSchema = PlaybillSchema.superRefine(
(val, ctx) => {
// For each ability, ensure unique IDs
const abilityIds = new Set<string>();
for (const ability of val.abilities) {
if (abilityIds.has(ability.id)) {
ctx.addIssue({
message: `Ability ${ability.id} has a duplicate ID`,
code: z.ZodIssueCode.custom,
});
}
abilityIds.add(ability.id);
}
// For each method, ensure its abilities exist
// Also ensure unique IDs
const methodIds = new Set<string>();
for (const method of val.methods) {
if (methodIds.has(method.id)) {
ctx.addIssue({
message: `Method ${method.id} has a duplicate ID`,
code: z.ZodIssueCode.custom,
});
}
methodIds.add(method.id);
const methodAbilityIds = method.abilities.flat();
for (const abilityId of methodAbilityIds) {
if (!val.abilities.some((a) => a.id === abilityId)) {
ctx.addIssue({
message: `Method ${method.id} references undefined ability ${abilityId}`,
code: z.ZodIssueCode.custom,
});
}
}
}
// For each species, ensure its abilities and IDs
const speciesIds = new Set<string>();
for (const species of val.species) {
if (speciesIds.has(species.id)) {
ctx.addIssue({
message: `Species ${species.id} has a duplicate ID`,
code: z.ZodIssueCode.custom,
});
}
speciesIds.add(species.id);
const speciesAbilityIds = species.abilities.flat();
for (const abilityId of speciesAbilityIds) {
if (!val.abilities.some((a) => a.id === abilityId)) {
ctx.addIssue({
message: `Species ${species.id} references undefined ability ${abilityId}`,
code: z.ZodIssueCode.custom,
});
}
}
}
// For each entity, ensure its species and known abilities exist
const entityIds = new Set<string>();
for (const entity of val.entities) {
if (entityIds.has(entity.id)) {
ctx.addIssue({
message: `Entity ${entity.id} has a duplicate ID`,
code: z.ZodIssueCode.custom,
});
}
entityIds.add(entity.id);
for (const abilityId of entity.abilities) {
if (!val.abilities.some((a) => a.id === abilityId)) {
ctx.addIssue({
message: `Entity ${entity.id} references undefined ability ${abilityId}`,
code: z.ZodIssueCode.custom,
});
}
}
if (!val.species.some((s) => s.id === entity.species)) {
ctx.addIssue({
message: `Entity ${entity.id} references undefined species ${entity.species}`,
code: z.ZodIssueCode.custom,
});
}
}
// For each item, ensure its tags exist
const itemIds = new Set<string>();
for (const item of val.items) {
if (itemIds.has(item.id)) {
ctx.addIssue({
message: `Item ${item.id} has a duplicate ID`,
code: z.ZodIssueCode.custom,
});
}
itemIds.add(item.id);
for (const tag of item.tags) {
if (!val.tags.some((t) => t.id === tag)) {
ctx.addIssue({
message: `Item ${item.id} references undefined tag ${tag}`,
code: z.ZodIssueCode.custom,
});
}
}
}
// For each tag, ensure unique IDs
const tagIds = new Set<string>();
for (const tag of val.tags) {
if (tagIds.has(tag.id)) {
ctx.addIssue({
message: `Tag ${tag.id} has a duplicate ID`,
code: z.ZodIssueCode.custom,
});
}
tagIds.add(tag.id);
}
// For each rule, ensure unique IDs
const ruleIds = new Set<string>();
for (const rule of val.rules) {
if (ruleIds.has(rule.id)) {
ctx.addIssue({
message: `Rule ${rule.id} has a duplicate ID`,
code: z.ZodIssueCode.custom,
});
}
ruleIds.add(rule.id);
}
// For each lore, ensure unique IDs
const loreIds = new Set<string>();
for (const lore of val.lore) {
if (loreIds.has(lore.id)) {
ctx.addIssue({
message: `Lore ${lore.id} has a duplicate ID`,
code: z.ZodIssueCode.custom,
});
}
loreIds.add(lore.id);
}
},
).brand("ValidatedPlaybill");
export type Playbill = z.infer<typeof PlaybillSchema>;
export type ValidatedPlaybill = z.infer<typeof ValidatedPlaybillSchema>;
export const ResourceMap = {
ability: AbilitySchema,
species: SpeciesSchema,
entity: EntitySchema,
item: ItemSchema,
tag: ItemTagSchema,
method: MethodSchema,
lore: LoreSchema,
rule: RuleSchema,
playbill: PlaybillSchema,
} as const;
export const AnyResourceSchema = z.discriminatedUnion("$define", [
AbilitySchema,
SpeciesSchema,
EntitySchema,
ItemSchema,
ItemTagSchema,
MethodSchema,
LoreSchema,
RuleSchema,
PlaybillSchema,
]);
export type AnyResource = z.infer<typeof AnyResourceSchema>;
export function getEmptyPlaybill(): Playbill {
return PlaybillSchema.parse({
$define: "playbill",
id: "empty",
});
}

View file

@ -1,9 +1,9 @@
import YAML from "yaml";
import {
type AnyResource,
AnyResourceSchema,
ResourceMap,
} from "./playbill-schema";
} from "@proscenium/playbill";
import YAML from "yaml";
import { extractFrontmatter, extractMarkdown } from "./markdown";
type FileFormat = "yaml" | "markdown";

View file

@ -1,5 +1,5 @@
import { version } from "../package.json" with { type: "json" };
import chalk from "chalk";
import { version } from "../package.json" with { type: "json" };
export const usage = `
${chalk.bold("muse")} - Compile and validate Playbills for Proscenium