refactor: implement base/modifier separation for variable reactivity

Introduce a distinction between base values (set via `/set` or
declarations) and active modifiers (applied via tags). This ensures
tag transitions and numeric modifiers are calculated correctly during
cascades.

- Refactor `VariableView` to use a flat list with hover popups for
  declarations and active modifiers.
- Update `command-dispatcher` to use `getCombined` for accurate
  value lookups and `setBase` for variable updates.
- Update `var-reactivity` to manage `baseValues`, `activeMods`, and
  `sourceActivations` separately.
- Export new utility functions for accessing combined values,
  modifiers, and declaration expressions.
This commit is contained in:
2026-07-13 10:10:47 +08:00
parent 411f4d79ff
commit c524dd5867
4 changed files with 375 additions and 435 deletions
+19 -9
View File
@@ -11,7 +11,7 @@ import { parseInput } from "./command-parser";
import { resolveRollPayload } from "./types/roll";
import { resolveSparkPayload } from "./types/spark";
import { evaluateExpression, expressionIsTag } from "./variable-expression";
import { computeCascade } from "./var-reactivity";
import { computeCascade, getCombined, setBase } from "./var-reactivity";
import type { VarDeclaration, TagModifier } from "./declare-parser";
// ---------------------------------------------------------------------------
@@ -140,7 +140,9 @@ function dispatchSet(
}
const key = p.key;
const oldValue = ctx.variables[key] ?? undefined;
// Use getCombined for old value so tag transitions are detected correctly
// even when the variable has active mods.
const oldValue = getCombined(key);
let newValue: string;
try {
@@ -152,11 +154,12 @@ function dispatchSet(
// Bare tag value
newValue = p.expr.trim();
} else if (p.expr) {
// Numeric expression — evaluate
// Numeric expression — evaluate using combined values
const result = evaluateExpression(p.expr, {
lookup: (name: string) => {
const k = "$" + name;
return ctx.variables[k] ?? undefined;
// Use getCombined for accurate values including active mods
return k === key ? undefined : getCombined(k);
},
});
newValue = String(result.value);
@@ -170,12 +173,11 @@ function dispatchSet(
};
}
// Send the direct set
const r1 = sendMessage("var", { action: "set", key, value: newValue });
const u1 = unwrap(r1);
if (!u1.ok) return u1;
// Update base value so getCombined returns the correct new value
// during cascade computation
setBase(key, newValue);
// Build working variable store for cascade
// Build working variable store for cascade (includes the new value)
const workingVars = { ...ctx.variables, [key]: newValue };
// Compute cascade (tag activation/deactivation + declaration re-eval)
@@ -189,6 +191,14 @@ function dispatchSet(
console.warn("[dispatch] cascade error:", e);
}
// Send the direct set last so cascade messages arrive first.
// Use getCombined so the stream store receives the correct value
// (base + active mods) rather than just the raw base.
const combinedValue = getCombined(key);
const r1 = sendMessage("var", { action: "set", key, value: combinedValue });
const u1 = unwrap(r1);
if (!u1.ok) return u1;
return { ok: true };
}