Skip to content

API Usage

NagasonicDev edited this page Aug 16, 2026 · 1 revision

API Usage

Alkatraz exposes a Java API that external plugins can depend on to create custom spells, register magic items, hook into the progression system, and extend the magic item pipeline with custom components, conditions, effects, and attribute sources.

Add the Maven repository and dependency to your plugin:

Maven repository:

<repository>
    <id>repsy</id>
    <name>Alkatraz</name>
    <url>https://repo.repsy.io/nagasonic/alkatrazapi</url>
</repository>

Maven dependency:

<dependency>
    <groupId>me.nagasonic</groupId>
    <artifactId>alkatraz-api</artifactId>
    <version>0.9.1</version>
    <scope>provided</scope>
</dependency>

Gradle:

compileOnly 'me.nagasonic:alkatraz-api:0.9.1'

All API types live in the me.nagasonic.alkatraz.api package.


Table of Contents

  1. Plugin Setup
  2. Creating a Custom Spell
  3. Registering Spells
  4. Listening to Spell Events
  5. Player Profile & Stats
  6. Magic Item Registries
  7. Item Definitions
  8. Magic Item Instances
  9. Attributes & Attribute Sources
  10. Components & Component Handlers
  11. Conditions
  12. Effects
  13. Triggers & Trigger Bindings
  14. Engravings
  15. Equipment & Virtual Slots
  16. Research Progress
  17. Arcane Knowledge Sources
  18. Mob API
  19. Recipe API
  20. DOM Utilities
  21. Permissions
  22. Complete Example Plugin

Plugin Setup

Your plugin's plugin.yml must declare a dependency on Alkatraz:

name: MyMagicPlugin
version: '1.0'
main: com.example.MyMagicPlugin
api-version: '1.19'
depend: [Alkatraz]

In your main class, verify Alkatraz is loaded before using any API:

public class MyMagicPlugin extends JavaPlugin {
    @Override
    public void onEnable() {
        if (Bukkit.getPluginManager().getPlugin("Alkatraz") == null) {
            getLogger().severe("Alkatraz not found! Disabling.");
            Bukkit.getPluginManager().disablePlugin(this);
            return;
        }
        // Safe to use Alkatraz API from here
    }
}

Creating a Custom Spell

Extend Spell and implement all abstract methods:

package com.example.spells;

import me.nagasonic.alkatraz.api.Element;
import me.nagasonic.alkatraz.api.events.SpellPrepareEvent;
import me.nagasonic.alkatraz.api.spells.Spell;
import org.bukkit.*;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Mob;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;

import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;

public class FireballSpell extends Spell {

    private double baseDamage;
    private double range;

    public FireballSpell(String type) {
        super(type);
    }

    @Override
    public void loadConfiguration() {
        this.id = "fireball";
        this.displayName = "Fireball";
        this.code = "RRRLL";
        this.element = Element.FIRE;
        this.cost = 20;
        this.requiredCircle = 1;
        this.enabled = true;
        this.baseDamage = 4.0;
        this.range = 20.0;
        this.cooldown = 3;
        this.castTime = 1.0;
        this.level = 1;
        this.maxMastery = 10;
        this.description = List.of(
                "&7Fires a magical fireball",
                "&7Deals fire damage on impact");
        this.masteryBarColor = BarColor.RED;
        this.guiItem = new ItemStack(Material.BLAZE_ROD);
    }

    @Override
    public void castAction(Player caster, ItemStack wand) {
        var direction = caster.getEyeLocation().getDirection();
        var start = caster.getEyeLocation();
        var end = start.clone().add(direction.clone().multiply(range));
        var world = start.getWorld();

        var step = new AtomicInteger(0);
        Bukkit.getScheduler().runTaskTimer(this, task -> {
            if (step.get() > 20) { task.cancel(); return; }
            double t = step.getAndIncrement() / 20.0;
            var loc = start.clone().add(
                    end.clone().subtract(start).toVector().multiply(t));
            world.spawnParticle(Particle.FLAME, loc, 5, 0.1, 0.1, 0.1, 0.02);

            world.getNearbyEntities(loc, 0.5, 0.5, 0.5).stream()
                    .filter(e -> e instanceof LivingEntity && !e.equals(caster))
                    .map(e -> (LivingEntity) e)
                    .findFirst()
                    .ifPresent(entity -> {
                        entity.damage(baseDamage, caster);
                        task.cancel();
                    });
        }, 0L, 1L);
    }

    @Override
    public void mobCastAction(Mob caster, ItemStack wand) {
        if (caster.getTarget() == null) return;
        var target = caster.getTarget();
        target.damage(baseDamage, caster);
    }

    @Override
    public int circleAction(LivingEntity caster, SpellPrepareEvent event) {
        return Bukkit.getScheduler().scheduleSyncRepeatingTask(this, () -> {
            if (event.isCancelled()) return;
            var loc = caster.getEyeLocation().add(
                    caster.getEyeLocation().getDirection().multiply(2));
            loc.getWorld().spawnParticle(Particle.FLAME, loc, 3,
                    0.3, 0.3, 0.3, 0);
        }, 0L, 2L);
    }

    @Override
    public ItemStack getSpellBook() {
        var book = new ItemStack(Material.BOOK);
        var meta = book.getItemMeta();
        if (meta != null) {
            meta.setDisplayName("Fireball Tome");
            meta.setLore(List.of("Teaches the Fireball spell"));
            book.setItemMeta(meta);
        }
        return book;
    }

    @Override
    public boolean canMobCast(Mob mob) {
        return mob.getTarget() != null;
    }
}

Abstract Methods

Method Description
loadConfiguration() Set all spell fields (id, name, code, element, cost, etc.)
castAction(Player, ItemStack) What happens when a player casts the spell
mobCastAction(Mob, ItemStack) What happens when a mob casts the spell
circleAction(LivingEntity, SpellPrepareEvent) Visual effect during the casting circle; return a scheduler task ID
getSpellBook() The item given when this spell is discovered

Optional Override

Method Description
canMobCast(Mob) Whether the mob can cast (default: true)

Spell Fields Reference

Field Type Description
id String Unique identifier
displayName String Player-facing name
code String 5-character cast code (R=Right, L=Left, S=Swap)
element Element FIRE, WATER, EARTH, AIR, LIGHT, DARK, or NONE
cost int Mana cost
requiredCircle int Minimum circle level to cast
cooldown long Cooldown in seconds
castTime double Cast time in seconds
level int Spell level
maxMastery int Maximum mastery level
enabled boolean Whether the spell is active
description List<String> Lore lines (supports & color codes)
masteryBarColor BarColor Boss bar color during casting
guiItem ItemStack Item shown in spell menus

Registering Spells

Use the static SpellAPI class to register spells at startup:

var spell = new FireballSpell("FIREBALL");
spell.loadConfiguration();
SpellAPI.registerSpell(spell);

SpellAPI Reference

Method Returns Description
registerSpell(Spell) void Register a spell instance
getSpell(String id) Spell (nullable) Look up a spell by ID
getSpellByCode(String code) Spell (nullable) Look up a spell by cast code
getAllSpells() Map<String, Spell> All registered spells
canCast(Player, Spell) boolean Checks enabled + circle level
getProfile(OfflinePlayer) MagicProfileView (nullable) Get a player's magic profile
getProfile(UUID) MagicProfileView (nullable) Get a player's magic profile by UUID
saveProfile(MagicProfileView) void Persist a profile

Listening to Spell Events

Alkatraz fires Bukkit events you can listen to:

Event Description
SpellPrepareEvent Fired when any entity begins casting (Cancellable). Access getCaster(), getSpell(), getWand()
PlayerSpellPrepareEvent Same as above, but getCaster() is guaranteed to return a Player
CastEvent Fired when any entity fires a spell (Cancellable). Same accessors
PlayerCastEvent Same as above, but getCaster() is guaranteed to return a Player
public class MyListener implements Listener {

    @EventHandler
    public void onSpellPrepare(SpellPrepareEvent event) {
        LivingEntity caster = event.getCaster();
        Spell spell = event.getSpell();
        ItemStack wand = event.getWand();
    }

    @EventHandler
    public void onPlayerCast(PlayerCastEvent event) {
        event.getCaster().sendMessage(
            "You cast " + event.getSpell().getDisplayName());
    }
}

Player Profile & Stats

Access a player's magic profile via SpellAPI.getProfile(player):

MagicProfileView profile = SpellAPI.getProfile(player);
if (profile != null) {
    int circle = profile.getCircleLevel();
    double mana = profile.getMana();
    double maxMana = profile.getMaxMana();
    double affinity = profile.getAffinity(Element.FIRE);
    double resistance = profile.getResistance(Element.FIRE);
    boolean discovered = profile.hasDiscoveredSpell("fireball");
    int mastery = profile.getSpellMastery("fireball");
}

Core Stats

Method Type Description
getCircleLevel() / setCircleLevel(int) int Current magic circle (1-9)
getStatPoints() / setStatPoints(int) int Unspent stat points
getMana() / setMana(double) double Current mana
getMaxMana() / setMaxMana(double) double Maximum mana
getManaRegeneration() double Mana regen rate
getExperience() double Total experience
getArcaneKnowledge() double Arcane knowledge
getResearchPoints() int Unspent research points

Element Stats

Method Returns Description
getPoints(Element) int Invested stat points in an element
getAffinity(Element) double Elemental affinity (damage bonus)
getResistance(Element) double Elemental resistance

Spell Data

Method Returns Description
hasDiscoveredSpell(String) boolean Whether the player knows a spell
getAllDiscoveredSpellTypes() Collection<String> All discovered spell IDs
getSpellMastery(String) int Mastery level of a spell

Research

Method Returns Description
hasCompletedResearch(String) boolean Research completion check
getCompletedResearchIds() Collection<String> All completed research IDs
hasStartedResearch(String) boolean Whether research was started

Boolean Flags

Method Description
canCast() / setCanCast(boolean) Whether the player can cast
isCasting() / setCasting(boolean) Whether the player is currently casting
isStealth() / setStealth(boolean) Stealth mode

Magic Item Registries

The MagicItemRegistries class holds 7 global registries for magic item types:

import me.nagasonic.alkatraz.api.magic.registry.MagicItemRegistries;
Registry Type Description
ITEM_DEFINITIONS Registry<ItemDefinition> Magic item type definitions
COMPONENT_TYPES Registry<ComponentType> Component type definitions
ENGRAVING_DEFINITIONS Registry<EngravingDefinition> Engraving/mod definitions
TRIGGER_TYPES Registry<TriggerType> Trigger type definitions
EFFECT_TYPES Registry<EffectType> Effect type definitions
CONDITION_TYPES Registry<ConditionType> Condition type definitions
ATTRIBUTE_TYPES Registry<AttributeType> Custom attribute type definitions

Registry Methods

// Register an entry (throws on duplicate)
MagicItemRegistries.ITEM_DEFINITIONS.register(myDefinition);

// Look up by key (Optional)
var opt = MagicItemRegistries.ITEM_DEFINITIONS.get(
    MagicKeys.alkatraz("my_item"));

// Look up or throw
var def = MagicItemRegistries.ITEM_DEFINITIONS.require(
    MagicKeys.alkatraz("my_item"));

// Check registration
boolean exists = MagicItemRegistries.ITEM_DEFINITIONS.isRegistered(
    MagicKeys.alkatraz("my_item"));

// Iterate all
Collection<ItemDefinition> all = MagicItemRegistries.ITEM_DEFINITIONS.values();

Namespaced Keys

All magic item registry entries use NamespacedKey. The utility class MagicKeys provides helpers:

import me.nagasonic.alkatraz.api.magic.registry.MagicKeys;

// Create a key in the alkatraz namespace
NamespacedKey key = MagicKeys.alkatraz("my_item");

// Parse from string ("alkatraz:my_item" or bare "my_item")
Optional<NamespacedKey> parsed = MagicKeys.parse("alkatraz:my_item");

// Format for display
String formatted = MagicKeys.format(key); // "alkatraz:my_item"

Item Definitions

An ItemDefinition describes a complete magic item type:

import me.nagasonic.alkatraz.api.magic.definition.ItemDefinition;
import me.nagasonic.alkatraz.api.magic.definition.ItemVisual;
import me.nagasonic.alkatraz.api.magic.trigger.TriggerBinding;
import me.nagasonic.alkatraz.api.magic.registry.MagicKeys;
import me.nagasonic.alkatraz.api.magic.registry.MagicItemRegistries;
import org.bukkit.Material;

// 1. Create the visual
var visual = new ItemVisual(
    Material.DIAMOND_SWORD,
    "&b&lMagic Sword",
    List.of("&7A powerful magic weapon"),
    0,          // customModelData
    false,      // unbreakable
    false,      // hideAttributes
    null        // dyeColor
);

// Or use the convenience factory:
var visual = ItemVisual.of(Material.DIAMOND_SWORD, "&bMagic Sword", List.of("&7Lore"));

// 2. Create trigger bindings
var bindings = List.of(new TriggerBinding(
    MagicKeys.alkatraz("on_equip"),   // trigger type
    List.of(),                         // conditions
    List.of(),                         // effects
    0                                  // priority
));

// 3. Build the definition
var definition = new ItemDefinition(
    MagicKeys.alkatraz("magic_sword"),        // key
    visual,                                     // visual
    List.of(MagicKeys.alkatraz("equipment")),   // components
    Map.of(MagicKeys.alkatraz("spell_power"), 10.0), // attributes
    Map.of(),                                    // vanilla attributes
    bindings,                                    // trigger bindings
    Map.of("tier", 1)                           // static config
);

// 4. Register it
MagicItemRegistries.ITEM_DEFINITIONS.register(definition);

Magic Item Instances

A MagicItemInstance represents a specific item with its own modifiers, engravings, and data:

import me.nagasonic.alkatraz.api.magic.instance.MagicItemInstance;
import me.nagasonic.alkatraz.api.magic.registry.MagicKeys;

// Create a default instance
var instance = MagicItemInstance.createDefault(
    MagicKeys.alkatraz("magic_sword"));

// Add modifiers (engravings)
instance.addModifier(MagicKeys.alkatraz("fire_rune"));

// Store progression data
instance.putProgression("uses", 42);

// Store custom plugin data
instance.putCustomData("enchanted_by", "MyPlugin");

// Access fields
UUID id = instance.instanceId();
NamespacedKey defKey = instance.definitionKey();
List<NamespacedKey> mods = instance.modifiers();
List<Engraving> engs = instance.engravings();
Map<String, Object> prog = instance.progression();
Map<String, Object> custom = instance.customData();

// Copy for independent mutation
var copy = instance.copy();

Attributes & Attribute Sources

The attribute system resolves custom numeric values (like spell_power, max_mana) by collecting contributions from multiple sources.

Attribute Types

Register custom attribute types:

import me.nagasonic.alkatraz.api.magic.attribute.AttributeType;
import me.nagasonic.alkatraz.api.magic.registry.MagicKeys;
import me.nagasonic.alkatraz.api.magic.registry.MagicItemRegistries;

var attrType = new AttributeType(
    MagicKeys.alkatraz("spell_power"),
    0.0,          // default value
    "Spell Power" // display name
);
MagicItemRegistries.ATTRIBUTE_TYPES.register(attrType);

Attribute Sources

Implement AttributeSource to contribute attribute values dynamically:

import me.nagasonic.alkatraz.api.magic.attribute.AttributeSource;
import me.nagasonic.alkatraz.api.magic.attribute.AttributeContribution;
import me.nagasonic.alkatraz.api.magic.trigger.TriggerContext;
import me.nagasonic.alkatraz.api.SpellAPI;
import org.bukkit.entity.LivingEntity;
import org.bukkit.entity.Player;

import java.util.Collection;
import java.util.List;

public class CircleBonusSource implements AttributeSource {

    @Override
    public AttributeContribution.AttributeSourceType sourceType() {
        return AttributeContribution.AttributeSourceType.BUFF;
    }

    @Override
    public Collection<AttributeContribution> collect(
            LivingEntity entity, TriggerContext context) {
        if (!(entity instanceof Player player)) return List.of();

        var profile = SpellAPI.getProfile(player);
        if (profile == null) return List.of();
        int circle = profile.getCircleLevel();
        if (circle <= 1) return List.of();

        double bonus = (circle - 1) * 2.0;
        double manaBonus = (circle - 1) * 10.0;

        return List.of(
            new AttributeContribution(
                MagicKeys.alkatraz("spell_power"),
                bonus,
                AttributeContribution.AttributeOperation.ADD,
                sourceType(),
                0),
            new AttributeContribution(
                MagicKeys.alkatraz("max_mana"),
                manaBonus,
                AttributeContribution.AttributeOperation.ADD,
                sourceType(),
                0)
        );
    }
}

Registering and Querying Attributes

import me.nagasonic.alkatraz.api.magic.attribute.AttributeService;

// Register the source
AttributeService.getInstance().registerSource(new CircleBonusSource());

// Query a single attribute
double power = AttributeService.getInstance().get(
    player, MagicKeys.alkatraz("spell_power"));

// Get a full snapshot
var ctx = TriggerContext.empty(player);
var snapshot = AttributeService.getInstance().snapshot(player, ctx);
double val = snapshot.get(MagicKeys.alkatraz("spell_power"), 0.0);
Map<NamespacedKey, Double> all = snapshot.asMap();

AttributeContribution Operations

Operation Description
ADD Added to the base value
MULTIPLY Multiplied against the accumulated value
SET Overrides the value (takes priority over base)

AttributeSource Types

Source Type Description
BASE Base entity stats
DEFINITION From the item definition
MODIFIER From engravings/modifiers
EQUIPMENT From equipped items
SKILL From skill investments
BUFF From temporary buffs
TEMPORARY Other temporary sources

Components & Component Handlers

Components are named features attached to item definitions. A ComponentHandler provides the runtime behavior.

Defining a Component Type

import me.nagasonic.alkatraz.api.magic.component.ComponentType;
import me.nagasonic.alkatraz.api.magic.registry.MagicKeys;

var type = new ComponentType.Builder()
    .key(MagicKeys.alkatraz("lucky_charm"))
    .description("A charm that brings good luck")
    .defaultConfig(Map.of("luck_amplifier", 1.0))
    .build();

Implementing a Component Handler

import me.nagasonic.alkatraz.api.magic.component.ComponentHandler;
import me.nagasonic.alkatraz.api.magic.definition.ItemDefinition;
import me.nagasonic.alkatraz.api.magic.instance.MagicItemInstance;
import me.nagasonic.alkatraz.api.magic.trigger.TriggerContext;
import org.bukkit.entity.Player;
import org.bukkit.event.player.PlayerInteractEvent;
import org.bukkit.inventory.ItemStack;

public class LuckyCharmHandler implements ComponentHandler {

    @Override
    public ComponentType type() {
        return new ComponentType.Builder()
            .key(MagicKeys.alkatraz("lucky_charm"))
            .description("A charm that brings good luck")
            .defaultConfig(Map.of("luck_amplifier", 1.0))
            .build();
    }

    @Override
    public void onEquip(Player player, ItemStack stack,
                        MagicItemInstance instance, ItemDefinition definition) {
        player.sendMessage("You feel lucky wielding " + definition.getKey().getKey());
    }

    @Override
    public void onUnequip(Player player, ItemStack stack,
                          MagicItemInstance instance, ItemDefinition definition) {
        player.sendMessage("Your lucky charm goes quiet.");
    }

    @Override
    public void onInteract(PlayerInteractEvent event, ItemStack stack,
                           MagicItemInstance instance, ItemDefinition definition) {
        event.getPlayer().sendMessage("Your lucky charm tingles!");
    }

    @Override
    public void onTrigger(TriggerContext context, ItemStack stack,
                          MagicItemInstance instance, ItemDefinition definition) {
        context.playerActor().ifPresent(player ->
            player.sendMessage("Lucky charm trigger activated!"));
    }
}

Available Hook Methods

Method Description
onEquip(Player, ItemStack, MagicItemInstance, ItemDefinition) When the item is equipped
onUnequip(Player, ItemStack, MagicItemInstance, ItemDefinition) When the item is unequipped
onInteract(PlayerInteractEvent, ItemStack, MagicItemInstance, ItemDefinition) On player interact
onTrigger(TriggerContext, ItemStack, MagicItemInstance, ItemDefinition) When a trigger fires
onSwap(PlayerSwapHandItemsEvent, ItemStack, MagicItemInstance, ItemDefinition) On hand swap
onDrop(PlayerDropItemEvent, ItemStack, MagicItemInstance, ItemDefinition) On item drop
onDeath(PlayerDeathEvent, Player, ItemStack, MagicItemInstance, ItemDefinition) On player death

Registering a Component Handler

import me.nagasonic.alkatraz.api.magic.component.ComponentHandlerRegistry;

ComponentHandlerRegistry.register(new LuckyCharmHandler());

// Query
Optional<ComponentHandler> handler = ComponentHandlerRegistry.get(
    MagicKeys.alkatraz("lucky_charm"));
boolean hasIt = ComponentHandlerRegistry.hasHandler(
    MagicKeys.alkatraz("lucky_charm"));

// Get the built-in wand handler
Optional<ComponentHandler> wand = ComponentHandlerRegistry.getWandHandler();

Conditions

A Condition is a boolean test evaluated before a trigger's effects run.

import me.nagasonic.alkatraz.api.magic.condition.Condition;
import me.nagasonic.alkatraz.api.magic.trigger.TriggerContext;

public class CircleLevelCondition implements Condition {

    private final int minCircle;

    public CircleLevelCondition(int minCircle) {
        this.minCircle = minCircle;
    }

    @Override
    public boolean test(TriggerContext context) {
        return context.playerActor().map(player -> {
            var profile = me.nagasonic.alkatraz.api.SpellAPI.getProfile(player);
            return profile != null && profile.getCircleLevel() >= minCircle;
        }).orElse(false);
    }
}

Registering a Condition Type

To support YAML-driven conditions, register a ConditionType with a factory:

import me.nagasonic.alkatraz.api.magic.condition.ConditionType;
import me.nagasonic.alkatraz.api.magic.registry.MagicKeys;
import me.nagasonic.alkatraz.api.magic.registry.MagicItemRegistries;

var condType = new ConditionType(
    MagicKeys.alkatraz("circle_check"),
    config -> {
        int min = config.containsKey("minCircle")
            ? ((Number) config.get("minCircle")).intValue() : 1;
        return new CircleLevelCondition(min);
    });
MagicItemRegistries.CONDITION_TYPES.register(condType);

Effects

An Effect runs when a trigger fires (if all conditions pass).

import me.nagasonic.alkatraz.api.magic.effect.Effect;
import me.nagasonic.alkatraz.api.magic.trigger.TriggerContext;
import org.bukkit.Sound;

public class AnnounceEffect implements Effect {

    private final String title;
    private final String subtitle;

    public AnnounceEffect(String title, String subtitle) {
        this.title = title;
        this.subtitle = subtitle;
    }

    @Override
    public void execute(TriggerContext context) {
        context.playerActor().ifPresent(player -> {
            player.sendTitle(title, subtitle, 10, 40, 10);
            player.playSound(player.getLocation(),
                Sound.ENTITY_PLAYER_LEVELUP, 1.0f, 1.0f);
        });
    }
}

Registering an Effect Type

import me.nagasonic.alkatraz.api.magic.effect.EffectType;
import me.nagasonic.alkatraz.api.magic.registry.MagicKeys;
import me.nagasonic.alkatraz.api.magic.registry.MagicItemRegistries;

var effType = new EffectType(
    MagicKeys.alkatraz("custom_announce"),
    config -> {
        String title = config.getOrDefault("title", "&5&lCUSTOM").toString();
        String subtitle = config.getOrDefault("subtitle", "Effect!").toString();
        return new AnnounceEffect(title, subtitle);
    });
MagicItemRegistries.EFFECT_TYPES.register(effType);

Triggers & Trigger Bindings

A TriggerType defines when effects fire. A TriggerBinding connects a trigger type to conditions and effects.

Built-in Trigger Types

Key Description
alkatraz:on_equip When the item is equipped
alkatraz:on_spell_cast When a spell is cast
alkatraz:on_spell_hit When a spell hits a target
alkatraz:on_kill When an entity is killed

Registering a Custom Trigger Type

import me.nagasonic.alkatraz.api.magic.trigger.TriggerType;
import me.nagasonic.alkatraz.api.magic.registry.MagicKeys;
import me.nagasonic.alkatraz.api.magic.registry.MagicItemRegistries;

var triggerType = new TriggerType(
    MagicKeys.alkatraz("on_test_event"),
    "Fired by the test plugin");
MagicItemRegistries.TRIGGER_TYPES.register(triggerType);

Creating Trigger Bindings

import me.nagasonic.alkatraz.api.magic.trigger.TriggerBinding;

var binding = new TriggerBinding(
    MagicKeys.alkatraz("on_equip"),           // trigger type key
    List.of(new CircleLevelCondition(3)),      // conditions
    List.of(new AnnounceEffect("&aEquipped!", "You feel powerful")), // effects
    10                                         // priority
);

Trigger bindings are attached to ItemDefinition or EngravingDefinition via their triggers parameter. The core's TriggerPipeline evaluates bindings when triggers fire.

Trigger Context

The TriggerContext carries runtime data through the pipeline:

import me.nagasonic.alkatraz.api.magic.trigger.TriggerContext;

// Full constructor
var ctx = new TriggerContext(
    player,                   // actor
    target,                   // target
    MagicKeys.alkatraz("on_test_event"), // trigger type
    magicItemInstance,        // source item
    EquipmentSlot.MAIN_HAND,  // equipment slot
    Map.of("param", "value")  // parameters
);

// Convenience
var empty = TriggerContext.empty(player);

// Access
LivingEntity actor = ctx.actor();
LivingEntity target = ctx.target();
Optional<Player> pActor = ctx.playerActor();
MagicItemInstance srcItem = ctx.sourceItem();
Object val = ctx.parameter("param");
ctx.setParameter("key", "value");
ctx.setCancelled(true);

// Derivation
var newCtx = ctx.withSource(otherInstance, EquipmentSlot.OFF_HAND);

Trigger Events

Internal trigger events (not Bukkit events) are fired by the core pipeline:

Class Key Convenience Methods
SpellCastTriggerEvent alkatraz:on_spell_cast caster()
SpellHitTriggerEvent alkatraz:on_spell_hit caster(), hitTarget()
EquipTriggerEvent alkatraz:on_equip player()
EntityKilledTriggerEvent alkatraz:on_kill killer(), victim()

Engravings

An EngravingDefinition (also called a "modifier" or "rune") is an upgrade that can be applied to magic items. It provides attributes, conditions, and effects just like item definitions.

import me.nagasonic.alkatraz.api.magic.modifier.EngravingDefinition;
import me.nagasonic.alkatraz.api.magic.definition.ItemVisual;
import me.nagasonic.alkatraz.api.magic.registry.MagicKeys;
import me.nagasonic.alkatraz.api.magic.registry.MagicItemRegistries;
import org.bukkit.Material;

var visual = new ItemVisual(
    Material.ENCHANTED_BOOK,
    "&5&lFire Rune",
    List.of("&7Adds fire power to weapons"),
    0, false, false, null);

var engraving = new EngravingDefinition(
    MagicKeys.alkatraz("fire_rune"),
    visual,
    Map.of(MagicKeys.alkatraz("spell_power"), 5.0),  // attributes
    List.of(new CircleLevelCondition(2)),               // conditions
    List.of(new AnnounceEffect("&6&lFire!", "&cPower surges")), // effects
    List.of("weapon"),                                  // allowed item types
    Map.of("max_stack", 3, "destroy_on_remove", false)  // config
);

MagicItemRegistries.ENGRAVING_DEFINITIONS.register(engraving);

Equipment & Virtual Slots

Equipment Slots

The EquipmentSlot class extends vanilla slots with virtual slots for accessories:

Constant Virtual? Description
MAIN_HAND No Vanilla main hand
OFF_HAND No Vanilla off hand
HEAD No Helmet slot
CHEST No Chestplate slot
LEGS No Leggings slot
FEET No Boots slot
BODY No Body slot
RING Yes Ring accessory
NECKLACE Yes Necklace accessory
BRACELET Yes Bracelet accessory
PENDANT Yes Pendant accessory

Create custom virtual slots:

var customSlot = EquipmentSlot.of("test_custom_slot");

Virtual Slot Resolvers

A VirtualSlotResolver finds the item in a player's inventory that fills a virtual slot:

import me.nagasonic.alkatraz.api.magic.equipment.VirtualSlotResolver;
import org.bukkit.Material;
import org.bukkit.entity.Player;
import org.bukkit.inventory.ItemStack;

public class TokenResolver implements VirtualSlotResolver {
    @Override
    public ItemStack resolve(Player player) {
        for (ItemStack item : player.getInventory().getContents()) {
            if (item != null && item.hasItemMeta()
                    && item.getItemMeta().hasDisplayName()
                    && item.getItemMeta().getDisplayName().contains("MY_TOKEN")) {
                return item;
            }
        }
        return new ItemStack(Material.AIR);
    }
}

Research Progress

External plugins can register research progress providers so Alkatraz knows when custom research objectives are completed:

import me.nagasonic.alkatraz.api.progression.research.ResearchProgressRegistry;

// Register a provider for your namespace
ResearchProgressRegistry.register("myplugin", (player, researchId) -> {
    // Return true if the player has completed this research
    var profile = SpellAPI.getProfile(player);
    return profile != null && profile.hasCompletedResearch(researchId);
});

// Check completion
boolean done = ResearchProgressRegistry.hasCompleted(
    player, "myplugin", "some_research_id");

Research Definition Classes

Class Description
ResearchCategory A category in the research tree (id, displayName, icon)
ResearchNode A single research node with prerequisites, objectives, and rewards
ResearchObjective A progress objective (event, amount, filters)
ResearchReward A reward given on completion (type, target, amount, operation)
ResearchState Enum: HIDDEN, LOCKED, AVAILABLE, IN_PROGRESS, COMPLETED

Arcane Knowledge Sources

Register custom sources of Arcane Knowledge so your plugin's actions grant knowledge:

import me.nagasonic.alkatraz.api.progression.arcane.ArcaneKnowledgeRegistry;
import me.nagasonic.alkatraz.api.progression.arcane.ArcaneKnowledgeSource;

// Register a source with circle-specific amounts
var source = new ArcaneKnowledgeSource(
    "test_kill",       // id
    10.0,              // base amount
    true,              // enabled
    Map.of(1, 5.0, 2, 15.0, 3, 30.0) // circle -> amount overrides
);
ArcaneKnowledgeRegistry.register(source);

// Query
Optional<ArcaneKnowledgeSource> found = ArcaneKnowledgeRegistry.get("test_kill");
Map<String, ArcaneKnowledgeSource> all = ArcaneKnowledgeRegistry.all();

Mob API

Magic Entity Types

import me.nagasonic.alkatraz.api.mobs.MagicEntityType;

// Built-in types
for (MagicEntityType type : MagicEntityType.values()) {
    // ZOMBIE_MAGE, ZOMBIE_FIGHTER, SKELETAL_MAGE
}

