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.
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 #
| 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()andtarget(enemy), which answers the id of the chosen actor ornull. The instancedefaultuses 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), plusstatus,config,save,load,preview.sequencemust rise with every damage or heal; invalid, stale or over-capacity commands change nothing. - What feeds the table:
combat.damagedcounts only when the attacker is engaged with that enemy (callengagefirst),combat.healedadds threat for the healer on the enemies that know the healed actor, andcombat.killedorentity.despawnedremoves 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
opandresult; never a read or astep), combat.threat.refused (a hook cancelled a command, withbyandreason).
Hooks and modding #
combat.threat.create: an instance is about to be created; a tap may change itsconfigor cancel, andcreatethen fails with the reason.combat.threat.request: a decided command (everyopexcept the reads andstep) is about to apply; a tap may change thecommandor cancel, andrequestthen 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 #
| 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
evadebelong 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.