Repository navigation
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.apipackage.
- Plugin Setup
- Creating a Custom Spell
- Registering Spells
- Listening to Spell Events
- Player Profile & Stats
- Magic Item Registries
- Item Definitions
- Magic Item Instances
- Attributes & Attribute Sources
- Components & Component Handlers
- Conditions
- Effects
- Triggers & Trigger Bindings
- Engravings
- Equipment & Virtual Slots
- Research Progress
- Arcane Knowledge Sources
- Mob API
- Recipe API
- DOM Utilities
- Permissions
- Complete Example Plugin
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
}
}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;
}
}| 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 |
| Method | Description |
|---|---|
canMobCast(Mob) |
Whether the mob can cast (default: true) |
| 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 |
Use the static SpellAPI class to register spells at startup:
var spell = new FireballSpell("FIREBALL");
spell.loadConfiguration();
SpellAPI.registerSpell(spell);| 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 |
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());
}
}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");
}| 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 |
| Method | Returns | Description |
|---|---|---|
getPoints(Element) |
int |
Invested stat points in an element |
getAffinity(Element) |
double |
Elemental affinity (damage bonus) |
getResistance(Element) |
double |
Elemental resistance |
| 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 |
| Method | Returns | Description |
|---|---|---|
hasCompletedResearch(String) |
boolean |
Research completion check |
getCompletedResearchIds() |
Collection<String> |
All completed research IDs |
hasStartedResearch(String) |
boolean |
Whether research was started |
| Method | Description |
|---|---|
canCast() / setCanCast(boolean)
|
Whether the player can cast |
isCasting() / setCasting(boolean)
|
Whether the player is currently casting |
isStealth() / setStealth(boolean)
|
Stealth mode |
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 |
// 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();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"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);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();The attribute system resolves custom numeric values (like spell_power, max_mana) by collecting contributions from multiple sources.
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);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)
);
}
}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();| Operation | Description |
|---|---|
ADD |
Added to the base value |
MULTIPLY |
Multiplied against the accumulated value |
SET |
Overrides the value (takes priority over base) |
| 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 are named features attached to item definitions. A ComponentHandler provides the runtime behavior.
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();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!"));
}
}| 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 |
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();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);
}
}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);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);
});
}
}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);A TriggerType defines when effects fire. A TriggerBinding connects a trigger type to conditions and effects.
| 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 |
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);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.
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);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()
|
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);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");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);
}
}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");| 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 |
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();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");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();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();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.
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"));An AlkatrazRecipe exposes its key, type (RecipeType), result, ingredients, requirements, permissions, and station payloads via getters (getKey(), getType(), getResult(), getRequirements(), etc.).
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() + "!");
}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: 16Recipes 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());var pair = new Pair<>("hello", 42);
String key = pair.getOne(); // "hello"
int val = pair.getTwo(); // 42Classifies 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_BLOCKThe 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 |
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.