feat: add stat-modifiers role for automatic stat generation
Introduces a new `stat-modifiers` CSV role that simplifies the creation of template stats and their associated modifier stats. A single CSV block now automatically generates: - A template stat definition. - Multiple modifier stat definitions (prefixed with the template ID). - The corresponding template table. This reduces the need for manual YAML definitions for every modifier associated with a template.
This commit is contained in:
@@ -12,6 +12,7 @@ import {
|
||||
parseStatYaml,
|
||||
parseStatCsv,
|
||||
parseTemplateCsv,
|
||||
parseStatModifiers,
|
||||
type StatDef,
|
||||
type StatTemplate,
|
||||
} from "./stat-parser.js";
|
||||
@@ -111,6 +112,14 @@ export function processBlocks(
|
||||
);
|
||||
}
|
||||
|
||||
if (attrs.role === "stat-modifiers") {
|
||||
const id = attrs.id || `_mod_${contentHash(body)}`;
|
||||
const result = parseStatModifiers(body, fileRelativePath, id);
|
||||
blocks.stats.push(result.statDef);
|
||||
blocks.stats.push(...result.modifierDefs);
|
||||
blocks.statTemplates.push(result.template);
|
||||
}
|
||||
|
||||
if (attrs.role === "spark-table") {
|
||||
const parsed = parseSparkTableCsv(body, fileRelativePath, slugger);
|
||||
if (parsed) blocks.sparkTables.push(parsed);
|
||||
|
||||
@@ -186,15 +186,15 @@ export function parseStatCsv(csv: string, source: string): StatDef[] {
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// Template CSV parser
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Parse a ```csv role=stat-template file=xxx block.
|
||||
* Parse a ```csv role=stat-template block.
|
||||
*
|
||||
* The first header is the dice notation (e.g. "1d10").
|
||||
* The second header is "label".
|
||||
* Remaining headers are modifier keys.
|
||||
* Remaining headers are modifier keys (bare names, no prefix).
|
||||
*/
|
||||
export function parseTemplateCsv(
|
||||
csv: string,
|
||||
@@ -207,10 +207,8 @@ export function parseTemplateCsv(
|
||||
const headers = lines[0].split(",").map((h) => h.trim().toLowerCase());
|
||||
if (headers.length < 2) return { name, notation: "", entries: [], source };
|
||||
|
||||
const notation = headers[0]; // e.g. "1d10"
|
||||
const notation = headers[0];
|
||||
const labelIdx = headers.indexOf("label");
|
||||
|
||||
// All columns after the notation & label are modifier keys
|
||||
const modKeys = headers.filter((h, i) => i !== 0 && i !== labelIdx);
|
||||
|
||||
const entries: TemplateEntry[] = [];
|
||||
@@ -241,6 +239,125 @@ export function parseTemplateCsv(
|
||||
return { name, notation, entries, source };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stat-modifiers parser
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Result of parsing a stat-modifiers block. */
|
||||
export interface StatModifiersResult {
|
||||
/** The template stat def (type: template) */
|
||||
statDef: StatDef;
|
||||
/** Modifier stat defs (type: modifier, one per value column) */
|
||||
modifierDefs: StatDef[];
|
||||
/** The template table with prefixed modifier keys */
|
||||
template: StatTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a ```csv role=stat-modifiers block.
|
||||
*
|
||||
* Auto-generates a template stat + modifier stat defs from a single CSV.
|
||||
*
|
||||
* The first header is the dice notation (e.g. "1d10").
|
||||
* The second header is "label".
|
||||
* Remaining headers are bare target names (e.g. "mind", "heart").
|
||||
*
|
||||
* Generated modifier keys: {id}_{column} (e.g. "age_mind").
|
||||
* Template entries internally map: column → {id}_{column}.
|
||||
*/
|
||||
export function parseStatModifiers(
|
||||
csv: string,
|
||||
source: string,
|
||||
id: string,
|
||||
scope: "player" | "global" = "player",
|
||||
): StatModifiersResult {
|
||||
const lines = csv.trim().split(/\r?\n/);
|
||||
const emptyTemplate: StatTemplate = {
|
||||
name: id,
|
||||
notation: "",
|
||||
entries: [],
|
||||
source,
|
||||
};
|
||||
|
||||
if (lines.length === 0) {
|
||||
return {
|
||||
statDef: { key: id, label: id, type: "template", scope, template: id, source },
|
||||
modifierDefs: [],
|
||||
template: emptyTemplate,
|
||||
};
|
||||
}
|
||||
|
||||
const headers = lines[0].split(",").map((h) => h.trim().toLowerCase());
|
||||
if (headers.length < 2) {
|
||||
return {
|
||||
statDef: { key: id, label: id, type: "template", scope, template: id, source },
|
||||
modifierDefs: [],
|
||||
template: emptyTemplate,
|
||||
};
|
||||
}
|
||||
|
||||
const notation = headers[0];
|
||||
const labelIdx = headers.indexOf("label");
|
||||
|
||||
// Bare column names (e.g. "mind", "heart") — these become targets
|
||||
const bareColumns = headers.filter((h, i) => i !== 0 && i !== labelIdx);
|
||||
|
||||
// Generate modifier stat defs: key = {id}_{column}, target = column
|
||||
const modifierDefs: StatDef[] = bareColumns.map((col) => ({
|
||||
key: `${id}_${col}`,
|
||||
label: col,
|
||||
type: "modifier" as const,
|
||||
scope,
|
||||
target: col,
|
||||
source,
|
||||
}));
|
||||
|
||||
// Build template entries with prefixed modifier keys
|
||||
const entries: TemplateEntry[] = [];
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const row = lines[i].trim();
|
||||
if (!row || row.startsWith("#")) continue;
|
||||
const cols = splitCsvRow(row);
|
||||
if (cols.length === 0) continue;
|
||||
|
||||
const range = cols[0]?.trim();
|
||||
if (!range) continue;
|
||||
|
||||
const label = labelIdx !== -1 ? cols[labelIdx]?.trim() || range : range;
|
||||
|
||||
const modifiers: Record<string, string> = {};
|
||||
for (const col of bareColumns) {
|
||||
const colIdx = headers.indexOf(col);
|
||||
if (colIdx !== -1 && colIdx < cols.length) {
|
||||
const val = cols[colIdx].trim();
|
||||
if (val) {
|
||||
modifiers[`${id}_${col}`] = val;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
entries.push({ range, label, modifiers });
|
||||
}
|
||||
|
||||
const template: StatTemplate = { name: id, notation, entries, source };
|
||||
|
||||
const statDef: StatDef = {
|
||||
key: id,
|
||||
label: id,
|
||||
type: "template",
|
||||
scope,
|
||||
template: id,
|
||||
source,
|
||||
};
|
||||
|
||||
return { statDef, modifierDefs, template };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function splitCsvRow(row: string): string[] {
|
||||
const cols: string[] = [];
|
||||
let current = "";
|
||||
|
||||
Reference in New Issue
Block a user