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.
This commit is contained in:
parent
ef71a43f0a
commit
4f3b03d082
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
|||
<div class="flex items-center px-3 py-1.5 hover:bg-white/50 transition-colors">
|
||||
<div class="flex-1 min-w-0 flex items-center gap-1.5">
|
||||
<span class="text-sm text-gray-800 truncate">
|
||||
{def.label}
|
||||
{modifierLabel(def, statDefs())}
|
||||
</span>
|
||||
<span class="text-[10px] text-gray-400 bg-gray-100 px-1 rounded shrink-0">
|
||||
{def.key}
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ async function scanClientSide(): Promise<JournalCompletions> {
|
|||
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {};
|
||||
|
||||
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<string, string> = {};
|
||||
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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue