Initial commit
This commit is contained in:
commit
4e31f18045
15 changed files with 779 additions and 0 deletions
19
src/binding.ts
Normal file
19
src/binding.ts
Normal 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
34
src/index.ts
Normal 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
52
src/lang.ts
Normal 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
264
src/playbill-schema.ts
Normal 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
36
src/resource.ts
Normal 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}`);
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue