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:
| Method | Key written | What it does |
|---|---|---|
withSkill(skill) | skill | The skill this perk is tied to |
withCooldown(ticks) | cooldown | Minimum ticks between activations |
withDuration(ticks) | duration | How long the perk stays active (0 = no tick) |
withChance(value) | chance | Activation chance — 0.0 to 1.0 |
withMinLevel(level) | min_level | Minimum required level in the skill |
withMaxLevel(level) | max_level | Maximum allowed level in the skill |
withPerXLevel(step) | per_x_level | Only activate on levels divisible by this value |
withMilestones(values) | milestones | Only 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 tagdefaults(tag => {})— arbitrary default tag editing throughPerkTagJS
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
| Callback | Context type | Extra methods |
|---|---|---|
conditions(ctx) | PerkConditionContextJS | — |
start(ctx) | PerkStartContextJS | getResult() |
tick(ctx) | PerkTickContextJS | getElapsedTicks() |
stop(ctx) | PerkStopContextJS | — |
status(ctx) | PerkStatusContextJS | addLine(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:
- Your startup defaults from the builder methods
- The matching entry from
perks.toml(if any) - Incoming data from the current PMMO event
- 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)andwithDouble('heal_amount', 1.0)set configurable knobs. Players can override these inperks.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)storesactiveandstarted_atinctx.getSettings()sotick()andstop()can read them later.start(ctx)writessteady_mind_startedtoctx.getResult()— this is one-shot output for the current execution pass.tick(ctx)usesctx.getElapsedTicks()with a modulo check to run healing at a configurable interval.stop(ctx)cleans up theactiveflag.status(ctx)only builds UI lines — it does not create extra state.