Optional<MagicEntityType> resolved = MagicEntityType.fromId("zombie_mage");

Spell Cast Configuration

import me.nagasonic.alkatraz.api.mobs.SpellCastConfig;

var castConfig = new SpellCastConfig(
    2.0,   // minCastDist
    10.0,  // maxCastDist
    20.0,  // castRange
    40     // cooldownTicks
);
boolean hasRangeKeeping = castConfig.hasRangeKeeping();

Mob Brain

Build a declarative AI description for magic mobs:

import me.nagasonic.alkatraz.api.mobs.MobBrain;

var brain = MobBrain.builder()
    .canSwim(true)
    .spellCast(castConfig)
    .meleeAttack(true)
    .lookAtPlayerRange(8.0f)
    .randomStroll(true)
    .build();

Recipe API

The recipe manager exposes a public API for querying and mutating recipes, listening for unlocks, and extending the recipe system with custom requirement types and crafting adapters.

RecipeManagerAPI

Obtain the API instance from Alkatraz.getRecipeManager():

import me.nagasonic.alkatraz.Alkatraz;
import me.nagasonic.alkatraz.items.magic.recipe.AlkatrazRecipe;
import me.nagasonic.alkatraz.items.magic.recipe.RecipeManagerAPI;
import me.nagasonic.alkatraz.api.magic.registry.MagicKeys;

RecipeManagerAPI recipes = Alkatraz.getRecipeManager();
Method Returns Description
getRecipe(NamespacedKey) Optional<AlkatrazRecipe> Look up a recipe by key
getRecipes() Collection<AlkatrazRecipe> All registered recipes (immutable view)
isUnlocked(Player, NamespacedKey) boolean Whether the player unlocked the recipe (recipes without requirements are always unlocked; false for unknown/null keys)
unlock(Player, NamespacedKey) void Unlock the recipe (idempotent; fires RecipeUnlockedEvent + notifications)
lock(Player, NamespacedKey) void Lock the recipe (idempotent; no event)
canCraft(Player, NamespacedKey) boolean Whether the player may craft right now (unlocked or currently meets every requirement; false for unknown keys)
registerRequirementType(String, RequirementFactory.Builder) void Register a custom requirement type used by recipe YAML
registerCustomAdapter(CustomCraftingAdapter) void Register a custom crafting adapter for type: custom recipes
reload() void Reload all recipe definitions from magic/recipes/
// Query and craft checks
Optional<AlkatrazRecipe> wand = recipes.getRecipe(MagicKeys.alkatraz("reinforced_wand"));
boolean unlocked = recipes.isUnlocked(player, MagicKeys.alkatraz("void_wand"));
boolean craftable = recipes.canCraft(player, MagicKeys.alkatraz("void_wand"));

// Mutate unlock state
recipes.unlock(player, MagicKeys.alkatraz("void_wand"));
recipes.lock(player, MagicKeys.alkatraz("void_wand"));

AlkatrazRecipe

An AlkatrazRecipe exposes its key, type (RecipeType), result, ingredients, requirements, permissions, and station payloads via getters (getKey(), getType(), getResult(), getRequirements(), etc.).

RecipeUnlockedEvent

me.nagasonic.alkatraz.events.RecipeUnlockedEvent fires whenever a player unlocks a recipe (via GUI, command, or API). Access getPlayer() and getRecipeKey() (a lowercased key string):

import me.nagasonic.alkatraz.events.RecipeUnlockedEvent;

@EventHandler
public void onRecipeUnlocked(RecipeUnlockedEvent event) {
    event.getPlayer().sendMessage("You unlocked " + event.getRecipeKey() + "!");
}

Custom Requirement Types

Implement the RecipeRequirement contract from the api module and register it with registerRequirementType:

import me.nagasonic.alkatraz.Alkatraz;
import me.nagasonic.alkatraz.api.recipe.RecipeRequirement;
import org.bukkit.entity.Player;

// 1. Implement the requirement
public class HasDiamondsRequirement implements RecipeRequirement {
    private final int count;
    public HasDiamondsRequirement(int count) { this.count = count; }

    @Override
    public boolean isMet(Player player) {
        return player.getInventory().contains(org.bukkit.Material.DIAMOND, count);
    }

    @Override
    public String getDescription() {
        return "Hold " + count + " diamonds";
    }
}

// 2. Register the type; the builder receives the recipe's requirement section
Alkatraz.getRecipeManager().registerRequirementType("has_diamonds", (spell, section) ->
    new me.nagasonic.alkatraz.configuration.requirement.Requirement() {
        private final RecipeRequirement req =
            new HasDiamondsRequirement(section.getInt("count", 1));

        @Override public boolean isMet(Player p) { return req.isMet(p); }
        @Override public String getDescription() { return req.getDescription(); }
        @Override public int getProgress(Player p) { return req.getProgress(p); }
    });

The registered type can then be used in any magic/recipes/*.yml:

requirements:
  - type: has_diamonds
    count: 16

Custom Crafting Adapters

Recipes with type: custom have no built-in GUI. Third-party plugins can extend CustomCraftingAdapter and register it to handle their own crafting menus:

import me.nagasonic.alkatraz.Alkatraz;
import me.nagasonic.alkatraz.items.magic.recipe.adapter.CustomCraftingAdapter;

public class MyAdapter extends CustomCraftingAdapter {
    @Override
    public void handleClick(InventoryClickEvent event, Player player) {
        // Read the click, look up recipes via RecipeRegistry.getByStation(RecipeType.CUSTOM),
        // gate with RecipeGate.canCraft(player, recipe), and set results.
    }
}

Alkatraz.getRecipeManager().registerCustomAdapter(new MyAdapter());

DOM Utilities

Pair

var pair = new Pair<>("hello", 42);
String key = pair.getOne();   // "hello"
int val = pair.getTwo();      // 42

Ground

Classifies Minecraft materials into ground types:

import me.nagasonic.alkatraz.api.dom.Ground;

Ground.DIRT.getType();                    // Material.DIRT
boolean isGround = Ground.isGround(Material.STONE);  // true
boolean isLoose = Ground.isLoose(Material.SAND);     // true
boolean isRock = Ground.isRock(Material.DEEPSLATE);  // true
boolean isOre = Ground.isOre(Material.DIAMOND_ORE);  // true
Material forBiome = Ground.getGround(Biome.FOREST);   // GRASS_BLOCK

Permissions

The Permission enum lists all plugin permissions:

import me.nagasonic.alkatraz.api.dom.Permission;

// Check if a player has a permission
boolean has = Permission.hasPermission(player, Permission.ALL_SPELLS);

// Get the permission string
String perm = Permission.COMMAND_CAST.getPermissionString();
// "alkatraz.command.cast"
Permission String
COMMAND_SPELLS_OTHER alkatraz.command.spells.other
COMMAND_GIVE alkatraz.command.give
COMMAND_MASTERY alkatraz.command.mastery
COMMAND_EXPERIENCE alkatraz.command.experience
COMMAND_CIRCLE alkatraz.command.circle
COMMAND_DISCOVER alkatraz.command.discoverspell
COMMAND_UNDISCOVER alkatraz.command.undiscoverspell
COMMAND_STATS_OTHER alkatraz.command.stats.other
COMMAND_RELOAD alkatraz.command.reload
COMMAND_SUMMON alkatraz.command.summon
COMMAND_CAST alkatraz.command.cast
COMMAND_CONVERT alkatraz.command.convert
COMMAND_EQUIPMENT alkatraz.command.equipment
COMMAND_PROFILE alkatraz.command.profile
ALL_SPELLS alkatraz.allspells

Complete Example Plugin

Below is a minimal but complete plugin that demonstrates the major API features:

package au.nagasonic.alkatrazTestPlugin;

import me.nagasonic.alkatraz.api.Element;
import me.nagasonic.alkatraz.api.SpellAPI;
import me.nagasonic.alkatraz.api.magic.attribute.AttributeService;
import me.nagasonic.alkatraz.api.magic.attribute.AttributeType;
import me.nagasonic.alkatraz.api.magic.component.ComponentHandlerRegistry;
import me.nagasonic.alkatraz.api.magic.registry.MagicKeys;
import me.nagasonic.alkatraz.api.magic.registry.MagicItemRegistries;
import me.nagasonic.alkatraz.api.magic.trigger.TriggerType;
import me.nagasonic.alkatraz.api.progression.arcane.ArcaneKnowledgeRegistry;
import me.nagasonic.alkatraz.api.progression.arcane.ArcaneKnowledgeSource;
import me.nagasonic.alkatraz.api.progression.research.ResearchProgressRegistry;
import org.bukkit.Bukkit;
import org.bukkit.plugin.java.JavaPlugin;

import java.util.Map;

public class ExamplePlugin extends JavaPlugin {

    @Override
    public void onEnable() {
        // 1. Verify Alkatraz
        if (Bukkit.getPluginManager().getPlugin("Alkatraz") == null) {
            getLogger().severe("Alkatraz not found!");
            Bukkit.getPluginManager().disablePlugin(this);
            return;
        }

        // 2. Register a custom spell
        var spell = new ExampleSpell("EXAMPLE");
        spell.loadConfiguration();
        SpellAPI.registerSpell(spell);

        // 3. Register a custom attribute type
        MagicItemRegistries.ATTRIBUTE_TYPES.register(new AttributeType(
            MagicKeys.alkatraz("test_bonus"), 0.0, "Test Bonus"));

        // 4. Register a custom trigger type
        MagicItemRegistries.TRIGGER_TYPES.register(new TriggerType(
            MagicKeys.alkatraz("on_test_event"), "Fired by example plugin"));

        // 5. Register an attribute source
        AttributeService.getInstance().registerSource(new CircleBonusSource());

        // 6. Register research progress
        ResearchProgressRegistry.register("example", (player, id) -> {
            var profile = SpellAPI.getProfile(player);
            return profile != null && profile.hasCompletedResearch(id);
        });

        // 7. Register arcane knowledge source
        ArcaneKnowledgeRegistry.register(new ArcaneKnowledgeSource(
            "example_kill", 10.0, true, Map.of(1, 5.0, 2, 15.0)));

        // 8. Register event listener
        Bukkit.getPluginManager().registerEvents(new ExampleListener(), this);

        getLogger().info("ExamplePlugin enabled with Alkatraz API.");
    }
}

For a full working example with all subcommands demonstrating every API type, see the AlkatrazTestPlugin repository.

Clone this wiki locally