diff --git a/src/cli/completions/block-processor.ts b/src/cli/completions/block-processor.ts
index c9d631a..5633d55 100644
--- a/src/cli/completions/block-processor.ts
+++ b/src/cli/completions/block-processor.ts
@@ -114,7 +114,7 @@ export function processBlocks(
if (attrs.role === "stat-modifiers") {
const id = attrs.id || `_mod_${contentHash(body)}`;
- const result = parseStatModifiers(body, fileRelativePath, id);
+ const result = parseStatModifiers(body, fileRelativePath, id, "player", attrs.extra["label"]);
blocks.stats.push(result.statDef);
blocks.stats.push(...result.modifierDefs);
blocks.statTemplates.push(result.template);
diff --git a/src/cli/completions/stat-parser.ts b/src/cli/completions/stat-parser.ts
index fb370a1..0139512 100644
--- a/src/cli/completions/stat-parser.ts
+++ b/src/cli/completions/stat-parser.ts
@@ -270,7 +270,9 @@ export function parseStatModifiers(
source: string,
id: string,
scope: "player" | "global" = "player",
+ label?: string,
): StatModifiersResult {
+ const statLabel = label || id;
const lines = csv.trim().split(/\r?\n/);
const emptyTemplate: StatTemplate = {
name: id,
@@ -281,7 +283,7 @@ export function parseStatModifiers(
if (lines.length === 0) {
return {
- statDef: { key: id, label: id, type: "template", scope, template: id, source },
+ statDef: { key: id, label: statLabel, type: "template", scope, template: id, source },
modifierDefs: [],
template: emptyTemplate,
};
@@ -290,7 +292,7 @@ export function parseStatModifiers(
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 },
+ statDef: { key: id, label: statLabel, type: "template", scope, template: id, source },
modifierDefs: [],
template: emptyTemplate,
};
@@ -344,7 +346,7 @@ export function parseStatModifiers(
const statDef: StatDef = {
key: id,
- label: id,
+ label: statLabel,
type: "template",
scope,
template: id,
diff --git a/src/components/journal/StatsView.tsx b/src/components/journal/StatsView.tsx
index d6a2076..b69ea96 100644
--- a/src/components/journal/StatsView.tsx
+++ b/src/components/journal/StatsView.tsx
@@ -8,7 +8,7 @@
import { Component, For, createMemo, Show } from "solid-js";
import { useJournalStream } from "../stores/journalStream";
import { useJournalCompletions } from "./completions";
-import { fullKey } from "./stat-helpers";
+import { fullKey, modifierLabel } from "./stat-helpers";
import type { StatDef } from "./completions";
export const StatsView: Component = () => {
@@ -153,7 +153,7 @@ export const StatsView: Component = () => {
- {def.label}
+ {modifierLabel(def, statDefs())}
{def.key}
diff --git a/src/components/journal/completions.ts b/src/components/journal/completions.ts
index 1d2cdcb..4a463a7 100644
--- a/src/components/journal/completions.ts
+++ b/src/components/journal/completions.ts
@@ -156,7 +156,7 @@ async function scanClientSide(): Promise {
if (attrs.role === "stat-modifiers") {
const id = attrs.id || `_mod_${filePath}_${stats.length}`;
- const result = parseStatModifiers(body, filePath, id);
+ const result = parseStatModifiers(body, filePath, id, "player", attrs.extra["label"]);
stats.push(result.statDef);
stats.push(...result.modifierDefs);
statTemplates.push(result.template);
diff --git a/src/components/journal/stat-helpers.ts b/src/components/journal/stat-helpers.ts
index 8f84d1a..2de2794 100644
--- a/src/components/journal/stat-helpers.ts
+++ b/src/components/journal/stat-helpers.ts
@@ -18,6 +18,36 @@ export function fullKey(def: StatDef, playerName: string): string {
return def.scope === "player" ? `${playerName}:${def.key}` : def.key;
}
+/**
+ * Derive a display label for a modifier stat.
+ *
+ * If the key follows the `{parent}_{target}` convention (from stat-modifiers),
+ * returns `{parent_label}/{target_label}` by looking up both defs.
+ * Otherwise falls back to the def's own label.
+ */
+export function modifierLabel(
+ def: StatDef,
+ statDefs: StatDef[],
+): string {
+ if (def.type !== "modifier") return def.label;
+
+ // Try to split key as parent_target
+ const lastUnderscore = def.key.lastIndexOf("_");
+ if (lastUnderscore === -1) return def.label;
+
+ const parentKey = def.key.slice(0, lastUnderscore);
+ const targetKey = def.key.slice(lastUnderscore + 1);
+
+ const parentDef = statDefs.find((d) => d.key === parentKey);
+ const targetDef = statDefs.find((d) => d.key === targetKey);
+
+ if (parentDef && targetDef) {
+ return `${parentDef.label}/${targetDef.label}`;
+ }
+
+ return def.label;
+}
+
/** Find a stat def by its full runtime key. */
export function findStatDef(
fk: string,
@@ -140,20 +170,11 @@ export function resolveTemplateSet(
const entry = tpl.entries.find((e) => e.label === value);
if (!entry || Object.keys(entry.modifiers).length === 0) return null;
- const lookup = makeStatLookup(runtimeStats, statDefs, playerName, def);
const resolved: Record = {};
for (const [mk, mv] of Object.entries(entry.modifiers)) {
const modFullKey = def.scope === "player" ? `${playerName}:${mk}` : mk;
- const currentVal = runtimeStats[modFullKey];
- const currentNum =
- currentVal !== undefined ? parseFloat(currentVal) : lookup(mk);
- const delta = parseFloat(mv);
- if (!isNaN(currentNum) && !isNaN(delta)) {
- resolved[modFullKey] = String(currentNum + delta);
- } else {
- resolved[modFullKey] = mv;
- }
+ resolved[modFullKey] = mv;
}
return resolved;
@@ -218,22 +239,12 @@ export function resolveStatRoll(
}
// Resolve modifier keys to full keys (same scope as def)
- // Modifier values like "+20" / "-10" are applied relative to current value
+ // Template values are absolute — set the modifier stat directly.
+ // The modifier stat (type: modifier) adds to its target in StatsView.
const resolvedModifiers: Record = {};
for (const [mk, mv] of Object.entries(entry.modifiers)) {
const modFullKey = def.scope === "player" ? `${playerName}:${mk}` : mk;
-
- // Compute absolute value: current + delta
- const currentVal = runtimeStats[modFullKey];
- const currentNum =
- currentVal !== undefined ? parseFloat(currentVal) : lookup(mk);
- const delta = parseFloat(mv);
- if (!isNaN(currentNum) && !isNaN(delta)) {
- resolvedModifiers[modFullKey] = String(currentNum + delta);
- } else {
- // If we can't compute relative, fall back to raw value
- resolvedModifiers[modFullKey] = mv;
- }
+ resolvedModifiers[modFullKey] = mv;
}
return {