Formulas
Most stat-based systems have values that aren’t entered directly — they’re computed from other values. D&D’s ability modifier is computed from the raw ability score. A spellcaster’s maximum spell slots per day might be computed from level and casting stat. A damage roll is computed from strength plus weapon die plus proficiency. These computed values are formulas in Ishvana’s Magic System module, and the formula engine is what makes the whole module feel alive instead of being a glorified spreadsheet.
What a formula looks like
Section titled “What a formula looks like”A formula has a name, an output type, and an expression. The expression references stats and other formulas by their variable keys. When a stat block is evaluated, formulas run in dependency order — formula B that depends on formula A waits for A to compute first, then runs on A’s result.
A classic D&D ability modifier formula looks like this:
name: Strength modifierkey: str_modexpression: floor((strength - 10) / 2)output_type: integerThat formula references a stat called strength. When a character’s effective Strength is 15, the formula evaluates to floor((15 - 10) / 2) = 2. A character with Strength 20 gets modifier +5. A character with Strength 8 gets modifier -1.
Output types
Section titled “Output types”Every formula has a declared output type that controls how its value gets displayed and used downstream:
- Integer — A whole number. Ability modifiers, skill points, hit dice count.
- Float — A decimal. Influence ratings, cultivation progress, percentage-based values.
- Dice — A dice notation string like
2d6+3. Useful for formulas that compute what die a character rolls, not the result of the roll. - Boolean — True or false. “Can this character cast level 3 spells?” is a boolean formula that’s true when caster_level >= 5.
The output type matters because other formulas that consume this formula’s output need to know what shape of value to expect. A formula that multiplies the result of another formula by 2 will error if the source formula returns a dice notation string.
Dice notation
Section titled “Dice notation”The expression engine supports full dice notation alongside math expressions. You can write:
| Notation | Meaning |
|---|---|
2d6 | Roll two six-sided dice, sum them |
1d20 | Roll one twenty-sided die |
4dF | Roll four Fate dice (each -1, 0, or +1) |
1d20kh1 | Roll one d20, keep the highest (trivial here, more useful with multiple dice) |
2d20kh1 | Advantage — roll two d20s, keep the highest |
2d20kl1 | Disadvantage — roll two d20s, keep the lowest |
3d6! | Exploding — 3d6 where 6s reroll and add |
1d6r1 | Reroll 1s once |
Dice expressions can be combined with stat references and arithmetic. 1d20 + strength_mod + proficiency_bonus is a valid formula expression that adds a d20 roll to a stat-derived modifier and a resource-derived modifier.
The probability analyzer in the Magic System module reads these expressions and computes distributions — so you can write a formula for “attack roll” and then run a probability analysis on it without having to re-enter the dice notation somewhere else.
Safe functions
Section titled “Safe functions”The expression engine whitelists a specific set of functions. You can’t call arbitrary code, and you can’t reference anything outside the ruleset. The allowed functions are:
- floor(x) — Round down to the nearest integer.
- ceil(x) — Round up.
- round(x) — Round to the nearest integer (0.5 rounds up).
- abs(x) — Absolute value.
- min(a, b, …) — The smallest of the arguments.
- max(a, b, …) — The largest of the arguments.
- clamp(x, low, high) — Clamps x to the range [low, high].
These seven cover almost every formula you’d want to write. If you need something else — a sqrt, a log, a trigonometric function — the answer is almost always “your ruleset is doing too much math, simplify it.” The function whitelist is deliberately small because a Magic System that needs logarithms is probably a system that would be clearer if it didn’t.
Circular dependency detection
Section titled “Circular dependency detection”A formula can reference another formula. That’s how multi-step derivations work — you compute an intermediate value and then use it in a later formula. The risk is that formula A might reference formula B, which references formula C, which references formula A. That’s a cycle, and the engine has no way to evaluate it.
Ishvana’s validator catches these cycles before they can cause problems. When you save a ruleset, the validator walks the formula dependency graph looking for loops. If it finds one, the save is rejected with a message naming every formula involved in the cycle. You have to break the cycle — either by eliminating one of the references or by introducing a third value — before the ruleset can save.
Evaluation order
Section titled “Evaluation order”When a stat block is evaluated, the engine runs formulas in dependency order. Any formula that depends on nothing (references only base stats) runs first. Formulas that depend on those run next. And so on until every formula has a value. The result is a fully computed stat block where every derived value reflects the current base values and modifiers.
Evaluation happens every time a base value or modifier changes. Change a character’s Strength and every formula that depends on Strength updates live in the stat block panel. The update is instantaneous — you’re not waiting for a recompute — because the formulas are simple enough that even a large ruleset evaluates in milliseconds.
What formulas can’t do
Section titled “What formulas can’t do”The formula engine is intentionally not Turing-complete. A formula can’t:
- Loop. No
while, nofor. Every formula runs once per evaluation. - Branch. No
if/else. You can usemin,max, andclampto get conditional-looking behavior, but there’s no true branching. - Call external code. The whitelist is the whitelist. No imports, no function definitions, no side effects.
- Modify state. Formulas are pure — they read from the stat block and return a value. They don’t write back.
These restrictions exist because a formula is a description of a value, not a program. The moment formulas can have side effects or loops, the evaluation order stops being obvious, and debugging a stat block becomes harder than it needs to be. The small, boring feature set is deliberate.