feat: implement stat system and StatsView
Introduces a comprehensive stat management system including: - `/stat` command support (set, del, roll) - `StatsView` component for displaying grouped stat tables - Formula evaluation for derived stats (supporting arithmetic and functions) - Stat definition parsing from YAML blocks - Permission checking for stat modification based on roles
This commit is contained in:
@@ -38,10 +38,24 @@ export interface SparkTableCompletion {
|
||||
headers: string[];
|
||||
}
|
||||
|
||||
/** A single stat definition parsed from a ```stat YAML block */
|
||||
export interface StatDef {
|
||||
key: string;
|
||||
label: string;
|
||||
type: "number" | "string" | "enum" | "modifier" | "derived";
|
||||
default?: string;
|
||||
target?: string;
|
||||
options?: string[];
|
||||
roll?: string;
|
||||
formula?: string;
|
||||
source: string;
|
||||
}
|
||||
|
||||
export interface JournalCompletions {
|
||||
dice: DiceCompletion[];
|
||||
links: LinkCompletion[];
|
||||
sparkTables: SparkTableCompletion[];
|
||||
stats: StatDef[];
|
||||
}
|
||||
|
||||
export type CompletionsState =
|
||||
@@ -67,6 +81,7 @@ async function tryServer(): Promise<JournalCompletions | null> {
|
||||
dice: Array.isArray(data.dice) ? data.dice : [],
|
||||
links: Array.isArray(data.links) ? data.links : [],
|
||||
sparkTables: Array.isArray(data.sparkTables) ? data.sparkTables : [],
|
||||
stats: Array.isArray(data.stats) ? data.stats : [],
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
@@ -80,7 +95,9 @@ async function scanClientSide(): Promise<JournalCompletions> {
|
||||
const dice: DiceCompletion[] = [];
|
||||
const links: LinkCompletion[] = [];
|
||||
const sparkTables: SparkTableCompletion[] = [];
|
||||
const stats: StatDef[] = [];
|
||||
const tagRegex = /<md-dice[^>]*>\s*([\s\S]*?)\s*<\/md-dice>/gi;
|
||||
const statBlockRegex = /```yaml\s+role=stat\s*\n([\s\S]*?)```/gi;
|
||||
|
||||
for (const filePath of paths) {
|
||||
const content = await getIndexedData(filePath);
|
||||
@@ -145,9 +162,18 @@ async function scanClientSide(): Promise<JournalCompletions> {
|
||||
|
||||
i = j - 1;
|
||||
}
|
||||
|
||||
// Stat block scan
|
||||
statBlockRegex.lastIndex = 0;
|
||||
let statMatch: RegExpExecArray | null;
|
||||
while ((statMatch = statBlockRegex.exec(content)) !== null) {
|
||||
const yaml = statMatch[1];
|
||||
const parsed = parseStatYaml(yaml, filePath);
|
||||
stats.push(...parsed);
|
||||
}
|
||||
}
|
||||
|
||||
return { dice, links, sparkTables };
|
||||
return { dice, links, sparkTables, stats };
|
||||
}
|
||||
|
||||
function splitTableRow(line: string): string[] | null {
|
||||
@@ -159,6 +185,102 @@ function splitTableRow(line: string): string[] | null {
|
||||
return inner.split("|").map((c) => c.trim());
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stat YAML parser
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Parse a ```yaml role=stat block into StatDef entries.
|
||||
* Handles a minimal YAML subset: list of objects with key/value pairs.
|
||||
*/
|
||||
function parseStatYaml(yaml: string, source: string): StatDef[] {
|
||||
const defs: StatDef[] = [];
|
||||
const lines = yaml.split(/\r?\n/);
|
||||
|
||||
let current: Record<string, string> | null = null;
|
||||
let collectingOptions = false;
|
||||
let options: string[] = [];
|
||||
|
||||
function flushCurrent() {
|
||||
if (!current || !current.key) return;
|
||||
const type = (current.type || "number") as StatDef["type"];
|
||||
defs.push({
|
||||
key: current.key,
|
||||
label: current.label || current.key,
|
||||
type,
|
||||
default: current.default,
|
||||
target: current.target,
|
||||
options:
|
||||
type === "enum" && options.length > 0
|
||||
? [...options]
|
||||
: current.options
|
||||
? current.options
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter(Boolean)
|
||||
: undefined,
|
||||
roll: current.roll,
|
||||
formula: current.formula,
|
||||
source,
|
||||
});
|
||||
current = null;
|
||||
options = [];
|
||||
collectingOptions = false;
|
||||
}
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
|
||||
// Skip empty lines and comments
|
||||
if (!trimmed || trimmed.startsWith("#")) continue;
|
||||
|
||||
// New entry: "- key: value"
|
||||
if (trimmed.startsWith("- ")) {
|
||||
flushCurrent();
|
||||
current = {};
|
||||
collectingOptions = false;
|
||||
const rest = trimmed.slice(2);
|
||||
const colonIdx = rest.indexOf(":");
|
||||
if (colonIdx === -1) continue;
|
||||
const k = rest.slice(0, colonIdx).trim();
|
||||
const v = rest.slice(colonIdx + 1).trim();
|
||||
current[k] = v;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Continuation of current entry: " key: value"
|
||||
if (current && trimmed.match(/^\w/)) {
|
||||
const colonIdx = trimmed.indexOf(":");
|
||||
if (colonIdx === -1) continue;
|
||||
const k = trimmed.slice(0, colonIdx).trim();
|
||||
const v = trimmed.slice(colonIdx + 1).trim();
|
||||
|
||||
if (k === "options") {
|
||||
// Inline options: options: [a, b, c]
|
||||
if (v.startsWith("[") && v.endsWith("]")) {
|
||||
current[k] = v.slice(1, -1);
|
||||
} else {
|
||||
// Multi-line options list
|
||||
collectingOptions = true;
|
||||
options = [];
|
||||
}
|
||||
} else {
|
||||
current[k] = v;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Options list item: " - value"
|
||||
if (collectingOptions && trimmed.startsWith("- ")) {
|
||||
options.push(trimmed.slice(2).trim());
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
flushCurrent();
|
||||
return defs;
|
||||
}
|
||||
|
||||
// ------------------- Init (runs eagerly at import time) -------------------
|
||||
|
||||
// Using a top-level IIFE so the promise starts immediately
|
||||
@@ -216,5 +338,8 @@ export function useJournalCompletions(): {
|
||||
if (s.status === "loaded") {
|
||||
return { state: s, data: s.data };
|
||||
}
|
||||
return { state: s, data: { dice: [], links: [], sparkTables: [] } };
|
||||
return {
|
||||
state: s,
|
||||
data: { dice: [], links: [], sparkTables: [], stats: [] },
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user