promotional bannermobile promotional banner

Overgrown's Apoli

A data-driven power system that's build for custom abilities, mechanics, and entire origin systems from JSON alone.
Back to Files

Apoli (1.90.0)

File nameapoli-1.90.0.jar
Uploader
OvergrownMCOvergrownMC
Uploaded
Sep 19, 2026
Downloads
13
Size
29.5 MB
Mod Loaders
Fabric
File ID
8925522
Type
R
Release
Supported game versions
  • 1.21.1

Curse Maven Snippet

Fabric

modImplementation "curse.maven:apoli-1631826:8925522"

Learn more about Curse Maven

What's new

Apoli — Changelog

Everything since 1.83.0, the last published build. Covers Fabric 1.20.1, Fabric 1.21.1 and NeoForge 1.21.1 unless a note says otherwise.


1.90.0

Added — the apoli:print entity action

Writes a line to the server log, and optionally to the entity's chat. A debugging tool: drop it into an action list to see whether that branch ran and what a value came out as. Also answers to apoli:log and apoli:logger.

Field Type Default What it does
text Text Component optional What to print. A plain string works; so does a full component with colour, translate or keybind.
number Expression optional A number to print, evaluated against the entity every time the action runs.
message_id String apoli/print The logger name the line is written under, so your prints can be filtered out of the rest of the log.
show_in_chat Boolean false Also send the line to the entity. Only players can see chat.

With both text and number the line reads Text: <text> Number: <number>. With one, it is just that value. With neither it prints the message_id, so a bare {"type": "apoli:print"} is a "did this run?" marker.

Fixed — apoli:print crashed on anything that was not a player

The action cast the acting entity to a player before doing anything else, so running it on a mob threw a ClassCastException out of the middle of whatever action list it sat in. It works on any entity now.

Fixed — apoli:print crashed on an expression, and on an empty config

A number written as an expression rather than a literal threw instead of printing — the action asked for the expression's constant value, which a real expression does not have. That is the only reason the field is an expression at all. A config with neither text nor number threw as well.

Fixed — apoli:print truncated decimals

"number": 2.5 printed 2. Numbers keep their decimals now, and a whole number prints as 5 rather than 5.0.

Fixed — a particle that could not be read failed silently

apoli:spawn_particles, apoli:particle and a raycast's particles all skipped a particle they could not build without a word, so a misspelled id — or a form the version does not accept — looked exactly like a power that was not running. Apoli names the particle in the log now, once, the first time it tries to build it.

That turned up a real trap. The params short form is 1.20.1 only:

{"type": "minecraft:dust", "params": "0.96 0.95 0.76 1.5"}

1.20.1 reads it as /particle command arguments. 1.21.1 reads it as SNBT, cannot, and spawns nothing. Write the fields out instead — this is read identically on every version:

{"type": "minecraft:dust", "color": [0.96, 0.95, 0.76], "scale": 1.5}

Fixed — inline badges on an apoli:multiple power logged a false warning

Origins reads a power's inline badges straight out of its file, but the apoli:multiple expansion did not recognise the field and warned on every load that it was "being ignored". The badges were never ignored; the warning was.

Fixed — Fabric 1.20.1 did not compile

apoli:freeze's action class imported an unrelated Fabric event class instead of Entity and Optional, on that tree only, so 1.89.0 never built there.

Performance — apoli:block_in_radius stops as soon as the answer is decided

The condition used to count every matching block in the radius before comparing, and built a list of every position in the shape before it started. A ">=" 1 check over a radius of 5 walked 1331 positions and allocated about a thousand short-lived objects, even when the first block it looked at already settled the question. It walks the shape in place now and returns the moment the count passes the threshold — the answer is identical for every comparison, because any count above the threshold compares the same way.

It still has to look at the whole radius to prove a block is absent, so this is still the expensive kind of condition to put on a power that is evaluated every tick — and apoli:shader re-resolves its condition once per player, every tick. When a per-tick power needs an expensive condition, put the condition on an apoli:action_over_time with an interval, have its rising_action and falling_action write a resource, and test that resource instead. There is a worked example on the apoli:action_over_time page.

Performance — smaller things

  • apoli:print resolves its logger once, when the power is read, instead of asking the logging framework for it on every call — that lookup walks the call stack. It also returns before evaluating number when nobody is listening, i.e. show_in_chat off and the logger's level excluding INFO.
  • apoli:action_on_key_press no longer allocates a lookup key per power, per tick, when no cooldown is running. An origin with twenty keyed sub-powers was allocating twenty short-lived objects every tick, per holder, to find an empty map.

1.89.0

Removed — the apoli:freeze power type

The apoli:freeze and apoli:unfreeze entity actions do everything it did and take a duration, so the power was a second way to say the same thing.

  • {"type": "apoli:freeze"} as a power keeps working. It is read as an apoli:action_over_time running the apoli:freeze action every tick, so existing packs behave exactly as before — including their condition.
  • New packs should write the action. It can be fired from anything, takes a duration, and sits in an actions list next to other steps.

Fixed — freezing never actually dealt freeze damage

Vanilla drains the freeze counter by 2 every tick before it checks whether you are fully frozen, so a counter pinned at exactly the threshold is always 1 below it when the damage check runs. The old power pinned it there, which is why it showed full frost and never hurt anyone. apoli:freeze with no duration now sets the counter far enough past the threshold to survive the drain, so "frozen solid" means what it says.

Fixed — apoli:freeze evaluated its duration twice

A duration written as an expression was evaluated once to test its sign and again to apply it, so anything involving random(…) froze you for a different length than it checked. duration is also optional now.

Fixed — apoli:modify_fog powers fought over fields they did not set

Every active fog power used to claim every field. Two powers at the same priority meant the second one did nothing at all, even for fields the first one left alone — so {"s": 0, "v": 6} plus {"r": 0.4, "g": 0, "b": 0.5} drew thick fog in the wrong colour. Each field is now decided on its own, and a power only enters the contest for a field it actually sets.

Fixed — apoli:modify_fog fade-out could flash black

A fog power ending with no recorded vanilla colour faded towards black instead of towards the colour the game was about to draw.

Performance

  • Resolving fog no longer allocates a map and seven Optionals per tick; it folds into a reused buffer, and the power id is interned instead of rebuilt per call.
  • Fog interpolation returns the target itself once a fade has finished, so a settled fog allocates nothing per frame.

1.88.0

Added — reflective projectiles

  • apoli:fire_projectile, as a power type and as an entity action, takes reflective, max_bounces (default 4, -1 for no limit) and bounce_speed (default 1.0). A reflective projectile mirrors its velocity through the block face it hits and carries on flying instead of stopping.
  • Every bounce runs block_action_on_hit and the new bientity_action_on_bounce. bientity_action_on_miss is held back until the projectile actually stops, so "missed" still means missed.
  • Works on Apoli's own texture_location projectiles and on any vanilla or modded projectile fired through entity_type.

Added — apoli:modify_resource_change

A power type that scales the size of every change made to the holder's resources and cooldowns, rather than setting a value.

  • modify (gain, drain, both), resources, cooldowns and an optional resource pick what it touches.
  • Cooldowns are read as remaining ticks, so their natural countdown of one tick per tick is a drain — "modify": "drain" with multiply_total: 1 empties every cooldown twice as fast. Fractional rates are carried between ticks rather than rounded away.
  • Holders without the power skip the whole path; the resource-write hook only costs anything while some loaded pack uses it.

Added — apoli:source damage condition

Checks which power dealt the damage, and which source granted that power. With no fields it matches anything a power caused and nothing a sword, a fall or a creeper did. source matches the grant source, so "origins:layer/origin" means "anything the player's origin gave them". Attribution follows the power through delayed and nested actions.

Added — advancement triggers

  • apoli:power fires when a power is granted, with optional power and source.
  • apoli:resource fires when a resource or cooldown the player holds is written to, with optional resource and a value int range.

Added — voice chat

  • apoli:modify_speaking_range, the speaker-side counterpart to apoli:modify_hearing_range: it changes how far the holder's own voice carries. normal and whisper pick which channel. Speaking range resolves first and each listener's hearing range applies on top, so the two compose.
  • apoli:modify_hearing_range takes a bientity_condition, evaluated per speaker with the holder as the actor and the speaker as the target — "I hear my own team twice as far, everyone else normally". Pairwise ranges are resolved once a tick and only for players who are actually talking. A power that sets it is skipped for game sounds, which have no speaker to test.
  • Compat verified against Simple Voice Chat 2.6.24. Every API member Apoli uses is unchanged; the build still compiles against the 2.6.0 API so one jar works across the whole 2.6.x line.

Added — apoli:toggle turn-on / turn-off conditions

turn_on_condition and turn_off_condition gate the key, separately per direction — a state you can only enter while sneaking and only leave while standing. Neither gates apoli:toggle the entity action, and retain_state: false still forces the toggle off when the power's own condition stops holding, so a state can always be taken away from a holder who could not have let go of it themselves.


1.87.1

Fixed

  • apoli:custom_model_render in geometry mode rendered upside down and mirrored on custom projectiles, hanging below the projectile. Entity models are authored in a flipped space that a living entity's renderer sets up for free; the projectile renderer was missing it. The same .geo.json now looks identical on a player and on a projectile.

1.87.0

The scale system from 1.86.0 stored its values correctly but almost nothing acted on them. This release makes it work.

Fixed — the scale system did nothing visible

  • Hitboxes never scaled. The hitbox hook sat on a method that every player and mob overrides, so it never ran for either.
  • Models never scaled on players. The model hook sat on a method the player renderer overrides. Scaling now happens above every renderer, so it covers players, mobs, armour stands, item frames and modded entities alike. The entity's shadow scales with it.
  • apoli:scale powers did nothing on non-living entities, despite the power ticking them.

Fixed — held items

  • Held items were only scaled in first person, which is why apoli:held_item and apoli:base interacted so strangely. Both views now go through one path.
  • apoli:held_item is no longer built from apoli:base. In third person the item is drawn inside the entity's model and already follows it, so base was applying twice. apoli:held_item is the extra multiplier on top, in both views. This matches Pehkui.

Fixed — multiplayer

  • Scales are sent to every player tracking the entity, and re-sent on join, respawn and dimension change.
  • A scale now survives death — respawning keeps the size you had.
  • A ticks animation is sent once as its start, end and easing rather than as a value per tick, so a four-second shrink costs one packet and every client eases through it in step.

Performance

  • An entity whose size is settled does no per-tick work at all. The tick hook returns immediately unless an animation is in flight.
  • A scaled entity with no apoli:scale power keeps its resolved values until something actually changes them — granting the power, revoking it, suppressing it, or a /reload. Only expression-driven scales are recomputed every tick.
  • Render frames share one cached resolution instead of recomputing per frame.

Added

  • apoli:action_over_time accepts an Expression for interval, on the power and on each entry of actions. The gap is measured from the last firing and recomputed when the action runs, so it can follow a resource, an enchantment level, anything an expression can read.

    {
      "type": "apoli:action_over_time",
      "interval": "20 + min(enchantment[origins:water_protection, armor, sum], 8) * 6",
      "condition": { "type": "apoli:in_rain" },
      "entity_action": { "type": "apoli:damage", "amount": 2, "damage_type": "origins:hurt_by_water" }
    }
    

    An expression interval also fires on the tick the condition becomes true instead of up to one interval later. The trade: it checks the condition every tick, where a plain number checks once per interval. A plain number costs exactly what it did before.

  • scale_type is accepted as a spelling of scale_types on the apoli:scale power and entity action.

Changed

  • A malformed interval or onset_delay on apoli:action_over_time is now logged. It used to be swallowed and silently replaced with the default, so a typo in an expression turned into "every 20 ticks" with no trace.

1.86.0

Added — the scale system

