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.

PieceWhat it answersCatalog class
TriggerWhen does this action fire?TriggerType
ConditionShould it actually fire right now?Conditions
TargetWho or what does the effect apply to?Targets
EffectWhat actually happens?Effects
StateWhat 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.

VoidBladeAddon.java
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.

TriggerWhen it firesContext populated
ON_RIGHT_CLICKRight-click air or block holding the itemplayer, item
ON_LEFT_CLICKLeft-click air or block holding the itemplayer, item
ON_ATTACKPlayer hits a living entity with the item in main handplayer, item, victim
ON_TAKE_DAMAGEPlayer takes damage with the item in inventoryplayer, item, attacker
ON_KILLPlayer kills a living entity holding the itemplayer, item, victim
ON_DEATHPlayer dies with the item in inventoryplayer, item
ON_HOLD_TICKEvery 1 second while item is in main handplayer, item
ON_SNEAK_STARTPlayer begins sneaking with the item in inventoryplayer, item
ON_SNEAK_ENDPlayer stops sneaking with the item in inventoryplayer, item
ON_DROPPlayer drops the itemplayer, item
ON_PICKUPPlayer picks up an instance of the itemplayer, item
ON_INTERACT_ENTITYPlayer right-clicks an entity holding the itemplayer, item, victim
Note

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.

EffectDescription
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()
ConditionPasses 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().

TargetResolves 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.");
  }
});
Tip

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.