BELOW THE IRON Modding guide

Magic#

This page lets you add new spells to the game: a syllable, a spell definition, a damage element, a visual combination, a reaction, a named formula or a grimoire. Spell definitions live in public/config/magic.json. Book models and drops use items.json and loot.json. None of this needs code.

How a spell is built#

The player types a formula: a short chain of syllables. The game turns that chain into a spell in four steps. Take KA TOR, the fireball.

  KA  TOR              what the player types
   |   |
   v   v
 grammar                essence first, then form, then modifiers
   |
   v
 definitions            ka  -> "fire"      tor -> "blast"
   |
   v
 actions + statuses     fire damage, burning for 2 turns, burst of radius 2

The first damage in the essence decides the spell element. For ka it is fire.

Add a syllable#

Goal: let the player type a new syllable, ICE, that casts water magic.

The game ships with a water definition, but no syllable points to it. Add one under magic.syllables:

{ "ice": { "role": "essence", "name": "SYLLABLE_ICE", "definition": "water", "signature": { "glyph": "❄", "color": "#9dcbd1", "rhythm": "hold", "stroke": "dashed", "timbre": { "wave": "triangle", "pitch": 440 } } } }
In game

The syllabary lists a new essence with a snowflake. ICE casts frost damage and freezes the target for 3 turns.

Variations:

Add a spell definition#

Goal: replace what air does with a lightning storm that pushes enemies back further each time it repeats.

{ "storm": { "name": "MAGIC_SYSTEM_AIR", "role": "CONTENT_ELECTRICITY_SPREADS_THROUGH_LINKS", "traits": ["electric"], "visual": "lightning", "actions": [{ "kind": "damage", "element": "lightning", "factor": 1 }, { "kind": "push", "extra": 0 }], "geometry": { "chainRadius": 3 }, "transforms": [{ "kind": "scale-with-repeats", "action": "push", "param": "extra", "perRepeat": 1 }] } }
In game

A syllable that points to storm deals lightning damage, which jumps to creatures within 3 cells. It also pushes the target, one more cell for each repeat.

The parts of a definition:

Every syllable needs a reagent: the item that is spent when it is cast. A reagent has an id (lowercase letters only), a name, a symbol, a color, an effect that names a definition, and a power above 0:

{ "id": "stormseed", "name": "WORLD_SPIRIT_ASH", "symbol": "✧", "color": "#9dcbd1", "effect": "air", "power": 9 }

Add a damage element#

Goal: add a new element, holy, then make an item deal it.

Declare it in magic.elements. The visual block is optional. Anything you leave out gets a neutral grey look.

{ "id": "holy", "label": "ELEMENT_HOLY", "visual": { "shape": "light", "color": "#ffd700", "missile": true } }

Write ELEMENT_HOLY in every file of public/config/locales/. Now a modifier can add holy damage to every hit. Put it in an affix effect:

{ "hook": "hit.element", "op": "add", "value": 2, "when": { "element": "holy" } }
In game

Each hit with the item deals 2 extra holy damage, shown in the holy colour. A spell can deal it too: use "element": "holy" in a damage action.

Variations:

Add an effect combination#

Goal: when a spell holds both fire and a binding effect, add a flame layer to its animation.

{ "id": "scorch", "requires": ["fire", "bind"], "visual": { "shape": "flame", "color": "#ff8d38", "glyph": "♨" } }
In game

A spell with fire and binding (a web, a tangle) gets an extra flame layer, and fields it leaves behind show the ♨ glyph. Damage, statuses and power do not change.

requires lists two to four effect names. Use the visual name of each effect: water is frost, air is lightning, earth and anchors are bind, life is heal. A combination only draws a layer if its visual names a shape. Without one, it only supplies its glyph.

Reactions#

A reaction rewrites a spell when its syllables carry certain traits together. The game ships with eight. For example, heat plus propulsive makes a fireball.

Goal: when heat meets a wave, drop the damage and set the target on fire instead.

{ "id": "kindling-wave", "requires": ["heat", "resonant"], "priority": 80, "name": "WORLD_KINDLING_WAVE", "description": "WORLD_KINDLING_WAVE_BURNS_WITHOUT_BLOW", "remove": ["damage"], "actions": [{ "kind": "status", "status": "burning", "turns": 2 }] }
In game

Casting KA RUM (fire plus wave) no longer hurts anyone directly. It burns everything it touches for 2 turns.

Statuses#

A status is a lasting condition on a creature. A status action names one and a number of turns, from 1 to 100:

{ "kind": "status", "status": "regeneration", "turns": 3, "factor": 1 }
In game

The target regains health every turn for 3 turns. Each turn it heals the spell power times factor.

There are four statuses: burning (damage each turn), regeneration, frozen (cannot move) and slowed. Only regeneration takes a factor. Adding a new status needs code.

Named formulas, favourites and grimoires#

A named formula is a formula the game knows by name. Its id is its syllables joined with +, and the same id is used by known formulas, favourites and grimoires.

Goal: give KA TOR a name.

{ "ka+tor": { "syllables": ["ka", "tor"], "name": "MAGIC_FORMULA_FIREBALL" } }
In game

The player sees "Fireball" in their formula list. If the id does not match the syllables, or the grammar refuses the chain, the game fails to load and names the faulty path.

Favourites. The first few favourites are quick slots, bound to F1 to F4 by default. Set how many (1 to 12):

{ "quickSlots": 4 }

Grimoires. A grimoire distribution belongs to loot.json. Its grimoire reference names a model in items.json, under grimoires. The model owns the name and icon. It teaches count formulas the player does not know yet, picked from your named formulas. depthWeights makes it more common deeper: the weight is weight until the first level, then the weight of the last level reached.

{ "type": "grimoire", "grimoire": "grimoire", "weight": 1, "count": 1, "depthWeights": [{ "minLevel": 1, "weight": 3 }, { "minLevel": 6, "weight": 5 }] }
In game

Grimoires drop in the floors listed, and reading one teaches a new formula. From level 6, it drops more often.

More settings#

These are rarely needed. Most live in magic.json; push settings belong to combat.json. Reagents, including their name, symbol and effect reference, belong to resources.json.

magic.effectNames gives names to the shared effects light, heal and bind. Each entry chooses exactly one definition reference or one label key. An element keeps priority over this alias. An unknown definition or a missing translation is refused.

Advanced: push, light, resistance, cost and sounds
  • Push. combat.json sets the default push distance and its cap. The push action adds its extra on top, up to the cap: { "distance": 1, "maxDistance": 2 }.
  • Light anchors. A light action has an anchor: caster lights you, target lights the creature hit, ground leaves a light on the floor. Example: { "kind": "light", "anchor": "target", "radius": 4 }.
  • Transformations. A transform changes an action after the spell is planned. convert-action turns healing into regeneration; spread-over-turns spreads a hit over several turns.
  • Magic resistance. variantMagicResistances in creature-rules.json sets how much spell damage a creature variant absorbs, from 0 to 1: { "ash": 0.5 }. Weapons and traps are not reduced.
  • Shape sounds. magic.shapeSounds gives each drawn shape a launch sound and an impact sound, as file paths from the site root.
  • Syllable signatures. The signature only changes how a syllable looks and sounds. Add voice with a .mp3, .ogg or .wav path to use a recorded voice.
  • Mana cost. magic.manaCost is base + perPower x power + perSyllable x syllables: { "base": 0, "perPower": 0.6, "perSyllable": 1 }.