Skip to main content

Powers

A power is the top-level unit in Apoli: something an entity has. Every power is a JSON object with a type, and the type decides what the power does.

The shape of a power

{
   "name":"Tough",
   "description":"You have more health than most.",
   "type":"apoli:attribute",
   "modifiers":[
      {
         "attribute":"minecraft:generic.max_health",
         "operation":"add_base_early",
         "value":10
      }
   ]
}

Every power type shares these seven fields. All other fields depend on the type.

FieldTypeDefaultDescription
typeIdentifierrequiredWhich power type this is.
nameText ComponentautoThe display name. Falls back to a translation key.
descriptionText ComponentautoThe description shown in menus. Falls back to a translation key.
conditionEntity ConditionoptionalThe power is only active while this passes.
hiddenBooleanfalseHides the power from the origin and power screens.
tagsstring or list of strings[]Free-form labels. Actions that work on powers can select powers by tag instead of by ID. See Tagging powers.
load_conditionMeta conditionoptionalChecked during the reload. If it fails, the power is skipped before it is parsed. See Skipping a power at load time.

The top-level condition makes any power conditional without changing its type. With a condition of apoli:sneaking, the power only works while the entity sneaks.

Skipping a power at load time

condition decides whether a loaded power is active. load_condition decides whether the power is read at all.

FieldTypeDefaultDescription
load_conditionMeta conditionoptionalChecked once during the reload, before the rest of the power reaches a codec. If it doesn't hold, the power is skipped entirely.

The order matters. A power whose load_condition fails is dropped before its other fields are parsed, so those fields don't have to parse on this game version or with this set of mods. A condition can't do that: the power has to load first, and a field the codec rejects takes the whole power with it.

Only condition types that need no context are allowed here, because a reload has no entity, item or block to test:

inverted works as it does on any other condition. Any other type is rejected with a line in the log and the power is kept, so a typo never deletes a power without telling you.

{
  "type": "apoli:multiple",
  "dragon_wings": {
    "type": "apoli:wings",
    "wings_type": "icarus:purple_dragon_wings",
    "load_condition": { "type": "apoli:mod_loaded", "mod": "icarus" }
  },
  "elytra": {
    "type": "apoli:elytra_flight",
    "render_elytra": false,
    "load_condition": { "type": "apoli:mod_loaded", "mod": "icarus", "inverted": true }
  }
}

On an apoli:multiple sub-power, load_condition applies to that sub-power alone. A dropped sub-power is left out of the parent's sub_powers list, so nothing goes looking for it. Put load_condition at the top level of the apoli:multiple to apply it to the whole bundle.

Origins reads the same field on files in origins/ and origin_layers/, so an origin can exist only where its powers do.

Kinds of power

There are over a hundred power types, and each one has its own page in the Powers section of the sidebar. They fall into a few loose groups:

GroupWhat they doExamples
Attributes and statsChange numbersapoli:attribute, apoli:modify_damage
MovementFlight, climbing, phasingapoli:creative_flight, apoli:climbing
Prevention and immunityTake an ability awayapoli:invulnerability, apoli:prevent_death
Action powersRun actions on a triggerapoli:action_on_hit, apoli:action_over_time
Resources and stateStore a number, a timer or a groupapoli:resource, apoli:cooldown
RenderingColours, overlays, particlesapoli:model_color, apoli:particle
Items and inventoriesContainers, recipes, projectilesapoli:inventory, apoli:fire_projectile
StructureBundle powers or act as a flagapoli:multiple, apoli:simple

Granting powers

A power file does nothing on its own. Something has to give the power to an entity:

Once an entity has the power, Apoli applies its effect. If the power has a top-level condition, it is only active while that condition holds.

Global powers

A file in data/<namespace>/global_powers/<name>.json grants powers to entities by entity type, with no origin and no command. Every matching entity gets the powers when it loads into the world.

{
  "entity_types": [
    "minecraft:pig",
    "#minecraft:undead"
  ],
  "powers": [
    "example:tough"
  ],
  "order": 0
}
FieldTypeDefaultDescription
entity_typesIdentifier, or an array of themall entitiesThe entity types that get the powers. Entries starting with # are entity type tags. Leave the field out to match every entity.
powersArray of IdentifierrequiredThe powers to grant.
replaceBooleanfalseDiscards the powers that every set with an earlier order gave this entity type, then adds these.
orderInteger0Which sets apply first. Ties are broken by file ID.
loading_priorityInteger0When two data packs define the same file ID, the higher value wins.

The powers come from the source apoli:global, so they sit alongside anything an origin or a command granted, and they are revoked cleanly when a set stops matching.

warning

Global power sets are the most expensive feature in Apoli when misused. A set with no entity_types grants its powers to every entity in the world, including arrows, dropped items and area effect clouds, and each of those entities then ticks each power. Limit sets to the types or tags you need, and think hard before putting an apoli:action_over_time in one.

Which powers an entity type receives is worked out once per entity type each time data packs load, and the result is cached, so spawning a thousand zombies doesn't evaluate the sets a thousand times. When no global power sets exist, the whole system costs one boolean check each time an entity loads.

Sets are evaluated again on /reload, and every loaded entity is checked against the new result. Powers whose set no longer matches are revoked in the same pass.

Tagging powers

tags puts one or more free-form labels on a power. Nothing reads them by default. They let actions that work on powers name a group of powers instead of listing every ID:

{
    "type": "apoli:action_on_key_press",
    "tags": ["copyable_move"],
    "entity_action": {
        "type": "apoli:execute_command",
        "command": "say hi"
    }
}

Three actions take a tags list and act on every power the entity holds that has one of those tags:

Two conditions accept tags too:

With tags, "copy one move from whatever origin you touched" is a single action. Tag the moves once and the action finds them, including powers a pack adds later.

Tags are plain strings, matched exactly and case-sensitively. They are not identifiers and get no namespace, so on a server with several packs, prefix your tags with something of your own (mypack.copyable) so two packs don't use the same word for different things.