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

@ -7,10 +7,7 @@
},
"files": {
"ignoreUnknown": false,
"ignore": [
"*.d.ts",
"*.json"
]
"ignore": ["*.d.ts", "*.json"]
},
"formatter": {
"enabled": true,

View file

@ -2,8 +2,9 @@
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "lang",
"name": "@proscenium/muse",
"dependencies": {
"@proscenium/playbill": "0.0.1",
"chalk": "^5.4.1",
"yaml": "^2.7.0",
"zod": "^3.24.1",
@ -36,13 +37,15 @@
"@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="],
"@types/bun": ["@types/bun@1.2.2", "", { "dependencies": { "bun-types": "1.2.2" } }, "sha512-tr74gdku+AEDN5ergNiBnplr7hpDp3V1h7fqI2GcR/rsUaM39jpSeKH0TFibRvU0KwniRx5POgaYnaXbk0hU+w=="],
"@proscenium/playbill": ["@proscenium/playbill@0.0.1", "https://git.astral.camp/api/packages/proscenium/npm/%40proscenium%2Fplaybill/-/0.0.1/playbill-0.0.1.tgz", { "peerDependencies": { "typescript": "^5" } }, "sha512-uvv2Bt1+rjzuXl5y2t2C4KGgVda3orTZMVzb6gNAzStzKutiR3xJZkw2ULukYWWGyZ9BzzkRClSKVYka2Z3Png=="],
"@types/node": ["@types/node@22.13.1", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew=="],
"@types/bun": ["@types/bun@1.2.3", "", { "dependencies": { "bun-types": "1.2.3" } }, "sha512-054h79ipETRfjtsCW9qJK8Ipof67Pw9bodFWmkfkaUaRiIQ1dIV2VTlheshlBx3mpKr0KeK8VqnMMCtgN9rQtw=="],
"@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="],
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
"bun-types": ["bun-types@1.2.2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg=="],
"bun-types": ["bun-types@1.2.3", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-P7AeyTseLKAvgaZqQrvp3RqFM3yN9PlcLuSTe7SoJOfZkER73mLdT2vEQi8U64S1YvM/ldcNiQjn0Sn7H9lGgg=="],
"chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
@ -52,6 +55,6 @@
"yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="],
"zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="],
"zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
}
}

View file

@ -13,7 +13,8 @@
"dependencies": {
"chalk": "^5.4.1",
"yaml": "^2.7.0",
"zod": "^3.24.1"
"zod": "^3.24.1",
"@proscenium/playbill": "0.0.1"
},
"scripts": {
"fmt": "bunx --bun biome check --fix",

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