Project icon Effectly
Effectly

The Universal Ability API Done Right

17
14 hours ago
LibraryUtilityAdventure

References

Updated Sep 3, 2026

References

> Commands

/ability add <effect_id> [value] [conditions...] [player]
/ability remove <effect_id> [player]
/ability list [player]
/ability available

Example Usage

/ability add Bouncy 2.5
/ability add move_speed 1.5 zone 3 swimming
/ability add health_regen 2 health_below 25 Steve
/ability remove Bouncy

> JSON-level configurations

Effects

Server/Effectly/Effects/<Effect_Id>.json

{
  "Handler": {
    "Id": "Thorns",
    "DamageCause": "Effectly_Thorns"
  },
  "Type": "Numeric",
  "Default": 1.0,
  "Min": 0.0,
  "Max": 3.0,
  "Description": "Shown by /ability available",
  "Conditions": [
    { "Type": "Swimming" },
    { "Type": "in_zone", "Param": 3 },
    { "Type": "in_zone", "ZoneIds": [1, 3] },
    { "Type": "health_below", "Param": 25 }
  ],
  "Triggers": {
    "OnLand": [
      {
        "Type": "Bounce",
        "Threshold": { "Magnitude": 1.5 },
        "Cooldown": 0.5,
        "Restitution": 4.0
      }
    ],
    "OnFall": [
      {
        "Type": "Heavy",
        "Reduction": 1.0
      }
    ]
  },
  "Enabled": true
}
{
  "Triggers": {
    "OnLand": [
      { "Type": "Bounce", "Threshold": { "Magnitude": 1.5 }, "Restitution": 4.0 }
    ]
  },
  "Type": "Numeric",
  "Default": 1.0,
  "Min": 0.0,
  "Max": 3.0
}

Conditions

Server/Effectly/Conditions/<Condition_Id>.json

{
  "Handler": {
    "Id": "health",
    "Target": "Self",
    "Comparison": "Below",
    "Threshold": 50
  },
  "Keyword": "health_below",
  "Description": "your health below %"
}
{
  "Handler": {
    "Id": "in_liquid",
    "State": "Swimming"
  },
  "Keyword": "swimming",
  "Description": "active while swimming"
}

> Tag-level configurations

Basic Effect

{
  "Tags": {
    "Effectly:Ability": ["Bouncy"]
  }
}

With Condition

{
  "Tags": {
    "Effectly:Ability": ["Bouncy::swimming"]
  }
}
{
  "Tags": {
    "Effectly:Ability": ["Bouncy::zone 3 health_below 25"]
  }
}

With Global Condition

{
  "Tags": {
    "Effectly:Ability": ["Bouncy", "Heavy"],
    "Effectly:Condition": ["zone 3"]
  }
}

With Multiplier

{
  "Tags": {
    "Effectly:Ability": ["Bouncy:2.5"]
  }
}

With All Three

{
  "Tags": {
    "Effectly:Ability": ["Bouncy:2.5:swimming", "Heavy:0.75"],
    "Effectly:Condition": ["zone 3"]
  }
}

> Metadata-level configurations

Basic Effect

stack = EquipmentAbilityMetadata.write(stack, Map.of("Bouncy", 1.0));

With Condition

With Global Condition

stack = stack.withMetadata(EquipmentAbilityMetadata.CONDITIONS_KEY,
        Codec.STRING_ARRAY, new String[] { "zone 3", "swimming" });

With Multiplier

stack = EquipmentAbilityMetadata.write(stack, Map.of("Bouncy", 2.5));

With All Three

stack = EquipmentAbilityMetadata.write(stack, Map.of("Bouncy", 2.5, "Heavy", 0.75));
stack = stack.withMetadata(EquipmentAbilityMetadata.CONDITIONS_KEY,
        Codec.STRING_ARRAY, new String[] { "zone 3" });

> Code-level configurations

Basic Effect

AbilityService.setAbility(playerUuid, "Bouncy", 1.0);

With Condition

AbilityService.setAbility(playerUuid, "Bouncy", 1.0,
        List.of(new AbilityConditionSpec("Swimming")));
AbilityService.setAbility(playerUuid, "Bouncy", 1.0, List.of(
        new AbilityConditionSpec("health_below", 25),
        new AbilityConditionSpec("in_zone", null, List.of(1, 3))));

With Global Condition

With Multiplier

AbilityService.setAbility(playerUuid, "Bouncy", 2.5);

With All Three

AbilityService.setAbility(playerUuid, "Bouncy", 2.5,
        List.of(new AbilityConditionSpec("Swimming")), "mymod:armor_set");
AbilityService.removeAbility(playerUuid, "Bouncy", "mymod:armor_set");

> Registering a new...

Effect

Server/Effectly/Effects/My_Effect.json

{
  "Triggers": {
    "OnLand": [
      { "Type": "Bounce", "Threshold": { "Magnitude": 1.5 }, "Restitution": 4.0 }
    ]
  },
  "Type": "Numeric",
  "Default": 1.0,
  "Min": 0.0,
  "Max": 3.0,
  "Description": "Bounce back upward when landing from a fall"
}

Trigger

TriggerRegistry.register(new Trigger(OnMyThingProducerSystem.TRIGGER,
        registry -> registry.registerSystem(new OnMyThingProducerSystem())));
public final class OnMyThingProducerSystem extends EntityTickingSystem<EntityStore> {

    public static final String TRIGGER = "OnMyThing";

    @Nonnull
    @Override
    public Query<EntityStore> getQuery() {
        return ActionHolderComponent.getComponentType();
    }

    @Override
    public void tick(
            float dt,
            int index,
            @Nonnull ArchetypeChunk<EntityStore> archetypeChunk,
            @Nonnull Store<EntityStore> store,
            @Nonnull CommandBuffer<EntityStore> commandBuffer) {
        var ref = archetypeChunk.getReferenceTo(index);
        if (ref == null || !ref.isValid()) return;
        var holder = archetypeChunk.getComponent(index, ActionHolderComponent.getComponentType());
        var playerRef = archetypeChunk.getComponent(index, PlayerRef.getComponentType());
        var transform = archetypeChunk.getComponent(index, TransformComponent.getComponentType());
        var world = store.getExternalData().getWorld();
        if (holder == null || playerRef == null || transform == null || world == null) return;

        double magnitude = 1.0;
        ActionDispatch.fire(TRIGGER, ref, store, commandBuffer, world,
                holder, magnitude, transform.getPosition(), null, null);
    }
}

Action

public final class MyAction extends Action {

    @Nonnull
    public static final String ID = "MyAction";

    @Nonnull
    public static final BuilderCodec<MyAction> CODEC = BuilderCodec
            .builder(MyAction.class, MyAction::new, Action.BASE_CODEC)
            .append(new KeyedCodec<>("Strength", Codec.DOUBLE),
                    (action, v) -> action.strength = v,
                    action -> action.strength)
            .add()
            .build();

    private double strength = 1.0;

    @Override
    public boolean execute(@Nonnull ActionContext context) {
        Velocity velocity = context.getStore().getComponent(
                context.getHolderRef(), Velocity.getComponentType());
        if (velocity == null) return false;
        velocity.addInstruction(
                new Vector3d(0, strength * context.getValue() * context.getMagnitude(), 0),
                null, ChangeVelocityType.Add);
        return true;
    }
}
ActionRegistry.register(MyAction.ID, MyAction.class, MyAction.CODEC);
{
  "Triggers": {
    "OnLand": [
      {
        "Type": "MyAction",
        "Threshold": { "Magnitude": 2.0 },
        "Cooldown": 1.0,
        "Strength": 3.0
      }
    ]
  }
}

Ability

public final class MyAbilityHandler implements AbilityHandler {

    @Nonnull
    public static final String ID = "My_Ability";

    @Nonnull
    @Override
    public String getId() {
        return ID;
    }

    @Override
    public void install(@Nonnull ComponentRegistryProxy<EntityStore> registry) {
        MyAbilityComponent.register(registry);
        registry.registerSystem(new MyAbilitySystem());
    }

    @Override
    public void grant(@Nonnull AbilityContext context, @Nonnull String abilityId, @Nonnull AbilityEntry entry) {
        if (context.getComponents().getComponent(context.getRef(), MyAbilityComponent.getComponentType()) != null) return;
        context.getComponents().putComponent(context.getRef(),
                MyAbilityComponent.getComponentType(), new MyAbilityComponent());
    }

    @Override
    public void revoke(@Nonnull AbilityContext context, @Nonnull String abilityId) {
        if (AbilityHandlerRegistry.holdsAnyFor(context, this)) return;
        context.getComponents().tryRemoveComponent(context.getRef(), MyAbilityComponent.getComponentType());
    }
}
AbilityHandlerRegistry.register(new MyAbilityHandler());
var active = AbilityConditionUtils.bestActiveForHandler(
        ref, store, world, MyAbilityHandler.ID);
if (active == null) return;
double value = active.value();
Powered byHytaleModding