Add more flags and verbose logging
This commit is contained in:
parent
4e31f18045
commit
542d28cb53
13 changed files with 406 additions and 135 deletions
16
README.md
16
README.md
|
@ -1,15 +1,7 @@
|
|||
# lang
|
||||
# Playbill Builder CLI
|
||||
|
||||
To install dependencies:
|
||||
This is a CLI tool for compiling YAML files into a validated Playbill for [Proscenium](https://proscenium.game)
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
## Usage
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.2.2. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
|
||||
todo
|
||||
|
|
3
bun.lock
3
bun.lock
|
@ -4,6 +4,7 @@
|
|||
"": {
|
||||
"name": "lang",
|
||||
"dependencies": {
|
||||
"chalk": "^5.4.1",
|
||||
"yaml": "^2.7.0",
|
||||
"zod": "^3.24.1",
|
||||
},
|
||||
|
@ -43,6 +44,8 @@
|
|||
|
||||
"bun-types": ["bun-types@1.2.2", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-RCbMH5elr9gjgDGDhkTTugA21XtJAy/9jkKe/G3WR2q17VPGhcquf9Sir6uay9iW+7P/BV0CAHA1XlHXMAVKHg=="],
|
||||
|
||||
"chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="],
|
||||
|
||||
"typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
|
||||
|
||||
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"name": "lang",
|
||||
"name": "@endeavorance/muse",
|
||||
"version": "0.0.1",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
|
@ -10,7 +11,13 @@
|
|||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "^5.4.1",
|
||||
"yaml": "^2.7.0",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"scripts": {
|
||||
"fmt": "bunx --bun biome check --fix",
|
||||
"build": "bun build ./src/index.ts --outfile muse --compile",
|
||||
"demo": "bun run ./src/index.ts -- ./playbill/binding.yaml"
|
||||
}
|
||||
}
|
|
@ -1,3 +1,7 @@
|
|||
$binding: playbill
|
||||
id: the-great-spires
|
||||
name: The Great Spires
|
||||
version: "1"
|
||||
author: "Endeavorance <hello@endeavorance.camp>"
|
||||
version: 0.0.1
|
||||
files:
|
||||
- ./spires/**/*.yaml
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
$define: ability
|
||||
id: identify-poison
|
||||
name: Identify Poison
|
||||
type: action
|
||||
costs:
|
||||
ap: 1
|
||||
roll: focus
|
||||
boons:
|
||||
- You know the antidote to the detected poison
|
||||
- Your detection is imperceptible
|
||||
banes:
|
||||
- It is obvious you are checking for poison
|
||||
description: |-
|
||||
Focus your senses on a food or drink to determine if it is poisoned.
|
||||
|
||||
{dmg:1,magic}
|
||||
You can also identify the type of poison used.
|
||||
---
|
||||
$define: ability
|
||||
id: neutralize-poison
|
||||
name: Neutralize Poison
|
||||
type: action
|
||||
roll: cunning
|
||||
description: Neutralize a poison
|
|
@ -1,6 +0,0 @@
|
|||
$define: method
|
||||
id: gourmond
|
||||
name: Gourmond
|
||||
curator: Lump
|
||||
abilities: [["identify-poison"]]
|
||||
description: Your prowess in cooking is second only to your prowess in eating.
|
38
playbill/spires/methods/gourmond.yaml
Normal file
38
playbill/spires/methods/gourmond.yaml
Normal file
|
@ -0,0 +1,38 @@
|
|||
$define: method
|
||||
id: gourmond
|
||||
name: Gourmond
|
||||
curator: TBD
|
||||
abilities:
|
||||
- [identify-poison, neutralize-poison]
|
||||
description: Your prowess in cooking is second only to your prowess in eating.
|
||||
---
|
||||
$define: ability
|
||||
id: identify-poison
|
||||
name: Identify Poison
|
||||
type: action
|
||||
costs:
|
||||
ap: 1
|
||||
roll: focus
|
||||
boons:
|
||||
- You know the antidote to the detected poison
|
||||
- Your detection is imperceptible
|
||||
banes:
|
||||
- It is obvious you are checking for poison
|
||||
description: |-
|
||||
Focus your senses on a food or drink to determine if it is poisoned.
|
||||
|
||||
{dmg:1,magic}
|
||||
You can also identify the type of poison used.
|
||||
---
|
||||
$define: ability
|
||||
id: neutralize-poison
|
||||
name: Neutralize Poison
|
||||
type: action
|
||||
roll: cunning
|
||||
description: Neutralize a poison by adding an ingredient known to render the poison inert
|
||||
boons:
|
||||
- You manage to neutralize the poison without drawing any attention
|
||||
- You are able to also neutralize poisons in the food of your party members
|
||||
banes:
|
||||
- You are not able to fully neutralize the poison, only weakening the effects
|
||||
- Someone catches on to you
|
|
@ -1,19 +1,59 @@
|
|||
import path from "node:path";
|
||||
import { Glob } from "bun";
|
||||
import YAML from "yaml";
|
||||
import z from "zod";
|
||||
|
||||
const BindingSchema = z.object({
|
||||
$binding: z.literal("playbill"),
|
||||
id: z.string(),
|
||||
playbill: z.string().default("Unnamed playbill"),
|
||||
name: z.string().default("Unnamed playbill"),
|
||||
author: z.string().optional(),
|
||||
version: z.number(),
|
||||
version: z.string(),
|
||||
files: z.array(z.string()).default(["**/*.yaml"]),
|
||||
});
|
||||
|
||||
type Binding = z.infer<typeof BindingSchema>;
|
||||
|
||||
export async function loadBindingFile(filePath: string): Promise<Binding> {
|
||||
export interface PlaybillBinding {
|
||||
info: Binding;
|
||||
bindingPath: string;
|
||||
files: string[];
|
||||
}
|
||||
|
||||
export async function loadBindingFile(
|
||||
filePath: string,
|
||||
): Promise<PlaybillBinding> {
|
||||
const file = Bun.file(filePath);
|
||||
const text = await file.text();
|
||||
const parsed = YAML.parse(text);
|
||||
return BindingSchema.parse(parsed);
|
||||
const binding = BindingSchema.parse(parsed);
|
||||
|
||||
const fileGlobs = binding.files;
|
||||
const bindingFileDirname = filePath.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 bindingFileAbsolutePath = path.resolve(filePath);
|
||||
const filteredFilePaths = allFilePaths.filter((filePath) => {
|
||||
return filePath !== bindingFileAbsolutePath;
|
||||
});
|
||||
|
||||
return {
|
||||
info: binding,
|
||||
bindingPath: filePath,
|
||||
files: filteredFilePaths,
|
||||
};
|
||||
}
|
||||
|
|
174
src/index.ts
174
src/index.ts
|
@ -1,34 +1,164 @@
|
|||
import { Glob } from "bun";
|
||||
import chalk from "chalk";
|
||||
import { parseArgs } from "node:util";
|
||||
import { loadBindingFile } from "./binding";
|
||||
import type { AnyResource } from "./playbill-schema";
|
||||
import {
|
||||
type AnyResource,
|
||||
ValidatedPlaybillSchema,
|
||||
getEmptyPlaybill,
|
||||
} from "./playbill-schema";
|
||||
import { loadResourceFile } from "./resource";
|
||||
import { version } from "../package.json" with { type: "json" };
|
||||
|
||||
const { values: options, positionals: args } = parseArgs({
|
||||
args: Bun.argv.slice(2),
|
||||
options: {
|
||||
write: {
|
||||
short: "w",
|
||||
type: "boolean",
|
||||
default: false,
|
||||
},
|
||||
check: {
|
||||
short: "c",
|
||||
type: "boolean",
|
||||
default: false,
|
||||
},
|
||||
outfile: {
|
||||
short: "o",
|
||||
type: "string",
|
||||
default: "playbill.json",
|
||||
},
|
||||
help: {
|
||||
short: "h",
|
||||
default: false,
|
||||
type: "boolean",
|
||||
},
|
||||
verbose: {
|
||||
short: "v",
|
||||
default: false,
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
strict: true,
|
||||
allowPositionals: true,
|
||||
});
|
||||
|
||||
if (args.length === 0 || options.help) {
|
||||
console.log(`${chalk.bold("muse")} - Compile and validate Playbills for Proscenium
|
||||
${chalk.dim(`v${version}`)}
|
||||
|
||||
Usage:
|
||||
muse [/path/to/binding.yaml] <options>
|
||||
|
||||
Options:
|
||||
--check Only load and check the current binding and resources, but do not compile
|
||||
--write Write the output to a file. If not specified, outputs to stdout
|
||||
--outfile Specify the output file path [default: playbill.json]
|
||||
--verbose, -v Verbose output
|
||||
--help, -h Show this help message
|
||||
`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (options.check && options.write) {
|
||||
console.error("Cannot use --check and --write together");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const VERBOSE = options.verbose;
|
||||
|
||||
function verboseLog(msg: string) {
|
||||
if (VERBOSE) {
|
||||
console.log(chalk.dim(msg));
|
||||
}
|
||||
}
|
||||
|
||||
const bindingPath = args[0] ?? "./binding.yaml";
|
||||
|
||||
verboseLog(`Using binding file: ${bindingPath}`);
|
||||
|
||||
// Check if the binding file exists
|
||||
const bindingFileCheck = Bun.file(bindingPath);
|
||||
const bindingFileExists = await bindingFileCheck.exists();
|
||||
|
||||
if (!bindingFileExists) {
|
||||
console.error(`Binding file not found: ${bindingPath}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
verboseLog("↳ Binding file found");
|
||||
|
||||
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);
|
||||
}
|
||||
verboseLog(
|
||||
`↳ Binding loaded with ${binding.files.length} associated resources`,
|
||||
);
|
||||
|
||||
const loadedResources: AnyResource[] = [];
|
||||
|
||||
for (const filepath of allFilePaths) {
|
||||
verboseLog("Loading resources");
|
||||
|
||||
// Load resources listed in the binding file
|
||||
for (const filepath of binding.files) {
|
||||
verboseLog(`↳ Loading ${filepath}...`);
|
||||
const loaded = await loadResourceFile(filepath);
|
||||
verboseLog(` ↳ ${filepath} loaded with ${loaded.length} resources`);
|
||||
loadedResources.push(...loaded);
|
||||
}
|
||||
|
||||
console.log(loadedResources);
|
||||
verboseLog("Constructing playbill");
|
||||
|
||||
// Consjtruct the playbill object
|
||||
const playbill = getEmptyPlaybill();
|
||||
|
||||
playbill.id = binding.info.id;
|
||||
playbill.name = binding.info.name;
|
||||
playbill.author = binding.info.author ?? "Anonymous";
|
||||
playbill.version = binding.info.version;
|
||||
|
||||
for (const resource of loadedResources) {
|
||||
switch (resource.$define) {
|
||||
case "ability":
|
||||
playbill.abilities.push(resource);
|
||||
break;
|
||||
case "species":
|
||||
playbill.species.push(resource);
|
||||
break;
|
||||
case "entity":
|
||||
playbill.entities.push(resource);
|
||||
break;
|
||||
case "item":
|
||||
playbill.items.push(resource);
|
||||
break;
|
||||
case "tag":
|
||||
playbill.tags.push(resource);
|
||||
break;
|
||||
case "method":
|
||||
playbill.methods.push(resource);
|
||||
break;
|
||||
case "lore":
|
||||
playbill.lore.push(resource);
|
||||
break;
|
||||
case "rule":
|
||||
playbill.rules.push(resource);
|
||||
break;
|
||||
case "playbill":
|
||||
throw new Error("Cannot load playbills rn dawg");
|
||||
}
|
||||
}
|
||||
|
||||
verboseLog("Validating playbill");
|
||||
|
||||
// Validate the playbill object
|
||||
const validatedPlaybill = ValidatedPlaybillSchema.parse(playbill);
|
||||
|
||||
verboseLog("Playbill validated");
|
||||
|
||||
if (options.write) {
|
||||
await Bun.write(options.outfile, JSON.stringify(validatedPlaybill, null, 2));
|
||||
} else if (!options.check) {
|
||||
console.log(validatedPlaybill);
|
||||
}
|
||||
|
||||
if (options.check) {
|
||||
console.log(chalk.green("Playbill validated successfully"));
|
||||
}
|
||||
|
|
57
src/lang.ts
57
src/lang.ts
|
@ -1,42 +1,41 @@
|
|||
import type { ZodSchema } from "zod";
|
||||
import { z } from "zod";
|
||||
|
||||
const directiveHandlers = {
|
||||
const ALL_DIRECTIVES = ["dmg"] as const;
|
||||
const DirectiveSchema = z.enum(ALL_DIRECTIVES);
|
||||
type DirectiveName = (typeof ALL_DIRECTIVES)[number];
|
||||
type DirectiveHandler = (...args: string[]) => string;
|
||||
|
||||
const ProsceniumLangDirectives: Record<DirectiveName, DirectiveHandler> = {
|
||||
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 {
|
||||
function processCommands(text: string): 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;
|
||||
return text.replace(commandPattern, (_match, command, args) => {
|
||||
// TODO: Improve error reporting
|
||||
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;
|
||||
}
|
||||
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;
|
||||
parsedArgs.push(currentArg.trim().replace(/^"|"$/g, ""));
|
||||
|
||||
return handler(...parsedArgs);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -44,7 +43,7 @@ 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);
|
||||
const processed = processCommands(line);
|
||||
processedLines.push(processed);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ import { type ZodSchema, z } from "zod";
|
|||
* Ability N/A
|
||||
* Tag N/A
|
||||
* Lore N/A
|
||||
* Rule N/A
|
||||
* Method Ability
|
||||
* Item Tag
|
||||
* Species Ability
|
||||
|
@ -37,9 +38,7 @@ 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 = [
|
||||
|
@ -50,6 +49,7 @@ const ResourceTypes = [
|
|||
"tag",
|
||||
"lore",
|
||||
"method",
|
||||
"rule",
|
||||
"playbill",
|
||||
] as const;
|
||||
const ResourceTypeSchema = z.enum(ResourceTypes);
|
||||
|
@ -136,6 +136,11 @@ export const SpeciesSchema = PlaybillResourceSchema.extend({
|
|||
talentMinimums: TalentSpreadSchema.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"),
|
||||
|
@ -150,12 +155,35 @@ export const PlaybillSchema = PlaybillResourceSchema.extend({
|
|||
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) {
|
||||
|
@ -168,8 +196,18 @@ export const ValidatedPlaybillSchema = PlaybillSchema.superRefine(
|
|||
}
|
||||
}
|
||||
|
||||
// For each species, ensure its abilities
|
||||
// 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) {
|
||||
|
@ -183,7 +221,16 @@ export const ValidatedPlaybillSchema = PlaybillSchema.superRefine(
|
|||
}
|
||||
|
||||
// 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({
|
||||
|
@ -202,7 +249,15 @@ export const ValidatedPlaybillSchema = PlaybillSchema.superRefine(
|
|||
}
|
||||
|
||||
// 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({
|
||||
|
@ -212,8 +267,44 @@ export const ValidatedPlaybillSchema = PlaybillSchema.superRefine(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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>;
|
||||
|
@ -226,6 +317,7 @@ export const ResourceMap: Record<ResourceType, ZodSchema> = {
|
|||
tag: ItemTagSchema,
|
||||
method: MethodSchema,
|
||||
lore: LoreSchema,
|
||||
rule: RuleSchema,
|
||||
playbill: PlaybillSchema,
|
||||
};
|
||||
|
||||
|
@ -237,6 +329,7 @@ export const AnyResourceSchema = z.discriminatedUnion("$define", [
|
|||
ItemTagSchema,
|
||||
MethodSchema,
|
||||
LoreSchema,
|
||||
RuleSchema,
|
||||
PlaybillSchema,
|
||||
]);
|
||||
|
||||
|
@ -246,19 +339,5 @@ 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",
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,32 +5,41 @@ import {
|
|||
ResourceMap,
|
||||
} from "./playbill-schema";
|
||||
|
||||
export class ResourceFileError extends Error { }
|
||||
export class NullResourceError extends Error { }
|
||||
|
||||
export async function loadResourceFile(
|
||||
filePath: string,
|
||||
): Promise<AnyResource[]> {
|
||||
try {
|
||||
const file = Bun.file(filePath);
|
||||
const text = await file.text();
|
||||
const file = Bun.file(filePath);
|
||||
const text = await file.text();
|
||||
|
||||
const parsedDocs = YAML.parseAllDocuments(text);
|
||||
const parsedDocs = YAML.parseAllDocuments(text);
|
||||
|
||||
const collection: AnyResource[] = [];
|
||||
if (parsedDocs.some((doc) => doc.toJS() === null)) {
|
||||
throw new NullResourceError(`Null resource defined in ${filePath}`);
|
||||
}
|
||||
|
||||
for (const doc of parsedDocs) {
|
||||
if (doc.errors.length > 0) {
|
||||
throw new Error(`Error parsing ${filePath}: ${doc.errors}`);
|
||||
}
|
||||
const errors = parsedDocs.flatMap((doc) => doc.errors);
|
||||
|
||||
const raw = doc.toJS();
|
||||
const parsed = AnyResourceSchema.parse(raw);
|
||||
const type = parsed.$define;
|
||||
const schemaToUse = ResourceMap[type];
|
||||
if (errors.length > 0) {
|
||||
throw new ResourceFileError(`Error parsing ${filePath}: ${errors}`);
|
||||
}
|
||||
|
||||
collection.push(schemaToUse.parse(parsed));
|
||||
const collection: AnyResource[] = [];
|
||||
|
||||
for (const doc of parsedDocs) {
|
||||
if (doc.errors.length > 0) {
|
||||
throw new ResourceFileError(`Error parsing ${filePath}: ${doc.errors}`);
|
||||
}
|
||||
|
||||
return collection;
|
||||
} catch (error) {
|
||||
throw new Error(`Error loading ${filePath}: ${error}`);
|
||||
const raw = doc.toJS();
|
||||
const parsed = AnyResourceSchema.parse(raw);
|
||||
const type = parsed.$define;
|
||||
const schemaToUse = ResourceMap[type];
|
||||
|
||||
collection.push(schemaToUse.parse(parsed));
|
||||
}
|
||||
|
||||
return collection;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue