Initial commit

This commit is contained in:
Endeavorance 2025-02-12 10:31:22 -05:00
commit 4e31f18045
15 changed files with 779 additions and 0 deletions

19
src/binding.ts Normal file
View file

@ -0,0 +1,19 @@
import YAML from "yaml";
import z from "zod";
const BindingSchema = z.object({
id: z.string(),
playbill: z.string().default("Unnamed playbill"),
author: z.string().optional(),
version: z.number(),
files: z.array(z.string()).default(["**/*.yaml"]),
});
type Binding = z.infer<typeof BindingSchema>;
export async function loadBindingFile(filePath: string): Promise<Binding> {
const file = Bun.file(filePath);
const text = await file.text();
const parsed = YAML.parse(text);
return BindingSchema.parse(parsed);
}

34
src/index.ts Normal file
View file

@ -0,0 +1,34 @@
import { Glob } from "bun";
import { loadBindingFile } from "./binding";
import type { AnyResource } from "./playbill-schema";
import { loadResourceFile } from "./resource";
const bindingPath = Bun.argv[2];
const binding = await loadBindingFile(bindingPath);
const fileGlobs = binding.files;
const bindingFileDirname = bindingPath.split("/").slice(0, -1).join("/");
const allFilePaths: string[] = [];
for (const thisGlob of fileGlobs) {
const glob = new Glob(thisGlob);
const results = glob.scanSync({
cwd: bindingFileDirname,
absolute: true,
followSymlinks: true,
onlyFiles: true,
});
allFilePaths.push(...results);
}
const loadedResources: AnyResource[] = [];
for (const filepath of allFilePaths) {
const loaded = await loadResourceFile(filepath);
loadedResources.push(...loaded);
}
console.log(loadedResources);

52
src/lang.ts Normal file
View file

@ -0,0 +1,52 @@
import type { ZodSchema } from "zod";
const directiveHandlers = {
dmg: (amount = "1", type = "physical") => {
return `<em class='dmg ${type}'>${amount}</em>`;
},
};
type DirectiveHandler = (...args: string[]) => string;
function processCommands(
text: string,
handlers: { [key: string]: DirectiveHandler },
): string {
const commandPattern = /\{(\w+):([^}]+)\}/g;
return text.replace(commandPattern, (match, command, args) => {
const handler = handlers[command];
if (handler) {
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(...parsedArgs);
}
return match;
});
}
export async function processLang(input: string): Promise<string> {
const lines = input.split("\n");
const processedLines: string[] = [];
for (const line of lines) {
const processed = processCommands(line, directiveHandlers);
processedLines.push(processed);
}
return processedLines.join("\n");
}

264
src/playbill-schema.ts Normal file
View file

@ -0,0 +1,264 @@
import { type ZodSchema, z } from "zod";
/*
* TYPE REFERENCES
* ------------------------
* Ability N/A
* Tag N/A
* Lore 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",
"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,
});
// 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([]),
});
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,
statMaxModifiers: StatSpreadSchema,
talents: TalentSpreadSchema,
});
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([]),
statMaxModifiers: StatSpreadSchema.default({}),
talentMinimums: TalentSpreadSchema.default({}),
});
// 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([]),
});
export const ValidatedPlaybillSchema = PlaybillSchema.superRefine(
(val, ctx) => {
// For each method, ensure its abilities exist
for (const method of val.methods) {
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
for (const species of val.species) {
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
for (const entity of val.entities) {
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
for (const item of val.items) {
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,
});
}
}
}
},
);
export type Playbill = z.infer<typeof PlaybillSchema>;
export type ValidatedPlaybill = z.infer<typeof ValidatedPlaybillSchema>;
export const ResourceMap: Record<ResourceType, ZodSchema> = {
ability: AbilitySchema,
species: SpeciesSchema,
entity: EntitySchema,
item: ItemSchema,
tag: ItemTagSchema,
method: MethodSchema,
lore: LoreSchema,
playbill: PlaybillSchema,
};
export const AnyResourceSchema = z.discriminatedUnion("$define", [
AbilitySchema,
SpeciesSchema,
EntitySchema,
ItemSchema,
ItemTagSchema,
MethodSchema,
LoreSchema,
PlaybillSchema,
]);
export type AnyResource = z.infer<typeof AnyResourceSchema>;
export function getEmptyPlaybill(): Playbill {
return PlaybillSchema.parse({
$define: "playbill",
id: "empty",
abilities: [
{
id: "fireball",
name: "Fireball",
type: "action",
description: "Cast a fireball! My goodness.",
},
{
id: "magic-shield",
name: "Magic Shield",
type: "cue",
description: "Defend yerself",
},
],
});
}

36
src/resource.ts Normal file
View file

@ -0,0 +1,36 @@
import YAML from "yaml";
import {
type AnyResource,
AnyResourceSchema,
ResourceMap,
} from "./playbill-schema";
export async function loadResourceFile(
filePath: string,
): Promise<AnyResource[]> {
try {
const file = Bun.file(filePath);
const text = await file.text();
const parsedDocs = YAML.parseAllDocuments(text);
const collection: AnyResource[] = [];
for (const doc of parsedDocs) {
if (doc.errors.length > 0) {
throw new Error(`Error parsing ${filePath}: ${doc.errors}`);
}
const raw = doc.toJS();
const parsed = AnyResourceSchema.parse(raw);
const type = parsed.$define;
const schemaToUse = ResourceMap[type];
collection.push(schemaToUse.parse(parsed));
}
return collection;
} catch (error) {
throw new Error(`Error loading ${filePath}: ${error}`);
}
}