Configuration

Everything lives in config.lua.

Config reference

KeyWhat it does
Config.ItemItem that opens the perk tablet. Default perk_tablet.
Config.BrandingTablet header branding: name, suffix and accent color. Sent to the NUI.
Config.MaxLevelHighest level any category can reach. Default 10.
Config.LevelsXP required for each level (the global curve). Tops out at 20,000 at level 10.
Config.CategoryLevelsOptional. Add Config.CategoryLevels = { categoryId = { [1] = 200, ... } } to give a category its own XP curve. Omit it entirely and every category uses Config.Levels.
Config.IgnorePerkRequirementstrue (default): spend points in any order once you meet the level. false: must own the previous perk in the chain first.
Config.CategoriesThe perk tree: tracks, categories, and the perks in each.
💡

To change level speed, tune the XP each activity awards in that activity's own config (e.g. jj-houserobbery's tiers). Don't lower the 20,000 cap.

Adding a category

A category is one progression track in the tablet (House Robbery, Fishing, your own job). It has a level 0-10 driven by XP, and a list of perks the player buys with the points they earn from levelling.

jj-perks only stores progression — it tracks XP, levels, and which perks a player owns, and shows them in the tablet. It does not change your gameplay on its own. The actual bonus is something your own script applies when it asks jj-perks "does this player own this perk?". So adding a category is two jobs: define it here, then read it from your script.

1. Define it in config

Categories live in Config.Categories, split into two tracks: criminal and civilian. Here's a full example:

{
    id          = "mining",                 -- unique key. your script passes this to every export
    label       = "Mining",                 -- shown on the tab
    icon        = "gem",                     -- Font Awesome icon name (no "fa-" prefix)
    description = "Mine faster and earn double drops on ores.",
    perks = {
        { id = "mine_r1",  label = "Quick Gather I",   description = "2% quicker gathering",       icon = "gauge-high", requiredLevel = 1,  requires = nil },
        { id = "mine_r2",  label = "Double Drops I",   description = "5% chance of double drops",  icon = "clone",      requiredLevel = 2,  requires = "mine_r1" },
        { id = "mine_r3",  label = "Quick Gather II",  description = "4% quicker gathering",       icon = "gauge-high", requiredLevel = 3,  requires = "mine_r2" },
        -- ...continue up to mine_r10
    },
}
FieldWhat it is
idThe category key. This is the string you pass to every export. Must be unique and never change once players have progress on it.
labelDisplay name on the tablet tab.
iconFont Awesome 6 icon name without the fa- prefix.
descriptionOne line shown under the category title.
perksThe list of perks, in order.

Each perk:

FieldWhat it is
idUnique perk id across the whole resource. This is what hasPerk(src, 'mine_r2') checks.
label / descriptionUI text only. The description is a promise to the player; your script is what makes it true.
iconFont Awesome name for the perk node.
requiredLevelThe category level the player must reach before they can buy this perk.
requiresThe perk id that must be owned first. nil for the first perk. Ignored entirely when Config.IgnorePerkRequirements = true (the default).
💡

Rule of thumb: keep 10 perks per category so the points line up with the 10 levels, and keep every id unique. Save and restart jj-perks and the new tab shows up.

At this point it tracks XP and lets players buy perks, but nothing in-game changes yet — see Exports & Integration for wiring the bonus into your script.