Size changing, built into Apoli. No Pehkui required.

  • Nineteen scale types, each one axis of an entity's size, all starting at 1. apoli:base is the master dial; apoli:width and apoli:height hang off it; apoli:hitbox_* and apoli:model_* hang off those — so something can look enormous and still fit through a door, or the reverse. The non-geometry dials are built from the geometry ones: apoli:falling is one divided by apoli:motion, so a bigger entity takes more fall damage and a smaller one less, with nothing to wire up.
  • apoli:scale power — size for exactly as long as the power is held. Takes an Expression, so an entity can shrink as it is hurt and grow back as it heals with no resource plumbing.
  • apoli:scale entity action — size that sticks. Stored on the entity, survives relogs and dimension changes, and eases into place over a number of ticks with a choice of fourteen easings.
  • apoli:scale entity condition to read one back.
  • /apoli:scaleget, set, add, subtract, multiply, divide, power, reset, list. Works on any entity, not just players.
  • Pehkui compatibility: when Pehkui is installed, Apoli hands its values to Pehkui rather than applying them itself, so the two mods agree on one size instead of multiplying each other.

Added

  • apoli:solid_hitbox — makes an entity's hitbox solid the way a boat or a shulker is, so you can stand on it. Takes a bientity_condition, so it can be solid to some entities and not others.

Fixed

  • Expressions inside modify_damage_dealt read the wrong entity. "amount": "nbt[Health]" read the victim's health instead of the attacker's, and resource(...) read the victim's resources. Every modifier now evaluates against the entity that holds the power, with actor_ and target_ bound correctly. modify_projectile_damage got the same fix; modify_damage_taken was already correct.
  • apoli:prevent_elytra_flight did nothing. It was registered with a codec and had no runtime hook at all. It now blocks take-off and also stops an elytra mid-glide when its condition turns true.

1.85.0

Added — body parts

  • apoli:body_part damage condition. Use it inside apoli:modify_damage to react to where a hit landed. It casts the attacker's look, a mob's swing or a projectile's path against the posed limbs, falls back to the #apoli:body_part/* damage type tags, and then to a random limb for damage with no direction.
  • Part groups that move as one rigid piecearms, legs, upper, lower and whole, each about the joint that holds it on, with an optional pivot. Plus the regions hands, feet, chest, back and achilles_heel for anchors and hits.
  • One registry now backs every part name in the mod, so a name that works in one field works in all of them.

Added

  • apoli:energy_swirl — the charged-creeper swirl as a power, preconfigured, with a scroll_speed.
  • Expressions read enchantments and NBT: enchantment[id, slot, sum|max] and nbt[source, path, aggregator] (alias data[...]), both with target_ and actor_ forms. NBT can come from the entity, a target or actor, an item slot, a block entity or command storage.

Changed

  • Texture overlays render on any living entity, not only players.

1.84.0

Four features that parsed but did nothing, finished.

Fixed

  • apoli:prevent_entity_collision was codec-only. It now works, and it covers both the soft shove and the solid hitboxes of boats and shulkers. Either side's power counts.
  • apoli:keep_inventory was codec-only. It now keeps the configured slots through death. Default slots are 0–35 and 100–103 (no offhand), matching Apace.
  • apoli:phasing with render_type: blindness looked like it did nothing. Only the fog half existed, and the sky is drawn at a fixed distance where terrain fog never reaches it — so you got black fog under a bright sky. The sky is now covered too, and the in-wall test uses all eight corners of the hitbox instead of a single eye-block check, so standing half in a wall counts.
  • apoli:teleport_to_spawn with player_spawn: true could drop you into a broken or obstructed bed. It now validates a respawn point the same way dying does and lands on the stand-up position. It only ever showed up before your first death, because dying clears an unusable respawn point.

Changed

  • A summon that runs out of max_life_ticks now dies properly instead of quietly vanishing. apoli:action_on_death never fired on a timeout, so a pack that hands out summon slots and takes them back on death leaked one every time.

Added

  • actor_resource(id) and actor_has_power(id) expression functions — the missing counterpart of target_resource. Inside apoli:target_action, a bare resource id reads the target; these read the entity that ran the action.
Type
Name
Game Versions
Mod Loaders
Size
Downloads
Uploaded