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
|
## Usage
|
||||||
bun install
|
|
||||||
```
|
|
||||||
|
|
||||||
To run:
|
todo
|
||||||
|
|
||||||
```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.
|
|
||||||
|
|
3
bun.lock
3
bun.lock
|
@ -4,6 +4,7 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "lang",
|
"name": "lang",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"chalk": "^5.4.1",
|
||||||
"yaml": "^2.7.0",
|
"yaml": "^2.7.0",
|
||||||
"zod": "^3.24.1",
|
"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=="],
|
"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=="],
|
"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=="],
|
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
||||||
|
|
11
package.json
11
package.json
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "lang",
|
"name": "@endeavorance/muse",
|
||||||
|
"version": "0.0.1",
|
||||||
"module": "index.ts",
|
"module": "index.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -10,7 +11,13 @@
|
||||||
"typescript": "^5.0.0"
|
"typescript": "^5.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"chalk": "^5.4.1",
|
||||||
"yaml": "^2.7.0",
|
"yaml": "^2.7.0",
|
||||||
"zod": "^3.24.1"
|
"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
|
id: the-great-spires
|
||||||
name: 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 YAML from "yaml";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
const BindingSchema = z.object({
|
const BindingSchema = z.object({
|
||||||
|
$binding: z.literal("playbill"),
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
playbill: z.string().default("Unnamed playbill"),
|
name: z.string().default("Unnamed playbill"),
|
||||||
author: z.string().optional(),
|
author: z.string().optional(),
|
||||||
version: z.number(),
|
version: z.string(),
|
||||||
files: z.array(z.string()).default(["**/*.yaml"]),
|
files: z.array(z.string()).default(["**/*.yaml"]),
|
||||||
});
|
});
|
||||||
|
|
||||||
type Binding = z.infer<typeof BindingSchema>;
|
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 file = Bun.file(filePath);
|
||||||
const text = await file.text();
|
const text = await file.text();
|
||||||
const parsed = YAML.parse(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 { loadBindingFile } from "./binding";
|
||||||
import type { AnyResource } from "./playbill-schema";
|
import {
|
||||||
|
type AnyResource,
|
||||||
|
ValidatedPlaybillSchema,
|
||||||
|
getEmptyPlaybill,
|
||||||
|
} from "./playbill-schema";
|
||||||
import { loadResourceFile } from "./resource";
|
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 binding = await loadBindingFile(bindingPath);
|
||||||
|
|
||||||
const fileGlobs = binding.files;
|
verboseLog(
|
||||||
const bindingFileDirname = bindingPath.split("/").slice(0, -1).join("/");
|
`↳ Binding loaded with ${binding.files.length} associated resources`,
|
||||||
|
);
|
||||||
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[] = [];
|
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);
|
const loaded = await loadResourceFile(filepath);
|
||||||
|
verboseLog(` ↳ ${filepath} loaded with ${loaded.length} resources`);
|
||||||
loadedResources.push(...loaded);
|
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") => {
|
dmg: (amount = "1", type = "physical") => {
|
||||||
return `<em class='dmg ${type}'>${amount}</em>`;
|
return `<em class='dmg ${type}'>${amount}</em>`;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type DirectiveHandler = (...args: string[]) => string;
|
function processCommands(text: string): string {
|
||||||
|
|
||||||
function processCommands(
|
|
||||||
text: string,
|
|
||||||
handlers: { [key: string]: DirectiveHandler },
|
|
||||||
): string {
|
|
||||||
const commandPattern = /\{(\w+):([^}]+)\}/g;
|
const commandPattern = /\{(\w+):([^}]+)\}/g;
|
||||||
|
|
||||||
return text.replace(commandPattern, (match, command, args) => {
|
return text.replace(commandPattern, (_match, command, args) => {
|
||||||
const handler = handlers[command];
|
// TODO: Improve error reporting
|
||||||
if (handler) {
|
const directive = DirectiveSchema.parse(command);
|
||||||
const parsedArgs = [];
|
const handler = ProsceniumLangDirectives[directive];
|
||||||
let currentArg = "";
|
const parsedArgs = [];
|
||||||
let inQuotes = false;
|
let currentArg = "";
|
||||||
|
let inQuotes = false;
|
||||||
|
|
||||||
for (let i = 0; i < args.length; i++) {
|
for (let i = 0; i < args.length; i++) {
|
||||||
const char = args[i];
|
const char = args[i];
|
||||||
if (char === '"' && (i === 0 || args[i - 1] !== "\\")) {
|
if (char === '"' && (i === 0 || args[i - 1] !== "\\")) {
|
||||||
inQuotes = !inQuotes;
|
inQuotes = !inQuotes;
|
||||||
} else if (char === "," && !inQuotes) {
|
} else if (char === "," && !inQuotes) {
|
||||||
parsedArgs.push(currentArg.trim().replace(/^"|"$/g, ""));
|
parsedArgs.push(currentArg.trim().replace(/^"|"$/g, ""));
|
||||||
currentArg = "";
|
currentArg = "";
|
||||||
} else {
|
} else {
|
||||||
currentArg += char;
|
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 lines = input.split("\n");
|
||||||
const processedLines: string[] = [];
|
const processedLines: string[] = [];
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const processed = processCommands(line, directiveHandlers);
|
const processed = processCommands(line);
|
||||||
processedLines.push(processed);
|
processedLines.push(processed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,7 @@ import { type ZodSchema, z } from "zod";
|
||||||
* Ability N/A
|
* Ability N/A
|
||||||
* Tag N/A
|
* Tag N/A
|
||||||
* Lore N/A
|
* Lore N/A
|
||||||
|
* Rule N/A
|
||||||
* Method Ability
|
* Method Ability
|
||||||
* Item Tag
|
* Item Tag
|
||||||
* Species Ability
|
* Species Ability
|
||||||
|
@ -37,9 +38,7 @@ export const StatsSchema = z.enum(STAT_ABBREVIATIONS);
|
||||||
export const ItemTypeSchema = z.enum(ITEM_TYPES);
|
export const ItemTypeSchema = z.enum(ITEM_TYPES);
|
||||||
export const RollTypeSchema = z.enum(ROLL_TYPES);
|
export const RollTypeSchema = z.enum(ROLL_TYPES);
|
||||||
export const AbilityTypeSchema = z.enum(ABILITY_TYPES);
|
export const AbilityTypeSchema = z.enum(ABILITY_TYPES);
|
||||||
|
|
||||||
export const TalentSchema = z.enum(TALENTS);
|
export const TalentSchema = z.enum(TALENTS);
|
||||||
|
|
||||||
export const TalentProwessSchema = z.enum(TALENT_PROWESS);
|
export const TalentProwessSchema = z.enum(TALENT_PROWESS);
|
||||||
|
|
||||||
const ResourceTypes = [
|
const ResourceTypes = [
|
||||||
|
@ -50,6 +49,7 @@ const ResourceTypes = [
|
||||||
"tag",
|
"tag",
|
||||||
"lore",
|
"lore",
|
||||||
"method",
|
"method",
|
||||||
|
"rule",
|
||||||
"playbill",
|
"playbill",
|
||||||
] as const;
|
] as const;
|
||||||
const ResourceTypeSchema = z.enum(ResourceTypes);
|
const ResourceTypeSchema = z.enum(ResourceTypes);
|
||||||
|
@ -136,6 +136,11 @@ export const SpeciesSchema = PlaybillResourceSchema.extend({
|
||||||
talentMinimums: TalentSpreadSchema.default({}),
|
talentMinimums: TalentSpreadSchema.default({}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const RuleSchema = PlaybillResourceSchema.extend({
|
||||||
|
$define: z.literal("rule"),
|
||||||
|
overrule: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
// Full Playbill Schema
|
// Full Playbill Schema
|
||||||
export const PlaybillSchema = PlaybillResourceSchema.extend({
|
export const PlaybillSchema = PlaybillResourceSchema.extend({
|
||||||
$define: z.literal("playbill"),
|
$define: z.literal("playbill"),
|
||||||
|
@ -150,12 +155,35 @@ export const PlaybillSchema = PlaybillResourceSchema.extend({
|
||||||
lore: z.array(LoreSchema).default([]),
|
lore: z.array(LoreSchema).default([]),
|
||||||
methods: z.array(MethodSchema).default([]),
|
methods: z.array(MethodSchema).default([]),
|
||||||
species: z.array(SpeciesSchema).default([]),
|
species: z.array(SpeciesSchema).default([]),
|
||||||
|
rules: z.array(RuleSchema).default([]),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const ValidatedPlaybillSchema = PlaybillSchema.superRefine(
|
export const ValidatedPlaybillSchema = PlaybillSchema.superRefine(
|
||||||
(val, ctx) => {
|
(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
|
// For each method, ensure its abilities exist
|
||||||
|
// Also ensure unique IDs
|
||||||
|
const methodIds = new Set<string>();
|
||||||
for (const method of val.methods) {
|
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();
|
const methodAbilityIds = method.abilities.flat();
|
||||||
|
|
||||||
for (const abilityId of methodAbilityIds) {
|
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) {
|
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();
|
const speciesAbilityIds = species.abilities.flat();
|
||||||
|
|
||||||
for (const abilityId of speciesAbilityIds) {
|
for (const abilityId of speciesAbilityIds) {
|
||||||
|
@ -183,7 +221,16 @@ export const ValidatedPlaybillSchema = PlaybillSchema.superRefine(
|
||||||
}
|
}
|
||||||
|
|
||||||
// For each entity, ensure its species and known abilities exist
|
// For each entity, ensure its species and known abilities exist
|
||||||
|
const entityIds = new Set<string>();
|
||||||
for (const entity of val.entities) {
|
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) {
|
for (const abilityId of entity.abilities) {
|
||||||
if (!val.abilities.some((a) => a.id === abilityId)) {
|
if (!val.abilities.some((a) => a.id === abilityId)) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
|
@ -202,7 +249,15 @@ export const ValidatedPlaybillSchema = PlaybillSchema.superRefine(
|
||||||
}
|
}
|
||||||
|
|
||||||
// For each item, ensure its tags exist
|
// For each item, ensure its tags exist
|
||||||
|
const itemIds = new Set<string>();
|
||||||
for (const item of val.items) {
|
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) {
|
for (const tag of item.tags) {
|
||||||
if (!val.tags.some((t) => t.id === tag)) {
|
if (!val.tags.some((t) => t.id === tag)) {
|
||||||
ctx.addIssue({
|
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 Playbill = z.infer<typeof PlaybillSchema>;
|
||||||
export type ValidatedPlaybill = z.infer<typeof ValidatedPlaybillSchema>;
|
export type ValidatedPlaybill = z.infer<typeof ValidatedPlaybillSchema>;
|
||||||
|
@ -226,6 +317,7 @@ export const ResourceMap: Record<ResourceType, ZodSchema> = {
|
||||||
tag: ItemTagSchema,
|
tag: ItemTagSchema,
|
||||||
method: MethodSchema,
|
method: MethodSchema,
|
||||||
lore: LoreSchema,
|
lore: LoreSchema,
|
||||||
|
rule: RuleSchema,
|
||||||
playbill: PlaybillSchema,
|
playbill: PlaybillSchema,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -237,6 +329,7 @@ export const AnyResourceSchema = z.discriminatedUnion("$define", [
|
||||||
ItemTagSchema,
|
ItemTagSchema,
|
||||||
MethodSchema,
|
MethodSchema,
|
||||||
LoreSchema,
|
LoreSchema,
|
||||||
|
RuleSchema,
|
||||||
PlaybillSchema,
|
PlaybillSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
@ -246,19 +339,5 @@ export function getEmptyPlaybill(): Playbill {
|
||||||
return PlaybillSchema.parse({
|
return PlaybillSchema.parse({
|
||||||
$define: "playbill",
|
$define: "playbill",
|
||||||
id: "empty",
|
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,
|
ResourceMap,
|
||||||
} from "./playbill-schema";
|
} from "./playbill-schema";
|
||||||
|
|
||||||
|
export class ResourceFileError extends Error { }
|
||||||
|
export class NullResourceError extends Error { }
|
||||||
|
|
||||||
export async function loadResourceFile(
|
export async function loadResourceFile(
|
||||||
filePath: string,
|
filePath: string,
|
||||||
): Promise<AnyResource[]> {
|
): Promise<AnyResource[]> {
|
||||||
try {
|
const file = Bun.file(filePath);
|
||||||
const file = Bun.file(filePath);
|
const text = await file.text();
|
||||||
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) {
|
const errors = parsedDocs.flatMap((doc) => doc.errors);
|
||||||
if (doc.errors.length > 0) {
|
|
||||||
throw new Error(`Error parsing ${filePath}: ${doc.errors}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const raw = doc.toJS();
|
if (errors.length > 0) {
|
||||||
const parsed = AnyResourceSchema.parse(raw);
|
throw new ResourceFileError(`Error parsing ${filePath}: ${errors}`);
|
||||||
const type = parsed.$define;
|
}
|
||||||
const schemaToUse = ResourceMap[type];
|
|
||||||
|
|
||||||
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;
|
const raw = doc.toJS();
|
||||||
} catch (error) {
|
const parsed = AnyResourceSchema.parse(raw);
|
||||||
throw new Error(`Error loading ${filePath}: ${error}`);
|
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