Initial commit
This commit is contained in:
commit
4e31f18045
15 changed files with 779 additions and 0 deletions
175
.gitignore
vendored
Normal file
175
.gitignore
vendored
Normal file
|
@ -0,0 +1,175 @@
|
|||
# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
|
||||
|
||||
# Logs
|
||||
|
||||
logs
|
||||
_.log
|
||||
npm-debug.log_
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# Caches
|
||||
|
||||
.cache
|
||||
|
||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# Runtime data
|
||||
|
||||
pids
|
||||
_.pid
|
||||
_.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# nyc test coverage
|
||||
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# Snowpack dependency directory (https://snowpack.dev/)
|
||||
|
||||
web_modules/
|
||||
|
||||
# TypeScript cache
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
|
||||
.eslintcache
|
||||
|
||||
# Optional stylelint cache
|
||||
|
||||
.stylelintcache
|
||||
|
||||
# Microbundle cache
|
||||
|
||||
.rpt2_cache/
|
||||
.rts2_cache_cjs/
|
||||
.rts2_cache_es/
|
||||
.rts2_cache_umd/
|
||||
|
||||
# Optional REPL history
|
||||
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variable files
|
||||
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
|
||||
.parcel-cache
|
||||
|
||||
# Next.js build output
|
||||
|
||||
.next
|
||||
out
|
||||
|
||||
# Nuxt.js build / generate output
|
||||
|
||||
.nuxt
|
||||
dist
|
||||
|
||||
# Gatsby files
|
||||
|
||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||
|
||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||
|
||||
# public
|
||||
|
||||
# vuepress build output
|
||||
|
||||
.vuepress/dist
|
||||
|
||||
# vuepress v2.x temp and cache directory
|
||||
|
||||
.temp
|
||||
|
||||
# Docusaurus cache and generated files
|
||||
|
||||
.docusaurus
|
||||
|
||||
# Serverless directories
|
||||
|
||||
.serverless/
|
||||
|
||||
# FuseBox cache
|
||||
|
||||
.fusebox/
|
||||
|
||||
# DynamoDB Local files
|
||||
|
||||
.dynamodb/
|
||||
|
||||
# TernJS port file
|
||||
|
||||
.tern-port
|
||||
|
||||
# Stores VSCode versions used for testing VSCode extensions
|
||||
|
||||
.vscode-test
|
||||
|
||||
# yarn v2
|
||||
|
||||
.yarn/cache
|
||||
.yarn/unplugged
|
||||
.yarn/build-state.yml
|
||||
.yarn/install-state.gz
|
||||
.pnp.*
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
15
README.md
Normal file
15
README.md
Normal file
|
@ -0,0 +1,15 @@
|
|||
# lang
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
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.
|
48
biome.json
Normal file
48
biome.json
Normal file
|
@ -0,0 +1,48 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"vcs": {
|
||||
"enabled": false,
|
||||
"clientKind": "git",
|
||||
"useIgnoreFile": true
|
||||
},
|
||||
"files": {
|
||||
"ignoreUnknown": false,
|
||||
"ignore": [
|
||||
"dist",
|
||||
".next",
|
||||
"public",
|
||||
"*.d.ts",
|
||||
"*.json",
|
||||
"build",
|
||||
"*.svelte",
|
||||
".svelte-kit"
|
||||
]
|
||||
},
|
||||
"formatter": {
|
||||
"enabled": true,
|
||||
"indentStyle": "space",
|
||||
"indentWidth": 2
|
||||
},
|
||||
"organizeImports": {
|
||||
"enabled": true
|
||||
},
|
||||
"linter": {
|
||||
"enabled": true,
|
||||
"rules": {
|
||||
"recommended": true,
|
||||
"suspicious": {
|
||||
"noArrayIndexKey": "off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"javascript": {
|
||||
"formatter": {
|
||||
"quoteStyle": "double"
|
||||
}
|
||||
},
|
||||
"css": {
|
||||
"parser": {
|
||||
"cssModules": true
|
||||
}
|
||||
}
|
||||
}
|
54
bun.lock
Normal file
54
bun.lock
Normal file
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "lang",
|
||||
"dependencies": {
|
||||
"yaml": "^2.7.0",
|
||||
"zod": "^3.24.1",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="],
|
||||
|
||||
"@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="],
|
||||
|
||||
"@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="],
|
||||
|
||||
"@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="],
|
||||
|
||||
"@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="],
|
||||
|
||||
"@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="],
|
||||
|
||||
"@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"@types/node": ["@types/node@22.13.1", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew=="],
|
||||
|
||||
"@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=="],
|
||||
|
||||
"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=="],
|
||||
|
||||
"yaml": ["yaml@2.7.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA=="],
|
||||
|
||||
"zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="],
|
||||
}
|
||||
}
|
16
package.json
Normal file
16
package.json
Normal file
|
@ -0,0 +1,16 @@
|
|||
{
|
||||
"name": "lang",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"yaml": "^2.7.0",
|
||||
"zod": "^3.24.1"
|
||||
}
|
||||
}
|
3
playbill/binding.yaml
Normal file
3
playbill/binding.yaml
Normal file
|
@ -0,0 +1,3 @@
|
|||
id: the-great-spires
|
||||
name: The Great Spires
|
||||
version: "1"
|
24
playbill/methods/abilities/identify-poison.yaml
Normal file
24
playbill/methods/abilities/identify-poison.yaml
Normal file
|
@ -0,0 +1,24 @@
|
|||
$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
|
6
playbill/methods/gourmond.yaml
Normal file
6
playbill/methods/gourmond.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
$define: method
|
||||
id: gourmond
|
||||
name: Gourmond
|
||||
curator: Lump
|
||||
abilities: [["identify-poison"]]
|
||||
description: Your prowess in cooking is second only to your prowess in eating.
|
6
playbill/world.yaml
Normal file
6
playbill/world.yaml
Normal file
|
@ -0,0 +1,6 @@
|
|||
$define: lore
|
||||
id: world
|
||||
description: |-
|
||||
This is a lore file.
|
||||
|
||||
It has some information in it, and is mostly just to be read.
|
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}`);
|
||||
}
|
||||
}
|
27
tsconfig.json
Normal file
27
tsconfig.json
Normal file
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
// Enable latest features
|
||||
"lib": ["ESNext", "DOM"],
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue