Examples¶
Annotated .mora files in current (Mora v2) syntax. Each example shows
the full source and explains what it does. Read top to bottom or jump to
the pattern you need.
For a tutorial introduction see the Language Guide; for the relation catalog see relations.md.
1. Iron weapons damage boost — static¶
The simplest useful rule: find every iron weapon and pin its damage.
namespace my_mod.balance
use form :as f
iron_weapons(W):
f/weapon(W)
f/keyword(W, @WeapMaterialIron)
=> set form/damage(W, 20)
What it does¶
Iterates every WEAP record in the load order, keeps those with the
WeapMaterialIron keyword, and sets each one's base damage to 20.
Because the body only references form/* relations, the phase
classifier tags this as static. The compiler fully evaluates the
rule against the FactDB and emits one patch entry per match. At game
load the runtime just applies the patches — no rule evaluation.
Patterns¶
=> setis legal onform/damagebecause its type iscountable<Int>.countablealso supportsaddandsub, if you wanted to bump or slash damage instead of pinning it.f/weapon(W)is the shape-gating predicate. Without it,f/keywordwould match on any record carrying the keyword (armor, magic effects, NPCs).form/*relations are polymorphic across record types; restrict with a predicate likeform/weapon,form/npc,form/armor.
2. Silver weapons except greatswords — static with negation¶
namespace my_mod.silver
use form :as f
vampire_bane(W):
f/weapon(W)
f/keyword(W, @WeapMaterialSilver)
not f/keyword(W, @WeapTypeGreatsword)
=> add form/keyword(W, @VampireBane)
What it does¶
Finds silver weapons that are not greatswords and adds the
VampireBane keyword to each. The silver keyword is preserved —
form/keyword is a list<FormRef>, so keywords accumulate unless you
explicitly remove.
Patterns¶
notapplies to the single clause that follows.Wis already bound by the earlierf/weapon(W)clause, so negation is safe.- Both
addandremoveare legal onlist<_>relations;setis not.
3. Tag bandits with derived rules¶
namespace my_mod.bandits
use form :as f
# Derived rule: name a concept once, reuse it.
bandit(NPC):
f/npc(NPC)
f/faction(NPC, @BanditFaction)
elite_bandit(NPC):
bandit(NPC)
f/base_level(NPC, L)
L >= 30
# Patching rule: adds a keyword.
tag_elite_bandits(NPC):
elite_bandit(NPC)
=> add form/keyword(NPC, @ActorTypeNPC)
What it does¶
bandit and elite_bandit are derived rules (no effects, just
predicates). They're inlined wherever used. Changing the definition of
"bandit" once updates every downstream rule automatically.
4. Rename every NPC — scalar set¶
form/name is a scalar<String>. Only set is legal on a scalar; the
type checker would reject => add form/name(...) with a verb-mismatch
error.
5. Maintain a threat marker — auto-retract¶
namespace my_mod.threats
use ref :as r
use form :as f
# Add a ThreatMarker keyword to every placed reference whose base is
# an NPC in the bandit faction. The keyword is automatically removed
# when the reference is unloaded or the faction membership changes.
maintain threat_marker(R):
r/is_npc(R)
r/base_form(R, Base)
f/faction(Base, @BanditFaction)
=> add ref/keyword(R, @ThreatMarker)
What it does¶
maintain tells the compiler this rule tracks truth values
differentially. The runtime engine tracks every binding: when the body
becomes satisfied, it calls the add handler and records an effect
handle; when the body stops being satisfied, it uses the handle to call
the matching retract handler. No removal logic in the rule.
Patterns¶
r/base_form(R, Base)is the canonical join from a live reference (RefId) to its base record (FormRef). Anyform/*query over a reference must go throughbase_formfirst.ref/keywordis declaredlist<FormRef>with bothapply_handler(RefAddKeyword) andretract_handler(RefRemoveKeyword) wired up, so using it inmaintainis legal. Amaintainrule targeting a list relation that lacks a retract handler is a compile error.
6. Bandit bounty — on rule with arithmetic¶
This is the capstone. It's an edge-triggered rule with arithmetic and
a built-in function — exactly the file shipped as
test_data/bandit_bounty.mora.
namespace my_mod.bounty
use ref :as r
use form :as f
# Pay the player a bounty for killing a bandit, scaled by the victim's
# level plus a danger bonus when the victim out-levels the player.
on bandit_bounty(Player, Victim):
event/killed(Victim, Player)
r/is_player(Player)
r/is_npc(Victim)
r/base_form(Victim, Base)
f/faction(Base, @BanditFaction)
r/level(Victim, VL)
r/level(Player, PL)
=> add player/gold(Player, 10 * VL + 5 * max(0, VL - PL))
What it does¶
When an actor kills another actor, the event/killed relation fires.
If the killer is the player, the victim is an NPC in BanditFaction,
and both have levels, the rule credits the player:
- A base reward of
10 * VL, scaling with victim level. - A danger bonus of
5 * max(0, VL - PL): extra gold when the victim out-levels the player, zero otherwise.max(0, ...)clamps the difference from below so a lower-level victim doesn't subtract.
Example: level-4 victim, level-1 player → 10*4 + 5*max(0,3) = 55 gold.
Same victim, level-10 player → 10*4 + 5*max(0,-6) = 40 gold.
Patterns¶
on— edge-triggered, fires once per+1transition of the body. Unlikemaintain, there's no retraction; adding gold is a one-shot.event/killeddrives the edge. Using anyevent/*relation is what makes a ruleon-eligible; using it in amaintainrule is a compile error (events are deltas, not state).r/base_formcrosses from the liveVictimreference intoform/factionon its base. Without the bridge, the type checker would rejectform/faction(Victim, ...)becauseVictimis aRefId, not aFormRef.- Arithmetic binds with standard precedence:
10 * VL + 5 * max(...)parses as(10*VL) + (5*max(...)), not left-to-right. - Built-in
maxwidens toFloatwhen any argument is a float; here bothVLandPLareInt, so the result isInt.
7. Weapon rebalance — multi-rule file¶
A realistic file combining derived rules, multiple effects per rule, and negation.
namespace rebalance.weapons
use form :as f
# Derived: all non-unique, non-staff weapons.
common_weapon(W):
f/weapon(W)
not f/keyword(W, @WeapTypeStaff)
not f/keyword(W, @DaedricArtifact)
# Iron tier: modest damage, low gold value.
iron_rebalance(W):
common_weapon(W)
f/keyword(W, @WeapMaterialIron)
=> set form/damage(W, 12)
=> set form/gold_value(W, 15)
# Steel tier: better damage, higher value.
steel_rebalance(W):
common_weapon(W)
f/keyword(W, @WeapMaterialSteel)
=> set form/damage(W, 18)
=> set form/gold_value(W, 45)
Patterns¶
- Single definition, multiple consumers. Both tier rules inherit the
exclusions in
common_weaponfor free. - Multiple effects per rule. Each
=>line applies to every match; the body only evaluates once. - Rules don't interact. A weapon with both
WeapMaterialIronandWeapMaterialSteel(unusual but possible) would be patched by both rules; conflict resolution (last rule wins by file order) is reported bymora inspect --conflicts.
See relations.md for the full catalog of relations you can reference in bodies and heads; see language-guide.md for the tutorial walk-through.