# Levels, attributes and skills (progression.levels) ## What it is for `progression.levels` gives a game experience points, levels, points to spend on attributes and on skills, and the stats they grant. XP comes from the kills made by the player, from missions and from your own rules. A level is bought with a growing curve; each level grants attribute points and skill points. Skills are plain data (the `skills` collection) with trees, costs, minimum level and prerequisites. Everything unlocked is summed into stats that `combat.core` applies: maximum health, damage and armour. Each player entity has its own sheet, so co-op works; the HUD variables show the sheet of the HUD player. The kit shows the level badge and the character sheet through an `ui.webui` fragment. ## Turn it on ```text gripforge_gamekit_install { project_id: "gkp_…", id: "progression.levels" } gripforge_gamekit_configure { project_id: "gkp_…", id: "progression.levels", config: { "maxLevel": 30, "baseXp": 120, "xpByTag": { "boss": 500, "elite": 80 } } } ``` It requires `game.engine`. Kills only count when `combat.core` is installed (the kit listens to `combat.killed`); the stats reach the player's health and damage through `combat.health` and `combat.damage`. The key `C` opens the character sheet (input action `character_sheet`). Skills are a data collection of the project: ```json { "skills": [ { "id": "cut", "name": "Cut", "tree": "one_handed", "cost": 1, "stats": { "damage_add": 2 } }, { "id": "deep", "name": "Deep cut", "tree": "one_handed", "requires": ["cut"], "minLevel": 3, "stats": { "damage_add": 3 } } ] } ``` ## First example The test sets `maxLevel: 3`, `baseXp: 100`, `growth: 2` and the two skills above, then drives the service `progression.levels`: 1. `toNext()` answers `100`; `addXp(120.5)` crosses level 2 and answers the XP kept (`20.5`). 2. `points()` is `{ attribute: 1, skill: 1 }`. `unlock("deep")` is `false` (level and prerequisite), `unlock("cut")` is `true`. 3. `spendAttribute("health")` is `true` once, then `false` (no point left). 4. `stats()` is `{ damage_add: 2, health_max: 10 }`. 5. A big `addXp(500)` stops at the cap: `level()` is `3`, `toNext()` is `0`, and `unlock("deep")` now works. ## Settings and defaults | Setting | Type | Default | Meaning | |---|---|---|---| | `maxLevel` | integer (2..999) | `50` | level cap; XP stops there | | `baseXp` | number | `100` | XP to reach level 2 | | `growth` | number (1..3) | `1.18` | each level needs this times the previous cost | | `xpPerKill` | number | `20` | XP for a player kill when the victim has no `xp` component (0: none) | | `xpByTag` | object | `{}` | XP by victim tag, e.g. `{ "boss": 500 }`; the highest matching tag wins over `xpPerKill` | | `attributePointsPerLevel` | integer (0..20) | `1` | attribute points granted per level | | `skillPointsPerLevel` | integer (0..20) | `1` | skill points granted per level | | `attributes` | list | health, stamina, magicka | what an attribute point buys: `{ id, label, stat, perPoint }` | | `healOnLevelUp` | boolean | `true` | a level up restores the player to full health | The default `attributes` raise the stats `health_max`, `stamina_max` and `mana_max` by 10 per point. The cost of going from level `L` to `L + 1` is `baseXp x growth^(L - 1)`, rounded. A victim's own `xp` component, if it has one, is its worth. A skill of the `skills` collection has `id`, `name`, and optionally `tree`, `description`, `cost` (skill points, default 1), `requires` (skills to unlock first), `minLevel` and `stats` (numbers granted while unlocked). All settings and the `skills` collection are moddable. ## Services, events, variables The service `progression.levels` (about the HUD player or the first sheet): - `xp()`, `level()`, `toNext()`: current XP, level, and XP missing for the next level (`0` at the cap). - `addXp(amount, reason?)`: adds XP, answers the XP kept. - `points()`: `{ attribute, skill }` points available. - `attributes()`: the attributes with their current `rank`. - `spendAttribute(id)`: spends one point; `false` if none left, unknown attribute, or a hook refused. - `skills()`: the skills with `unlocked` and `available`; `unlocked(id)`, `canUnlock(id)`, `unlock(id)`. - `stat(name)` and `stats()`: summed stats. Stats understood by other kits: `health_max` (through `combat.health`), `damage_add` and `damage_mult` (an outgoing modifier of `combat.damage`), `armor` (an incoming reduction of `100 / (100 + armor)`). Any other name is kept for your own use. - `sheet(open?)`: opens or closes the character sheet and answers its state. Events: `progression.xp` (`amount`, `xp`, `level`, `reason`), `progression.levelup` (`level`, `attributePoints`, `skillPoints`), `progression.attribute` (`attribute`, `rank`, `stat`, `value`), `progression.skill` (`skill`, `tree`), `progression.stats` (`stats`). The kit listens to `combat.killed`, `mission.completed` (XP from `rewards.xp`) and `game.phase` (a pause closes the sheet). Variables: `progression.sheet`, `progression.level`, `progression.xp`, `progression.xpNext`, `progression.attributePoints`, `progression.skillPoints`. Verbs for rules and scripts: `add_xp` (action, `amount`, optional `reason`), `unlock_skill` (action, `skill`), and the conditions `level_at_least` (`level`) and `skill_unlocked` (`skill`): ```json { "add_xp": 50 } { "unlock_skill": "cut" } { "level_at_least": 5 } ``` ## Hooks and modding - `xp.gain`: `{ player, amount, reason, level }`; `amount` is mutable; cancellable. Cancelled or 0: no XP, no event. - `level.up`: `{ player, level, attributePoints, skillPoints }`; both point counts are mutable; cancellable (the level is not gained, the XP is kept and the level is asked again at the next gain). - `attribute.spend`: `{ player, attribute, stat, rank, cost }`; `cost` is mutable; cancellable. - `skill.unlock`: `{ player, skill, tree, cost }`; `cost` is mutable; cancellable. A mod can double XP in a zone, grant extra points at level 10, or refuse a skill while a quest is open, without touching the kit. The settings and the `skills` collection can also be changed by a mod, and an entry can extend another. ## Engines The logic is one Rust module, identical on the web, the native player, Godot, Unity and Unreal. The level badge and the character sheet are an `ui.webui` fragment. ## When it does not do what you expect - No XP from kills: the killer must be the player and `combat.killed` must be emitted (install `combat.core`); `xpPerKill` may be `0`; or an `xp.gain` tap cancelled it. - `unlock(id)` answers `false`: not enough skill points, level below `minLevel`, a prerequisite missing, already unlocked, or a hook refused. `canUnlock(id)` tells before trying. - Maximum health does not change: the stat is `health_max` and needs `combat.health`. - XP stays at 0 at the top: the cap was reached. ## Limits - No respec: points are not given back. - Stats other than `health_max`, `damage_add`, `damage_mult` and `armor` are only summed; your game reads them with `stat`. - The damage modifiers follow the sheet of the HUD player. ## Versions Kit version 1.2.0 (service contract 1.1.0). 1.1.0 added one sheet per player entity (co-op); 1.2.0 added the hooks and the moddable settings.