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.

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

Also read offline — CLI: grip-forge docs enemiesMCP tool: gripforge_docs { "topic": "enemies" }

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.

KitWhat it adds
enemy.aithe enemies collection: health, team, attacks picked by reach, weight and cooldown; the behaviours hunt, melee, lunge, stagger; spawning and waves as data
enemy.bossboss fights: health-gated phases that cycle telegraphed patterns (slam, sweep, lunge, nova, summon), an intro, a health bar, a defeat event
enemy.hordeslow numerous enemies: ambient crowd, waves by night or timer, noise that attracts, a leash, optional infection, win and lose rules
enemy.stalkerone 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 #

KeyDefaultMeaning
perceptiontrueuse 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
aggression1attack cooldowns are divided by this
maxEnemies32live enemy cap; a spawn beyond it answers nothing
separation1.1personal space between enemies (m)
healthScaleVardifficulty.enemy_healtha 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 #

KitVerbsEvents
enemy.aispawn_enemyenemy.spawned, enemy.alerted (reason sight or impact), enemy.attack, enemy.staggered, enemy.killed
enemy.bossboss.started, boss.phase, boss.pattern, boss.defeated
enemy.hordehorde_wave, horde_noise, horde_cure, horde_clearhorde.wave, horde.wave_cleared, horde.noise, horde.hunting, horde.lost_track, horde.killed, horde.infected, horde.cured, horde.won, horde.lost
enemy.stalkersummon_stalker, stalker_retreatstalker.spotted, stalker.caught, stalker.retreated, stalker.returned

Hooks and modding #

HookPayloadWhat 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.huntentity, damage or hunt timechange 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 #

SymptomCause and fix
An enemy stands stillits NPC document does not list hunt / patrol among its behaviours, or it has not seen the player (perception on)
An enemy never attacksno attack is ready and in reach, or combat.melee is missing (attacks then keep their timing but strike nothing)
A spawn returns nothingmaxEnemies is reached, the enemy id is unknown, or a tap cancelled enemy.spawn
The boss skips a patternthe pattern id is missing or malformed in the document (a warning is logged)
Waves never comewaveMode is manual, or auto without world.daynight and with a long waveFirst
The stalker diesinvulnerable was turned off