From 4f3b03d082e3c1849bfbaa3d8f65f76bf479409b Mon Sep 17 00:00:00 2001 From: hypercross Date: Thu, 9 Jul 2026 11:28:17 +0800 Subject: [PATCH] feat: support custom labels for stat modifiers Allows passing an optional label to `parseStatModifiers` to override the default ID-based label. This enables more descriptive names in the UI and improves how modifier labels are derived in the journal view. --- src/cli/completions/block-processor.ts | 2 +- src/cli/completions/stat-parser.ts | 8 ++-- src/components/journal/StatsView.tsx | 4 +- src/components/journal/completions.ts | 2 +- src/components/journal/stat-helpers.ts | 57 +++++++++++++++----------- 5 files changed, 43 insertions(+), 30 deletions(-) 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 {