Threat

Usage guide of combat.threat: per-enemy threat tables fed by combat events, healing threat, target hysteresis and timed taunts, settings and defaults, service, commands, hooks and events.

Source: module-kits:kits/combat_threat/usage/en.mdView Markdown source

Also read offline — CLI: grip-forge docs combat-threatMCP tool: gripforge_docs { "topic": "combat-threat" }

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.

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 #

<!-- test: kits/game_engine/host/tests/combat_threat.rs::a_taunt_forces_the_target_until_it_runs_out --> 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.

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 #

SettingDefaultMeaning
maxEnemies64{{s_maxEnemies}}
maxActors40{{s_maxActors}}
healMultiplier0.5{{s_healMultiplier}}
meleeThreshold1.1{{s_meleeThreshold}}
rangedThreshold1.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: "<id>" }) takes it (own keys win). A hook document of a game or a mod looks like this:

{ "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 #

SymptomCause and fix
The target never changesA 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 threatThe 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.