Custom item SDK
Build custom items with declarative triggers, composable effects, conditions, and targeting. No event listeners to wire up. No NBT to wrangle. Five pieces, chained with a fluent builder.
The five pieces
Every rich custom item is a list of actions. Each action has one trigger, zero-or-more conditions, one target resolver, and an effect chain. That's it. The plugin handles event subscription, cooldowns, charges, and soulbound checks for you.
| Piece | What it answers | Catalog class |
|---|---|---|
| Trigger | When does this action fire? | TriggerType |
| Condition | Should it actually fire right now? | Conditions |
| Target | Who or what does the effect apply to? | Targets |
| Effect | What actually happens? | Effects |
| State | What persists on the item itself? | ItemState |
Hello, void blade
The shortest non-trivial example. Right-clicking with sneak does a forward dash; attacking deals bonus damage and plays a sound. No setup beyond depending on the plugin and grabbing the API.
public class VoidBladeAddon extends JavaPlugin {
@Override
public void onEnable() {
NexoraApi api = NexoraSMP.get().api();
api.customItems().rich("voidblade")
.displayName("<gradient:#9d4edd:#5b8cff>Void Blade</gradient>")
.lore("<gray>Forged in the void.</gray>")
.material(Material.NETHERITE_SWORD)
.soulbound(true)
.action(TriggerType.ON_ATTACK)
.effect(Effects.damage(8.0))
.effect(Effects.particle(Particle.SCULK_SOUL, 20))
.effect(Effects.sound(Sound.ENTITY_WARDEN_ATTACK_IMPACT, 1f, 0.8f))
.add()
.action(TriggerType.ON_RIGHT_CLICK)
.when(Conditions.sneaking())
.target(Targets.lookDirection(15))
.effect(Effects.dash(2.0, true))
.cooldown(5000)
.add()
.register();
}
}
Give it to a player from anywhere:
api.customItems().give(player, "voidblade");
Triggers
Trigger points are defined in the TriggerType enum. The plugin
subscribes to the underlying Bukkit events and dispatches matching actions
for you. Multiple actions on one item can share a trigger; they all fire
(in registration order) if their conditions pass.
| Trigger | When it fires | Context populated |
|---|---|---|
ON_RIGHT_CLICK | Right-click air or block holding the item | player, item |
ON_LEFT_CLICK | Left-click air or block holding the item | player, item |
ON_ATTACK | Player hits a living entity with the item in main hand | player, item, victim |
ON_TAKE_DAMAGE | Player takes damage with the item in inventory | player, item, attacker |
ON_KILL | Player kills a living entity holding the item | player, item, victim |
ON_DEATH | Player dies with the item in inventory | player, item |
ON_HOLD_TICK | Every 1 second while item is in main hand | player, item |
ON_SNEAK_START | Player begins sneaking with the item in inventory | player, item |
ON_SNEAK_END | Player stops sneaking with the item in inventory | player, item |
ON_DROP | Player drops the item | player, item |
ON_PICKUP | Player picks up an instance of the item | player, item |
ON_INTERACT_ENTITY | Player right-clicks an entity holding the item | player, item, victim |
Throwing inside a condition or effect is caught and logged. The remaining effects of the same action are skipped, but other actions continue.
Effects
The Effects class is a static catalog of building blocks.
Combine them in any order on an action; they run sequentially against the
resolved target list.
| Effect | Description |
|---|---|
Effects.damage(amount) | Inflict raw damage on each target, attributed to the holder |
Effects.heal(amount) | Heal each target up to their max health |
Effects.potion(type, seconds, amplifier) | Apply a vanilla potion effect |
Effects.particle(particle, count) | Spawn particles at each target |
Effects.sound(sound, volume, pitch) | Play a sound at each target |
Effects.knockback(strength) | Push each target away from the holder |
Effects.pull(strength) | Pull each target toward the holder |
Effects.lifesteal(fraction, basePerTarget) | Heal the holder for a fraction of damage dealt |
Effects.message(mini) | Send a MiniMessage-formatted line to the holder |
Effects.command(cmd) | Run a console command (substitutes %player% and %victim%) |
Effects.dash(power, upward) | Launch the holder forward (or upward) |
Effects.ignite(seconds) | Set each target on fire |
Effects.lightning() | Strike lightning at each target |
Effects.teleportToTarget(dy) | Teleport the holder to the first target |
Effects.chain(...effects) | Group multiple effects into a single composite |
Conditions
A condition is a predicate. All conditions on an action must pass for it to fire. Cooldown is not consumed and charges are not spent if any condition rejects.
.action(TriggerType.ON_ATTACK)
.when(Conditions.healthBelow(0.3))
.when(Conditions.victimIsPlayer())
.when(Conditions.chance(25))
.effect(Effects.damage(12.0))
.effect(Effects.message("<red>Vengeance.</red>"))
.add()
| Condition | Passes when… |
|---|---|
sneaking() / notSneaking() | Holder's sneak state matches |
onGround() / airborne() | Holder is on the ground or in the air |
inWorld(name) | Holder is in the named world |
healthBelow(frac) / healthAbove(frac) | Holder's health is below/above a fraction (0-1) |
isNight() / isDay() | World time is night/day |
victimType(type) | The victim is a specific entity type |
victimIsPlayer() | The victim is another player |
victimIsBoss() | The victim has a visible custom name |
hasPermission(node) | Holder has the permission node |
chance(pct) | Random roll under pct percent |
all(...) / any(...) / not(c) | Logical composition |
Targets
Targets resolve to a list of living entities the effects should act on.
Each effect iterates the same list. The default target is
Targets.self().
| Target | Resolves to |
|---|---|
self() | Just the holder |
attacker() | The entity that hit the holder (ON_TAKE_DAMAGE) |
victim() | The entity hit or interacted with |
none() | Empty list (use for player-only effects) |
lookDirection(range) | First entity along holder's look-vector, up to range blocks |
aoe(radius) | All living entities within radius of the holder |
aoeAtVictim(radius) | All living entities within radius of the victim |
cone(range, halfAngleDeg) | Entities inside a forward cone |
limit(target, n) | Wrap any target, keep only closest n |
State, charges, soulbound
Per-stack state lives on the item's NBT (PersistentDataContainer). It survives
drops, restarts, ender chests, anything short of /clear. Access
via api.customItems().state().
Charges
Set .charges(n) on the builder to stamp an initial count. Mark
actions with .consumesCharge(true) to spend one per fire. When
charges hit zero the action is silently skipped (no message, no cooldown spent).
Pass -1 for unlimited.
api.customItems().rich("healorb")
.material(Material.SPECTRAL_ARROW)
.charges(5)
.action(TriggerType.ON_RIGHT_CLICK)
.target(Targets.aoe(6))
.effect(Effects.heal(4.0))
.effect(Effects.particle(Particle.HEART, 10))
.consumesCharge(true)
.cooldown(3000)
.add()
.register();
Soulbound
Mark the item soulbound on the builder. When a player receives one through
give(), the item is bound to their UUID. Other players can't
pick it up, and the item's actions silently skip when held by anyone else.
api.customItems().rich("crown")
.material(Material.GOLDEN_HELMET)
.soulbound(true)
.register();
Direct state access
Read or mutate state on any tagged stack via ItemState:
ItemState st = api.customItems().state();
int kills = st.killCount(stack);
int charges = st.charges(stack);
st.addXp(stack, 10);
if (st.isSoulboundFor(stack, otherPlayer)) {
event.setCancelled(true);
}
The plugin auto-increments kill count on ON_KILL.
Combined example: vampire scythe
Pulls everything together. Heals on hit, has a sneak-channeled AoE pull, drains health when held in daylight as the cost of being a vampire weapon.
api.customItems().rich("vampire_scythe")
.displayName("<color:#a8174e>Vampire Scythe</color>")
.lore(
"<gray>Steals life on hit.</gray>",
"<gray><dark_red>Burns</dark_red> in daylight.</gray>",
"<gray>Sneak + right-click: drain pulse.</gray>")
.material(Material.NETHERITE_HOE)
.soulbound(true)
.action(TriggerType.ON_ATTACK)
.target(Targets.victim())
.effect(Effects.damage(7.0))
.effect(Effects.lifesteal(0.5, 7.0))
.effect(Effects.particle(Particle.DAMAGE_INDICATOR, 8))
.add()
.action(TriggerType.ON_RIGHT_CLICK)
.when(Conditions.sneaking())
.target(Targets.aoe(8))
.effect(Effects.pull(1.5))
.effect(Effects.damage(3.0))
.effect(Effects.lifesteal(0.3, 3.0))
.effect(Effects.particle(Particle.SOUL, 30))
.effect(Effects.sound(Sound.ENTITY_WITHER_SHOOT, 1f, 0.7f))
.cooldown(8000)
.add()
.action(TriggerType.ON_HOLD_TICK)
.when(Conditions.isDay())
.target(Targets.self())
.effect(Effects.damage(0.5))
.add()
.register();
Imperative escape hatch
The rich builder is the recommended path. If you need full Bukkit-event
control, you can still implement CustomItemProvider directly:
api.customItems().register(new CustomItemProvider() {
@Override public String id() { return "my_item"; }
@Override public String displayName() { return "My Item"; }
@Override public List<String> lore() { return List.of(); }
@Override public ItemStack baseItem() { return new ItemStack(Material.STICK); }
@Override public long abilityCooldownMs() { return 2000; }
@Override public void onUse(Player p, ItemStack s, PlayerInteractEvent e) {
p.sendMessage("Pew.");
}
});
Mix and match: an imperative item can still call
api.customItems().state().incrementKillCount(stack) to opt
into the persistent state helpers.
API stability
Everything under dev.nexoralabs.nexorasmp.api.* is the stable
public surface. Anything in .api.impl.* is private and may change
between patch releases. The NexoraApi.VERSION constant follows
semver and is bumped on breaking changes.