---
title: Perk Registration
hide_meta: true
---

# Startup 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.

```js
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 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

| 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:

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:

```js
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.
