Language Guide¶
A tutorial walk through writing .mora files in the current (Mora v2)
syntax. Each section builds on the previous. For the full relation
inventory see relations.md; for the architecture see
how-mora-works.md.
File structure¶
Every .mora file starts with a namespace declaration, optional
requires mod(...) guards, and optional use imports:
namespace my_mod.weapons
requires mod("Skyrim.esm")
use form :as f
use ref :as r
use event :refer [killed, entered_location]
namespace— unique identifier for this module. Other files canuse my_mod.weapons :refer [rule_name]to import your rules.requires mod(...)— compile fails if the plugin isn't present in the load order. Add one line per dependency.use— Clojure-style namespace import.:as ALIASdefines an alias;:refer [a, b]pulls names in unqualified. Unaliased fully qualified names (form/weapon) always work —useis purely additive.
There is no prelude: every namespace use is explicit.
Rules¶
A rule has a head (name and arguments, colon-terminated), an optional
annotation (maintain or on), and a body of clauses and effects:
Indentation is significant (Python-style). Multiple body clauses are
implicitly AND. Rules without any => effect are derived rules —
they name a concept for reuse but don't patch anything on their own.
Variables and identifiers¶
| Syntax | Meaning |
|---|---|
Weapon, NPC, VL |
Logic variable (Capitalized identifier) |
@WeapMaterialIron |
EditorID reference, resolved to a FormID at compile time |
:low, :fire |
Keyword (interned symbol, user-defined tag value) |
42, 1.5 |
Int / Float literal |
"Nazeem" |
String literal |
form/weapon |
Namespaced relation name |
@EditorID is how you refer to a specific record by its Editor ID.
Mora looks it up in your ESPs at compile time and fails the build if
nothing matches.
:keyword is a completely different thing — a lightweight, interned
tag value. You can use keywords as rule arguments to categorize things
without allocating FormIDs for them. Example:
threat_level(NPC, :high):
form/npc(NPC)
form/base_level(NPC, L)
L >= 30
threat_level(NPC, :low):
form/npc(NPC)
form/base_level(NPC, L)
L < 10
The result is your own threat_level predicate whose second argument is
the keyword :low or :high.
Clauses¶
Body clauses are queries:
- Positive pattern:
ns/rel(args). True when the relation holds. - Negation:
not ns/rel(args). True when it doesn't. Only use on clauses whose variables are already bound by earlier clauses. - Comparison:
Expr op Exprwith>,<,>=,<=,==,!=.
Example combining most of them:
high_value_weapon(W):
form/weapon(W)
form/gold_value(W, GV)
GV >= 1000
not form/keyword(W, @DaedricArtifact)
Types and verbs¶
Each relation carries a type of the form ctor<Elem>:
| Type constructor | Legal verbs in head position | Example effect |
|---|---|---|
scalar<T> |
set |
=> set form/name(F, "Nazeem") |
countable<T> |
set, add, sub |
=> add player/gold(P, 100) |
list<T> |
add, remove |
=> add form/keyword(W, @Enchanted) |
const<T> |
(none — read-only) | used only in body position |
predicate |
(none — unary existence) | form/npc(F) |
Argument types are one of: Int, Float, String, FormRef, Keyword,
RefId. Mismatches are compile errors pointing at the source span.
The full list of every relation, its type, and which verbs are legal is in relations.md.
Effects¶
Effects follow the body, each on its own line, prefixed with => <verb>:
iron_boost(W):
form/weapon(W)
form/keyword(W, @WeapMaterialIron)
=> set form/damage(W, 20)
=> add form/keyword(W, @WeapMaterialDaedric)
Every => line applies to every binding that satisfies the body. The
verb is validated against the relation's type constructor at compile time;
=> add form/name(F, "...") would fail because form/name is a scalar
and scalars only support set.
The same relation that you query in the body you assert in the head.
form/keyword(W, @Iron) in the body is a query, in the head after
add it's an assertion. Verbs distinguish the kind of assertion, so
there are no separate has_keyword / add_keyword / remove_keyword
relations — there's just form/keyword.
Static, maintain, on — three rule tiers¶
The compiler classifies each rule from its body dependencies:
- Unannotated — all body relations live in compile-time-known
namespaces (
form/*). Fully evaluated at compile time. maintain— body uses runtime state (ref/*,player/*,world/*). Compiled to an operator DAG; effects auto-retract when the body stops being satisfied.on— body uses at least oneevent/*relation. Compiled to a DAG; effects fire once on+1, no retraction.
Examples of each:
# Static: compile-time patch, frozen at build time.
iron_weapons(W):
form/weapon(W)
form/keyword(W, @WeapMaterialIron)
=> set form/damage(W, 20)
# Maintain: tag refs whose base is a bandit; auto-retracts when the
# ref is unloaded (or its base changes).
maintain threat_marker(R):
ref/is_npc(R)
ref/base_form(R, Base)
form/faction(Base, @BanditFaction)
=> add ref/keyword(R, @ThreatMarker)
# On: edge-triggered — fires exactly once per killing blow.
on kill_notify(Player, Victim):
event/killed(Victim, Player)
ref/is_player(Player)
=> add player/notification(Player, "Target eliminated.")
Misuses are compile errors: unannotated rule depending on a dynamic
relation, maintain rule using event/*, maintain rule whose effect
has no retract handler.
Arithmetic and built-in functions¶
Arithmetic works in head-position expressions, with standard precedence
(* / bind tighter than + -). Variables are bound by body clauses;
arithmetic only reads them:
Built-in functions: max, min, abs, clamp. They're pure,
validated at compile time (unknown name or wrong arity is an error),
and widen to Float if any argument is a float.
# Scaled bounty that never goes negative.
on bandit_bounty(Player, Victim):
event/killed(Victim, Player)
ref/is_player(Player)
ref/is_npc(Victim)
ref/base_form(Victim, Base)
form/faction(Base, @BanditFaction)
ref/level(Victim, VL)
ref/level(Player, PL)
=> add player/gold(Player, 10 * VL + 5 * max(0, VL - PL))
Derived rules¶
A rule without effects defines a derived predicate that other rules can use as if it were built-in:
bandit(NPC):
form/npc(NPC)
form/faction(NPC, @BanditFaction)
elite_bandit(NPC):
bandit(NPC)
form/base_level(NPC, L)
L >= 30
arm_elites(NPC):
elite_bandit(NPC)
=> add form/spell(NPC, @FlameCloak)
Derived rules are fully inlined during evaluation. Use them wherever a definition would otherwise be copied.
Negation¶
silver_but_not_greatsword(W):
form/weapon(W)
form/keyword(W, @WeapMaterialSilver)
not form/keyword(W, @WeapTypeGreatsword)
=> add form/keyword(W, @VampireBane)
Negation applies to a single clause and requires its variables to be already bound by earlier clauses.
Disjunction — matching "A or B" inside one rule body — is not yet a surface feature. For now, factor each branch into its own derived rule and consume both where you need the union:
valuable_weapon(Item):
form/weapon(Item)
form/gold_value(Item, V)
V >= 1000
valuable_armor(Item):
form/armor(Item)
form/gold_value(Item, V)
V >= 1000
Putting it together¶
The bandit bounty from test_data/bandit_bounty.mora is a good capstone.
It's an on rule — edge-triggered on the kill event — joining a live
reference to its base record through the ref/base_form bridge, reading
two stats, then computing an arithmetic reward with max:
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))
For more annotated examples — static keyword patches, maintain rules with
auto-retract, additional on rules — see examples.md.