# Enemies in your game (enemy.* kits) Enemies, bosses, hordes and stalkers described as data on top of the NPC kit and the combat kits. Readable offline: `gripforge docs enemies`, or the MCP tool `gripforge_docs` (topic `enemies`). ## What it is for An enemy is an NPC (see the NPC guide) with health, a team and attacks. Nothing here swings a sword: the strikes are `combat.melee` strikes, the damage goes through `combat.core` (see the Combat guide), the walking and seeing are `npc.core`. | Kit | What it adds | | --- | --- | | `enemy.ai` | the `enemies` collection: health, team, attacks picked by reach, weight and cooldown; the behaviours `hunt`, `melee`, `lunge`, `stagger`; spawning and waves as data | | `enemy.boss` | boss fights: health-gated phases that cycle telegraphed patterns (slam, sweep, lunge, nova, summon), an intro, a health bar, a defeat event | | `enemy.horde` | slow numerous enemies: ambient crowd, waves by night or timer, noise that attracts, a leash, optional infection, win and lose rules | | `enemy.stalker` | one hunter that cannot be killed: it roams closer, listens, chases on sight, withdraws to its lair, rests, comes back | ## Turn it on ```text gripforge_gamekit_install { project_id: "gkp_…", id: "enemy.ai" } gripforge_gamekit_configure { project_id: "gkp_…", id: "enemy.ai", config: { "maxEnemies": 24, "aggression": 1.2 } } ``` `enemy.ai` requires `npc.core`, `combat.core` and `combat.melee`; `enemy.boss` requires `enemy.ai`; `enemy.horde` requires `enemy.ai`, `npc.core` and `spawn.entities`; `enemy.stalker` requires `stealth.detection` and `npc.core`. Installing one brings what it needs. ## The enemies document ```json { "id": "shade", "name": "Shade", "npc": "shade_npc", "health": 60, "money": 5, "attacks": [ { "id": "claw", "shape": "arc", "range": 2, "arc": 100, "damage": 8, "windup": 0.4, "active": 0.15, "recovery": 0.5, "cooldown": 1.5, "weight": 3 }, { "id": "pounce", "shape": "line", "range": 5, "width": 1.2, "damage": 14, "windup": 0.7, "active": 0.2, "recovery": 0.8, "cooldown": 4, "lunge": 4 } ], "stagger": { "threshold": 0.25, "seconds": 0.8 } } ``` `npc` is an id of the project's NPC documents or an inline NPC. `attacks[]` also take `reach`, `knockback` and `contact` (`{ lineOfSight: true, heightTolerance: 1.5, maxTargets: 1 }`: a wall blocks the damage during the active window). An enemy with a `boss` key runs the boss behaviour. A document `{ "id": "new", "extends": "shade" }` of a game or a mod inherits an entry. Place enemies as instances: an archetype of kind `enemy` with the component `enemy { def }`, or spawn them from content: `{ "spawn_enemy": { "enemy": "shade", "count": 3, "at": { "zone": "pit" }, "spread": 4 } }` (the position can also be a point or `{ entity, offset }`; the default is 4 m beside the player). Variable `enemies.alive` counts the live ones. ## Allies and teams `team` (default `enemy`) is the combat team the NPC is registered with. An ally is a document on the players' team: ```json { "id": "sergeant", "name": "Sergeant", "npc": "sergeant_npc", "health": 180, "team": "player", "attacks": [{ "id": "cut", "shape": "arc", "range": 1.8, "arc": 120, "damage": 12, "windup": 0.3, "active": 0.12, "recovery": 0.4, "cooldown": 1.2 }] } ``` As soon as one NPC of the game is on another team than `enemy`, every NPC goes for the nearest hostile: a player, or an NPC of this kit whose `combat.team` differs from its own. Allies hunt the other teams and never hurt the players, since `combat.melee` only hits other teams; enemies go for an ally standing closer than the player. Any other team name is a faction that fights everyone else. A hunter keeps its target while it stays targetable unless another hostile is more than 1.5 m closer. With `perception`, sight, memory and line of sight bound who is seen for every team: give allies a sight range so they do not run across the map. The team is read live from the `combat.team` component and kept in the save. Allies count among the kit's live NPCs and emit `enemy.killed`, so give them no `money`. Without another team the targeting is unchanged. ## Settings and defaults ### enemy.ai | Key | Default | Meaning | | --- | --- | --- | | `perception` | `true` | use `npc.core` vision (range, field of view, obstacles), last-seen memory and alerts on confirmed damage; off keeps the older "always know where the player is" contract | | `aggression` | 1 | attack cooldowns are divided by this | | `maxEnemies` | 32 | live enemy cap; a spawn beyond it answers nothing | | `separation` | 1.1 | personal space between enemies (m) | | `healthScaleVar` | `difficulty.enemy_health` | a variable that multiplies the health of each new enemy (difficulty, co-op) | With perception on, author a patrol (`behavior.initial: patrol`, allowed behaviours patrol, hunt, melee, stagger, and a real walkable route): a calm guard that sees the player hunts; an occluded or lost target is pursued to the last place it was seen until `perception.memory` runs out, then the patrol resumes. No attack starts without visibility. ### enemy.boss `introSeconds` 1.5 (the wait after spawning), `telegraphScale` 1 (multiplies every windup). A boss is a document of `bosses`: ```json { "id": "warden", "name": "The Warden", "patterns": { "slam": { "type": "slam", "damage": 20, "radius": 4, "windup": 0.9, "at": "target" }, "rings": { "type": "nova", "damage": 12, "rings": 3, "radiusStep": 3, "interval": 0.3, "windup": 1.0 } }, "phases": [ { "id": "p1", "untilHealth": 0.5, "patterns": ["slam"], "pause": 2 }, { "id": "p2", "untilHealth": 0, "patterns": ["slam", "rings"], "pause": 1.2, "enrage": 1.3 } ] } ``` Patterns: `slam` (circle on the boss or on where the player stood), `sweep` (arc), `lunge` (line, the boss charges along it), `nova` (expanding rings), `summon` (`enemy`, `count`). Each accepts an optional `recovery` (0 to 60 s) in the Rust player. The phase is the first whose `untilHealth` is below the health ratio. Variables `boss.id`, `boss.name`, `boss.health`, `boss.max` feed a health bar. ### enemy.horde Defaults: `ambient` 6 around the players by day, `nightMultiplier` 2, `spawnEvery` 3 s, `spawnMin` / `spawnMax` 18 / 36 m, `despawnBeyond` 80, `maxAlive` 40, `aggro` 12 m, `hearing` 1, `leash` 2.5, `waveMode` `auto` (night with `world.daynight`, else timer), `waveFirst` 60 s, `waveEvery` 120 s, `waveBase` 8, `waveGrowth` 3, `waveMax` 60, `waveSeconds` 20, `winAfterWaves` 0 (endless), `loseOnDeath` `true`, `finish` `true`. Footsteps make noise above `speed` 5.5 m/s; infection is off (`enabled`), `chance` 0.2, `seconds` 30, `damagePerSecond` 1. Types are documents of `horde_types`: `{ "id": "walker", "enemy": "zombie_walker", "weight": 6, "fromWave": 1, "aggro": 12, "hearing": 1, "wander": 8 }`. Variable `horde.result` is `won` or `lost`; with `game.endings` write endings on it. ### enemy.stalker `huntSeconds` 90, `restSeconds` 30, `roamRadius` 10, `retreatSeconds` 25, `retreatOnCatch` `true`, `catchDamage` 0 (the game decides what a catch means), `invulnerable` `true`, and an optional `perceiver` (default: fov 120, range 18, hearing 1.5, catch 1.3). The stalker is an NPC carrying `stalker { lair }`. Its random draws come from a seeded stream saved with the game, so a loaded game roams to the same points. ## Verbs, events, variables | Kit | Verbs | Events | | --- | --- | --- | | `enemy.ai` | `spawn_enemy` | `enemy.spawned`, `enemy.alerted` (reason `sight` or `impact`), `enemy.attack`, `enemy.staggered`, `enemy.killed` | | `enemy.boss` | | `boss.started`, `boss.phase`, `boss.pattern`, `boss.defeated` | | `enemy.horde` | `horde_wave`, `horde_noise`, `horde_cure`, `horde_clear` | `horde.wave`, `horde.wave_cleared`, `horde.noise`, `horde.hunting`, `horde.lost_track`, `horde.killed`, `horde.infected`, `horde.cured`, `horde.won`, `horde.lost` | | `enemy.stalker` | `summon_stalker`, `stalker_retreat` | `stalker.spotted`, `stalker.caught`, `stalker.retreated`, `stalker.returned` | ## Hooks and modding | Hook | Payload | What a tap may do | | --- | --- | --- | | `enemy.spawn` | `{ enemy, x, y, z, tags, health }` | change `health`, cancel the spawn | | `enemy.attack` | `{ entity, enemy, attack, target, distance }` | pick another ready attack, cancel for this tick | | `enemy.stagger` | `{ entity, enemy, amount, seconds }` | change `seconds`, cancel | | `enemy.reward` | `{ entity, enemy, source, money }` | change `money` | | `stalker.catch`, `stalker.retreat`, `stalker.hunt` | entity, damage or hunt time | change `damage` or `huntSeconds`, cancel | The damage of a strike is the `combat.damage` hook of `combat.core` (kind `enemy`). ```json { "hook": "enemy.spawn", "match": { "enemy": "shade" }, "mul": { "health": 2 } } ``` Moddable: every setting above and the collections `enemies`, `bosses` and `horde_types`. ## Engines Enemies, bosses and the rest run as the same Rust logic on the web and in the native player. `enemy.ai` and `enemy.boss` are also delivered to Godot and Unity as files and to Unreal through the shared kit library; `enemy.horde` and `enemy.stalker` run on the web and in the native player. The hooks, the perception option, teams and the optional boss recovery are served by the Rust player; the older engine scripts keep their fixed behaviour. ## When it does not do what you expect | Symptom | Cause and fix | | --- | --- | | An enemy stands still | its NPC document does not list `hunt` / `patrol` among its behaviours, or it has not seen the player (perception on) | | An enemy never attacks | no attack is ready and in reach, or `combat.melee` is missing (attacks then keep their timing but strike nothing) | | A spawn returns nothing | `maxEnemies` is reached, the enemy id is unknown, or a tap cancelled `enemy.spawn` | | The boss skips a pattern | the pattern id is missing or malformed in the document (a warning is logged) | | Waves never come | `waveMode` is `manual`, or `auto` without `world.daynight` and with a long `waveFirst` | | The stalker dies | `invulnerable` was turned off |