# Threat and aggro (combat.threat) ## What it is for `combat.threat` decides whom an enemy attacks in a group fight. Each enemy keeps a table of the actors engaged with it; damage and effective healing add threat, and the enemy picks the actor with the most. To avoid flicker, a challenger must beat the current target by a margin (more at range than in melee), and a taunt forces a target for a number of fixed frames. The kit listens to `combat.damaged`, `combat.healed`, `combat.killed` and `entity.despawned` by itself; your enemy AI reads the chosen target with `target(enemy)`. ## Turn it on Install the kit in a project and set its settings with the tools below; `gripforge kit api combat.threat` lists every setting, and `gripforge docs combat-threat` prints this guide offline. ```text gripforge_gamekit_install { project_id: "gkp_…", id: "combat.threat" } gripforge_gamekit_configure { project_id: "gkp_…", id: "combat.threat", config: { "healMultiplier": 0.5, "meleeThreshold": 1.1, "rangedThreshold": 1.3 } } ``` ## First example Two actors are engaged with an orc; `ann` hits hard and holds the target, then a 3-frame taunt from `bob` forces `bob` even after a much bigger hit on `ann`, until it runs out. ```text combat.threat.request("default", { op: "engage", enemy: "orc", actor: "ann" }) combat.threat.request("default", { op: "engage", enemy: "orc", actor: "bob" }) combat.threat.request("default", { op: "damage", sequence: 1, enemy: "orc", actor: "ann", amount: 100 }) combat.threat.target("orc") // "ann" (after one fixed step) combat.threat.request("default", { op: "taunt", enemy: "orc", actor: "bob", frames: 3 }) combat.threat.target("orc") // "bob", even after a 1000-point hit on ann; ann again once the taunt has run out ``` ## Settings and defaults | Setting | Default | Meaning | | --- | --- | --- | | `maxEnemies` | 64 | {{s_maxEnemies}} | | `maxActors` | 40 | {{s_maxActors}} | | `healMultiplier` | 0.5 | {{s_healMultiplier}} | | `meleeThreshold` | 1.1 | {{s_meleeThreshold}} | | `rangedThreshold` | 1.3 | {{s_rangedThreshold}} | ## Services - `combat.threat` (service): `create(id, config)`, `request(id, command)`, `remove(id)`, `ids()` and `target(enemy)`, which answers the id of the chosen actor or `null`. The instance `default` uses the installed settings and is the one fed by combat events. - Commands of `request`: `engage` (`enemy`, `actor`, `melee`: admit a fighter, melee by default), `damage` (`sequence`, `enemy`, `actor`, `amount`), `heal` (`sequence`, `actor`, `beneficiary`, `amount`), `taunt` (`enemy`, `actor`, `frames`: 1 to 3600), `forget` (`actor`: remove from every table), `evade` (`enemy`: drop the encounter), `step` (one fixed frame), plus `status`, `config`, `save`, `load`, `preview`. `sequence` must rise with every damage or heal; invalid, stale or over-capacity commands change nothing. - What feeds the table: `combat.damaged` counts only when the attacker is engaged with that enemy (call `engage` first), `combat.healed` adds threat for the healer on the enemies that know the healed actor, and `combat.killed` or `entity.despawned` removes the actor everywhere (a dead enemy drops its own table). The target changes on the next fixed step. ## Events - combat.threat.created and combat.threat.removed (an instance appeared or went), combat.threat.changed (a decided command applied, with its `op` and `result`; never a read or a `step`), combat.threat.refused (a hook cancelled a command, with `by` and `reason`). ## Hooks and modding - `combat.threat.create`: an instance is about to be created; a tap may change its `config` or cancel, and `create` then fails with the reason. - `combat.threat.request`: a decided command (every `op` except the reads and `step`) is about to apply; a tap may change the `command` or cancel, and `request` then answers `{ ok: false, cancelled: true, by, reason }` with the instance unchanged. ## Using it from content Every setting is moddable. A `combat_threat_presets` document `{ id, extends?, config }` names a ready-made set of settings, and `create(id, { preset: "" })` takes it (own keys win). A hook document of a game or a mod looks like this: ```json { "id": "no_taunt_on_bosses", "hook": "combat.threat.request", "match": { "op": "taunt" }, "cancel": true, "reason": "immune" } ``` Script verbs: the action `combat_threat_request` sends a `command` to an instance (default `default`) through the request hook, and the condition `combat_threat_exists` tells whether an instance exists. ## When it does not do what you expect | Symptom | Cause and fix | | --- | --- | | The target never changes | A challenger must exceed the current target by `meleeThreshold` or `rangedThreshold`, and the selection runs on the fixed step: let a step pass, or use a `taunt`. | | Damage adds no threat | The attacker was never engaged with that enemy: send `engage` first. Only the game decides life, teams, visibility and distance. | | A command fails with "stale combat sequence" | `sequence` did not rise: use a number higher than every earlier damage or heal of this instance. | ## Limits - It keeps no health, physics or AI of its own: it only answers who the enemy should target. - Leash distance and resetting a monster's health after `evade` belong to your game. - It has no display module and no variables; it runs on the web player and the native Rust host. ## Versions Kit `combat.threat` 1.0.0 (since 2026-10-25). It needs `game.engine` ^1. A saved game keeps every instance; restore it with the same settings.