LogoKubeJS PMMO

Perk Registration

PmmoJS.registerPerk(...) registers new PMMO perk types from startup_scripts/. This is for defining a brand new perk — use PmmoJS.perksConfig(...) in server_scripts/ if you only want to edit existing perk entries.

Basic syntax

At minimum, a registered perk needs an ID, a side, a skill, and at least one callback. The builder provides default values for everything else.

PmmoJS.registerPerk(event => {
event
.create('kubejs:steady_mind', PMMOPerkSide.SERVER)
.withSkill('combat')
.description('A custom perk registered from KubeJS.')
.conditions(ctx => true)
.start(ctx => {})
.tick(ctx => {})
.stop(ctx => {})
.status(ctx => {})
.register()
})

After calling register(), your perk is live in PMMO's registry. You must call register() as the last step — without it, the builder discards everything.

Setting defaults

These builder methods define the startup default values for your perk. They become part of the tag that PMMO feeds into your callbacks.

Standard PMMO fields:

MethodKey writtenWhat it does
withSkill(skill)skillThe skill this perk is tied to
withCooldown(ticks)cooldownMinimum ticks between activations
withDuration(ticks)durationHow long the perk stays active (0 = no tick)
withChance(value)chanceActivation chance — 0.0 to 1.0
withMinLevel(level)min_levelMinimum required level in the skill
withMaxLevel(level)max_levelMaximum allowed level in the skill
withPerXLevel(step)per_x_levelOnly activate on levels divisible by this value
withMilestones(values)milestonesOnly activate on these specific levels

Custom fields:

  • withString(key, value), withBool(key, value), withInt(key, value)
  • withLong(key, value), withFloat(key, value), withDouble(key, value)
  • withStringList(key, values), withNumberList(key, values)
  • withCompound(key, value) — nest a compound tag
  • defaults(tag => {}) — arbitrary default tag editing through PerkTagJS

Lifecycle callbacks

Each callback runs at a specific point in the perk's lifecycle and receives a context object with a different set of methods.

conditions(ctx) — Should this perk activate?

Runs after PMMO's built-in validity check. Return true to allow activation, false to block it.

start(ctx) — The perk just activated

This is where you initialize state. start() is the only callback that gets both ctx.getSettings() (for persistent state) and ctx.getResult() (for one-shot output back into PMMO's current execution pass).

tick(ctx) — The perk is active

Fires continuously while the perk stays active. Use ctx.getElapsedTicks() for timing. PMMO ignores the return value.

stop(ctx) — The perk is deactivating

Clean up any persistent state you stored in ctx.getSettings().

status(ctx) — The UI is requesting display text

Use ctx.addLine(...) and ctx.addLines(...) to build status lines shown in PMMO's UI. PMMO ignores the return value.

Context objects at a glance

CallbackContext typeExtra methods
conditions(ctx)PerkConditionContextJS
start(ctx)PerkStartContextJSgetResult()
tick(ctx)PerkTickContextJSgetElapsedTicks()
stop(ctx)PerkStopContextJS
status(ctx)PerkStatusContextJSaddLine(v), addLines(vs), clearLines(), getLines()

All contexts share: getPerkId(), getPlayer(), getServerPlayer(), getSettings().

How settings are merged

Before calling your callbacks, PMMO builds one merged tag by layering these sources in order:

  1. Your startup defaults from the builder methods
  2. The matching entry from perks.toml (if any)
  3. Incoming data from the current PMMO event
  4. Output from earlier perks in the same execution pass

If the merged tag has a skill field, PMMO injects the current player's level for that skill.

ctx.getSettings() gives you the final merged result.

PerkTagJS quick reference

Your callbacks work with ctx.getSettings() and (in start()) ctx.getResult(), both of which are PerkTagJS instances.

Reading: keys(), has(key), getString(key), getInt(key), getLong(key), getFloat(key), getDouble(key), getBoolean(key), getOrCreateCompound(key). Each has a *Or(key, fallback) variant.

Writing: remove(key), clear(), putString(key, value), putBoolean(key, value), putInt(key, value), putLong(key, value), putFloat(key, value), putDouble(key, value), putCompound(key, value), putStringList(key, values), putNumberList(key, values), merge(value).

PMMO helpers: getSkill(), getResolvedLevel(), getCooldown(), getDuration(), getChance().

Full example

Here is a complete perk that heals the player while active, only when they are below full health:

PmmoJS.registerPerk(event => {
event
.create('kubejs:steady_mind', PMMOPerkSide.SERVER)
.withSkill('combat')
.withCooldown(200)
.withDuration(100)
.withMinLevel(10)
.withInt('heal_interval', 20)
.withDouble('heal_amount', 1.0)
.defaults(tag => {
tag.putBoolean('active', false)
tag.putLong('started_at', 0)
})
.description('Heals the player while the perk stays active.')
.conditions(ctx => {
return ctx.getPlayer().getHealth() < ctx.getPlayer().getMaxHealth()
})
.start(ctx => {
ctx.getSettings().putBoolean('active', true)
ctx.getSettings().putLong('started_at', ctx.getPlayer().level().getGameTime())
ctx.getResult().putString('steady_mind_started', ctx.getPerkId())
})
.tick(ctx => {
const settings = ctx.getSettings()
const interval = settings.getIntOr('heal_interval', 20)
const amount = settings.getDoubleOr('heal_amount', 1.0)
if (settings.getBooleanOr('active', false) && ctx.getElapsedTicks() % interval === 0) {
ctx.getPlayer().heal(amount)
}
})
.stop(ctx => {
ctx.getSettings().putBoolean('active', false)
})
.status(ctx => {
const settings = ctx.getSettings()
ctx.clearLines()
ctx.addLine(`perk: ${ctx.getPerkId()}`)
ctx.addLine(`skill: ${settings.getSkill()}`)
ctx.addLine(`level: ${settings.getResolvedLevel()}`)
ctx.addLine(`active: ${settings.getBooleanOr('active', false)}`)
})
.register()
})

The example uses several independent settings:

  • withInt('heal_interval', 20) and withDouble('heal_amount', 1.0) set configurable knobs. Players can override these in perks.toml.
  • defaults(tag => {}) sets private fields only your perk uses. PMMO ignores unknown keys.
  • conditions(ctx) only decides activation — it does not store state.
  • start(ctx) stores active and started_at in ctx.getSettings() so tick() and stop() can read them later.
  • start(ctx) writes steady_mind_started to ctx.getResult() — this is one-shot output for the current execution pass.
  • tick(ctx) uses ctx.getElapsedTicks() with a modulo check to run healing at a configurable interval.
  • stop(ctx) cleans up the active flag.
  • status(ctx) only builds UI lines — it does not create extra state.