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") {
|
if (attrs.role === "stat-modifiers") {
|
||||||
const id = attrs.id || `_mod_${contentHash(body)}`;
|
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.statDef);
|
||||||
blocks.stats.push(...result.modifierDefs);
|
blocks.stats.push(...result.modifierDefs);
|
||||||
blocks.statTemplates.push(result.template);
|
blocks.statTemplates.push(result.template);
|
||||||
|
|
|
||||||
|
|
@ -270,7 +270,9 @@ export function parseStatModifiers(
|
||||||
source: string,
|
source: string,
|
||||||
id: string,
|
id: string,
|
||||||
scope: "player" | "global" = "player",
|
scope: "player" | "global" = "player",
|
||||||
|
label?: string,
|
||||||
): StatModifiersResult {
|
): StatModifiersResult {
|
||||||
|
const statLabel = label || id;
|
||||||
const lines = csv.trim().split(/\r?\n/);
|
const lines = csv.trim().split(/\r?\n/);
|
||||||
const emptyTemplate: StatTemplate = {
|
const emptyTemplate: StatTemplate = {
|
||||||
name: id,
|
name: id,
|
||||||
|
|
@ -281,7 +283,7 @@ export function parseStatModifiers(
|
||||||
|
|
||||||
if (lines.length === 0) {
|
if (lines.length === 0) {
|
||||||
return {
|
return {
|
||||||
statDef: { key: id, label: id, type: "template", scope, template: id, source },
|
statDef: { key: id, label: statLabel, type: "template", scope, template: id, source },
|
||||||
modifierDefs: [],
|
modifierDefs: [],
|
||||||
template: emptyTemplate,
|
template: emptyTemplate,
|
||||||
};
|
};
|
||||||
|
|
@ -290,7 +292,7 @@ export function parseStatModifiers(
|
||||||
const headers = lines[0].split(",").map((h) => h.trim().toLowerCase());
|
const headers = lines[0].split(",").map((h) => h.trim().toLowerCase());
|
||||||
if (headers.length < 2) {
|
if (headers.length < 2) {
|
||||||
return {
|
return {
|
||||||
statDef: { key: id, label: id, type: "template", scope, template: id, source },
|
statDef: { key: id, label: statLabel, type: "template", scope, template: id, source },
|
||||||
modifierDefs: [],
|
modifierDefs: [],
|
||||||
template: emptyTemplate,
|
template: emptyTemplate,
|
||||||
};
|
};
|
||||||
|
|
@ -344,7 +346,7 @@ export function parseStatModifiers(
|
||||||
|
|
||||||
const statDef: StatDef = {
|
const statDef: StatDef = {
|
||||||
key: id,
|
key: id,
|
||||||
label: id,
|
label: statLabel,
|
||||||
type: "template",
|
type: "template",
|
||||||
scope,
|
scope,
|
||||||
template: id,
|
template: id,
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
import { Component, For, createMemo, Show } from "solid-js";
|
import { Component, For, createMemo, Show } from "solid-js";
|
||||||
import { useJournalStream } from "../stores/journalStream";
|
import { useJournalStream } from "../stores/journalStream";
|
||||||
import { useJournalCompletions } from "./completions";
|
import { useJournalCompletions } from "./completions";
|
||||||
import { fullKey } from "./stat-helpers";
|
import { fullKey, modifierLabel } from "./stat-helpers";
|
||||||
import type { StatDef } from "./completions";
|
import type { StatDef } from "./completions";
|
||||||
|
|
||||||
export const StatsView: Component = () => {
|
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 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">
|
<div class="flex-1 min-w-0 flex items-center gap-1.5">
|
||||||
<span class="text-sm text-gray-800 truncate">
|
<span class="text-sm text-gray-800 truncate">
|
||||||
{def.label}
|
{modifierLabel(def, statDefs())}
|
||||||
</span>
|
</span>
|
||||||
<span class="text-[10px] text-gray-400 bg-gray-100 px-1 rounded shrink-0">
|
<span class="text-[10px] text-gray-400 bg-gray-100 px-1 rounded shrink-0">
|
||||||
{def.key}
|
{def.key}
|
||||||
|
|
|
||||||
|
|
@ -156,7 +156,7 @@ async function scanClientSide(): Promise<JournalCompletions> {
|
||||||
|
|
||||||
if (attrs.role === "stat-modifiers") {
|
if (attrs.role === "stat-modifiers") {
|
||||||
const id = attrs.id || `_mod_${filePath}_${stats.length}`;
|
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.statDef);
|
||||||
stats.push(...result.modifierDefs);
|
stats.push(...result.modifierDefs);
|
||||||
statTemplates.push(result.template);
|
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;
|
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. */
|
/** Find a stat def by its full runtime key. */
|
||||||
export function findStatDef(
|
export function findStatDef(
|
||||||
fk: string,
|
fk: string,
|
||||||
|
|
@ -140,20 +170,11 @@ export function resolveTemplateSet(
|
||||||
const entry = tpl.entries.find((e) => e.label === value);
|
const entry = tpl.entries.find((e) => e.label === value);
|
||||||
if (!entry || Object.keys(entry.modifiers).length === 0) return null;
|
if (!entry || Object.keys(entry.modifiers).length === 0) return null;
|
||||||
|
|
||||||
const lookup = makeStatLookup(runtimeStats, statDefs, playerName, def);
|
|
||||||
const resolved: Record<string, string> = {};
|
const resolved: Record<string, string> = {};
|
||||||
|
|
||||||
for (const [mk, mv] of Object.entries(entry.modifiers)) {
|
for (const [mk, mv] of Object.entries(entry.modifiers)) {
|
||||||
const modFullKey = def.scope === "player" ? `${playerName}:${mk}` : mk;
|
const modFullKey = def.scope === "player" ? `${playerName}:${mk}` : mk;
|
||||||
const currentVal = runtimeStats[modFullKey];
|
resolved[modFullKey] = mv;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return resolved;
|
return resolved;
|
||||||
|
|
@ -218,22 +239,12 @@ export function resolveStatRoll(
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resolve modifier keys to full keys (same scope as def)
|
// 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> = {};
|
const resolvedModifiers: Record<string, string> = {};
|
||||||
for (const [mk, mv] of Object.entries(entry.modifiers)) {
|
for (const [mk, mv] of Object.entries(entry.modifiers)) {
|
||||||
const modFullKey = def.scope === "player" ? `${playerName}:${mk}` : mk;
|
const modFullKey = def.scope === "player" ? `${playerName}:${mk}` : mk;
|
||||||
|
resolvedModifiers[modFullKey] = mv;
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue