Expression (Data Type)

A String representing a mathematical expression.

A String representing a mathematical expression. Any field documented as accepting an Expression also accepts a plain number.

Where Expressions work

ConsumerFields
Attribute Modifiervalue
Status Effect Instanceduration, amplifier (evaluated against the entity receiving the effect)
apoli:chancechance
apoli:resource / apoli:cooldownmin, max, start_value / cooldown
apoli:action_on_key_press, apoli:action_on_key_sequence, apoli:action_on_hit, apoli:action_when_hit, apoli:action_on_kill, apoli:action_on_collision, apoli:fire_projectile, apoli:game_event_listenercooldown
Resourcecompare_to, position
apoli:modify_resourceposition, from_position
Model Part Transformationvalue, and each keyframe’s value
apoli:entity_in_radius, apoli:block_in_radiuscompare_to
apoli:change_resourcechange
Entity actionsadd_velocity (x/y/z), damage (amount), heal, exhaust, feed (food/saturation), gain_air, add_xp (points/levels), set_on_fire (duration)

Operators

OperationOperator(s)SyntaxDescription
Addition+a + bAdds a and b.
Subtraction-a - bSubtracts b from a.
Multiplication*, ×a * bMultiplies a by b.
Division/, ÷a / bDivides a by b.
Fraction_a_bRepresents a fraction of a / b.
Mixed number_c_a_bc + (a / b).
Exponentiation^a^bRaises a by the power of b (right-associative).
Factorial!a!Factorial of a.
Modulo#a # ba mod b.
Percentage%a%Decimal form of a as a percentage: (a / 100).
Negation--aNegative of a.
Tetration (hyper-4)^^a^^ba raised to itself b times.
Integer division\a\bInteger part of a / b.
Implied multiplication()a(b), 2xa * b.
Comparison==/=, !=/<>, <, <=, >, >=a < b1 if true, 0 if false, multiply or feed into if() to branch.
Boolean AND&&, &a && b1 if both are non-zero, else 0. Binds tighter than OR.
Boolean OR\|\|, \|a \|\| b1 if either is non-zero, else 0. Lowest precedence.

Functions

NameFunctionSyntaxDescription
Square rootsqrt()sqrt(a)√a.
Absolute valueabs()abs(a)|a|.
Minimum / Maximummin(), max()min(a, b, ...)Smallest / largest argument (any count).
Clampclamp()clamp(v, lo, hi)v limited to [lo, hi].
Floor / Ceilingfloor(), ceil()floor(a)Round down / up.
Roundround()round(a)Round to nearest integer.
Signsign(), sgn()sign(a)-1, 0 or 1.
Linear interpolationlerp()lerp(a, b, t)a + (b - a) * t.
Branchif()if(cond, then, else)then if cond is non-zero, else else. Only the taken branch is evaluated.
Logicand(), or(), not()and(a, b, ...)Boolean logic over non-zero values, returns 1/0.
Modulo / Powermod(), pow()mod(a, b)Function forms of # and ^.
Sine / Cosine / Tangentsin(), cos(), tan(), tg()sin(a)Trigonometry (radians).
Secant / Cosecant / Cotangentsec(), csc(), cosec(), cot(), ctg(), ctan()sec(a)Reciprocal trigonometry.
Inverse trigasin(), acos(), atan() (+ ar…/arc… aliases)asin(a)Inverse trigonometry.
Two-argument arctangentatan2()atan2(y, x)Angle of the point (x, y) in radians.
Hyperbolicsinh(), cosh(), tanh() (tgh, th), coth() (ctgh, cth), sech(), csch() (cosech)sinh(a)Hyperbolic functions.
Inverse hyperbolicasinh(), acosh(), atanh() (+ ar…/arc… aliases)asinh(a)Inverse hyperbolic functions.
Logarithmsln(), log2(), lg(), log10()ln(a)Natural, binary and common log.
Arbitrary-base logarithmlog()log(base, a)Log of a in base base.
Exponentialexp()exp(a)e^a.
Cube root / n-th rootcbrt(), root()root(n, a)∛a / n-th root of a (odd integer roots of negatives work).
Hypotenusehypot()hypot(a, b)√(a² + b²) without overflow.
Degrees / Radiansdeg(), rad()deg(a)Radians → degrees / degrees → radians.
Averageavg(), mean()avg(a, b, ...)Arithmetic mean of the arguments (any count).

Constants: pi, e.

Random functions

NameFunctionSyntaxDescription
Random uniform continuousrUni()rUni(a, b)Random real in [a, b].
Random uniform discreterUnid()rUnid(a, b)Random integer in [a, b].
Normal (Gaussian)rNor()rNor(μ, σ)Drawn from N(μ, σ).
Random from listrList()rList(a, b, ..., z)Random element (only the chosen one is evaluated).

Random generators

NameFunctionDescription
Random integer[Int]Random integer in [-2^31, 2^31 - 1].
Random bounded integer[IntX]Random integer in [-(10^X), 10^X], X = 1–9.
Random natural (incl. 0)[nat]Random integer in [0, 2^31 - 1].
Random natural (incl. 0, bounded)[natX]Random integer in [0, 10^X].
Random natural (excl. 0)[Nat]Random integer in [1, 2^31 - 1].
Random natural (excl. 0, bounded)[NatX]Random integer in [1, 10^X].
Random uniform [0, 1][Uni]Random uniform real in [0, 1].
Standard normal[Nor]Drawn from N(0, 1).

[ only starts a random generator when the brackets contain one of the names above. After enchantment, nbt or data it holds that function’s arguments (see Enchantment levels and Reading NBT). Everywhere else it indexes a table resource, so example:table[Nat] is a resource times a random natural, while example:table[2] is slot 2.

Bound variables

Variables are resolved when the expression is compiled and read live from the entity each evaluation. If the evaluating context has no entity (e.g. chance in a block action), entity variables read 0.

VariableMeaning
valueContext-dependent: the current resource/cooldown value in resource operations; the unmodified base value inside an Attribute Modifier. 0 elsewhere.
damageThe damage amount of the hit, inside actions fired by action_on_hit / action_when_hit (including their self/target/attacker/bi-entity actions and anything nested in them, e.g. a modify_resource with "value": "damage * 2"). 0 outside a hit context.
<namespace>:<path>Value of any Resource/Cooldown power the entity has (the full power id is the variable name). Missing → 0.
<namespace>:<path>_maxThe maximum of that Resource/Cooldown power, evaluated live — so it follows an expression-valued max. If no such resource exists, the whole name is read as a plain resource id instead. Missing → 0.
<namespace>:<path>_minThe minimum of that Resource power, same rules as _max.
<namespace>:<path>_sizeHow many slots that Resource power has (1 for a scalar resource). Same fallback rules as _max.
<namespace>:<path>[n]Slot n of a table Resource. n is itself an expression, so example:table[example:cursor + 1] works. Out of range → 0.
distanceDistance from the ray origin to the hit, inside apoli:raycast hooks. 0 elsewhere.
hit_x, hit_y, hit_zThe hit position, inside raycast hooks. 0 elsewhere.
countHow many entities matched, inside apoli:area_of_effect and raycast hooks. 0 elsewhere.
indexThe zero-based iteration number, inside per-entity hooks of area_of_effect and raycast. 0 elsewhere.
power_countHow many powers the entity holds.
health / max_healthCurrent / maximum health.
absorptionAbsorption hearts.
armorArmor value.
air / max_airAir supply ticks / maximum.
fall_distanceCurrent fall distance in blocks.
x, y, zEntity position.
yaw, pitchHead rotation in degrees.
velocity_x, velocity_y, velocity_zCurrent motion vector.
foodFood level (0–20). Players only, else 0.
saturationSaturation level. Players only, else 0.
xp_levelExperience level. Players only, else 0.
xp_progressProgress in the current XP level (0.0–1.0). Players only, else 0.
world_timeThe level’s game time (ticks).
day_timeThe level’s time-of-day (# 24000 for the clock time).
moon_phaseMoon phase, 0–7.

Actor and target

In a bi-entity context (any bi-entity action or condition), a bare variable reads the actor — the entity the power belongs to. Prefix it with target_ to read the other side instead, and actor_ is an explicit synonym for the default:

{
  "type": "apoli:damage",
  "damage_type": "player_attack",
  "amount": "5 + ((max_health - health) * 0.35)"
}

As a bi-entity action that deals more damage the more health the actor is missing. "target_max_health - target_health" would read the victim instead.

The binding covers the whole bi-entity action, including anything nested inside it — so an apoli:actor_action wrapping a apoli:modify_resource can still read target_health, and a bare variable inside an apoli:target_action reads the target because that is the entity in scope there while actor_ still reaches back to the actor.

Outside a bi-entity context there is no second entity, so target_ and actor_ both fall back to the one entity in scope. The prefixes work on the plain variables above, not on resource ids — use target_resource(...) and actor_resource(...) for those. That matters when an apoli:target_action runs a coordinate-taking action such as apoli:teleport: a bare my_pack:aim_x there reads the target’s resource, which is 0 if the target does not have it, so write actor_resource(my_pack:aim_x) to read the number the caster stored.

Bi-entity conditions do not set the binding; only actions do. That keeps the per-candidate condition path free of the extra bookkeeping.

Inside a damage modifier

The amount of an Attribute Modifier on apoli:modify_damage_dealt, apoli:modify_damage_taken and apoli:modify_projectile_damage is an expression, and it is bound the same way: a bare variable reads the power holder, actor_ reads the attacker and target_ reads the entity being hit. resource(...) reads the holder’s resource; actor_resource(...) and target_resource(...) reach the other side.

So on a modify_damage_dealt, health is the attacker’s health and target_health is the victim’s:

{
    "type": "apoli:modify_damage_dealt",
    "modifier": {
        "operation": "set_total",
        "amount": "health"
    }
}

hits for exactly as much as the attacker currently has, and "amount": "target_health" hits for exactly the victim’s remaining health. nbt[...] follows the same rule — nbt[Health] is the holder, nbt[target, Health] the other side.

Functions that take an id

A few functions take a bare namespace:path id as their first argument. Any remaining arguments are ordinary expressions.

FunctionMeaning
resource(id) / resource(id, n)The resource’s value, or the value in slot n. The explicit form of id and id[n].
target_resource(id) / target_resource(id, n)The same, read from the target in a bi-entity context.
actor_resource(id) / actor_resource(id, n)The same, read from the actor in a bi-entity context.
resource_size(id)How many slots the resource has.
resource_contains(id, v)1 if any slot of the resource holds v, else 0.
resource_index_of(id, v)The first slot holding v, or -1.
has_resource(id)1 if the entity has that resource at all, else 0.
has_power(id) / target_has_power(id) / actor_has_power(id)1 if the entity holds that power, else 0.

With the Origins mod installed, five more are registered:

FunctionMeaning
has_origin(id) / target_has_origin(id)1 if the player has that origin on any layer, counting an active swap.
in_origin_pool(id)1 if that origin is in any of the player’s swap pools.
has_origin_on(layer)1 if the player has a non-empty origin on that layer.
origin_impact(layer)The impact of the layer’s active origin: 0 none, 1 low, 2 medium, 3 high.

Origins also registers the plain variables origin_count (layers with a non-empty origin) and is_swapped (1 while any layer is swapped).

Unknown variable names, unknown functions and any other syntax error are a load-time error: the power (or other JSON file) containing the expression fails to parse and the error message names the offending expression and position. There is no fallback engine — the compiled engine is the only evaluator (the bundled mXparser fallback was removed in July 2026; everything it was kept around for is now supported natively).

Reading a table resource

A apoli:resource with a size above 1 stores several values. Index it with square brackets:

{
  "type": "apoli:add_velocity",
  "x": "example:pos[0]",
  "y": "example:pos[1]",
  "z": "example:pos[2]"
}

The index is a full expression, so it can be computed. example:table_size gives the slot count, and resource_contains(example:table, 5) searches the whole table. An unindexed reference reads slot 0.

Reading a resource’s bounds

Suffixing a resource id with _max or _min reads that resource’s limit rather than its current value:

{
  "type": "apoli:change_resource",
  "resource": "example:mana",
  "change": "example:mana_max / 10",
  "operation": "add"
}

The bound is evaluated at the same moment as the expression around it, so a resource whose max is itself an Expression ("max": "20 + 5 * xp_level") reports its current ceiling, not a stale one.

If there is no resource power at the stripped id, the name is treated as an ordinary resource id — so a resource genuinely called example:mana_max still resolves to its own value. Bound lookups nest up to 8 deep; beyond that they read 0, which stops a cycle (a max referring to its own _max) from hanging the server.

Enchantment levels

enchantment[...] reads the level of an enchantment on the entity’s gear:

{
  "type": "apoli:heal",
  "amount": "10 + enchantment[minecraft:mending, weapon.mainhand, sum]"
}

The arguments, separated by commas:

ArgumentValuesDefault
EnchantmentAn enchantment id such as minecraft:sharpness. The namespace can be left out.required
WhereAn Item Slot such as weapon.mainhand, armor.chest or hotbar.3; armor for the four armour slots; hands for both hands; equipment (also any or all) for every equipment slot.equipment
Calculationsum adds the levels found in each slot, max takes the highest — the same choice as apoli:enchantment’s calculation.sum

Everything after the id is optional and can come in either order, so enchantment[minecraft:mending, weapon.mainhand], enchantment[minecraft:protection, armor, max] and enchantment[minecraft:feather_falling] all work. On a single slot sum and max give the same number.

An enchantment that is not installed reads 0 rather than failing to load, so an expression can mention an enchantment from another mod. Hotbar, inventory and ender chest slots read players only; on any other entity they read 0. In a bi-entity context, target_enchantment[...] and actor_enchantment[...] read the target’s and the actor’s gear, like the target_ and actor_ variables.

Reading NBT

nbt[...] reads a number out of NBT data, the same way /data get does. data[...] is another name for it.

{
  "type": "apoli:modify_resource",
  "resource": "example:ammo",
  "modifier": {
    "operation": "set_total",
    "amount": "nbt[Inventory[{id:\"minecraft:arrow\"}].count, sum]"
  }
}

The arguments, separated by commas:

ArgumentValuesDefault
Sourceself (also entity) — the entity the expression runs on; target or actor — the other entity in a bi-entity context; an Item Slot — the item in that slot; block, <x>, <y>, <z> — the block entity at a position, where each coordinate is an expression of its own; storage, <id> — a command storage, as in /data get storage.self
PathAn NBT path, written exactly as in /data get: Health, Inventory[0].count, Items[{Slot:0b}].count.required
AggregateWhat to do when the path matches more than one value: first, sum, max, min, or count (also size) for how many values matched.first

So nbt[Health], nbt[Inventory[].count, sum], nbt[weapon.mainhand, components."minecraft:damage"], nbt[storage, example:vars, counter] and nbt[block, x, y - 1, z, Items[].count, sum] are all valid. The value is read the way /data get reports it: a number as itself, a string as its length, a list or compound as how many entries it holds, and a path that matches nothing as 0.

The paths are whatever /data get shows on your version of the game — item data lives under components on 1.21.1 and under tag on 1.20.1.

Writing an entity out to NBT is not free. Apoli does it at most once per entity per tick and shares the result between every nbt[...] that reads that entity during the tick, and caches items and block entities the same way — so a change made earlier in a tick shows up from the next tick. Where a plain variable exists (health, food, xp_level), use it: it reads the value directly. Expressions that run on the client see the client’s copy of the entity, which carries much less data, and storage reads 0 there.

NaN policy

If an Expression evaluates to NaN or ±Infinity (e.g. division by zero, sqrt(-1)), Apoli substitutes 0. Use if() for explicit fallbacks: if(max_health > 0, health / max_health, 1).