Enemies
Usage guide of enemy.ai, enemy.boss, enemy.horde and enemy.stalker: the enemies document, attacks, allies and teams, boss phases and patterns, waves and noise, the stalker cycle, settings and defaults, verbs, hooks, engines.
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 #
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 #
{ "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 #
<!-- test: kits/game_engine/host/tests/enemy_teams.rs::an_ally_hunts_and_damages_an_enemy_and_never_the_player --> team (default enemy) is the combat team the NPC is registered with. An ally is a document on the players' team:
{ "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:
{ "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).
{ "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 |