Stealth detection
Usage guide of stealth.detection: guards, creatures and cameras that see through a vision cone, light and cover and hear noises, with suspicion, four awareness states, light and cover volumes, settings, service, events and hooks.
Stealth perception (stealth.detection) #
What it is for #
The perception side of an infiltration or horror game. Any entity that carries a perceiver component watches and listens for the player: it sees through a vision cone, only along a clear line of sight, better in light than in shadow, less a crouching player than a running one, and it hears footsteps, thrown objects and gunshots. Each perceiver fills a suspicion meter and moves through four states: unaware, suspicious, searching, alert. The kit decides what is noticed; what a guard does while patrolling stays with its NPC document (see NPC guide), and the player's side (takedowns, hiding) is in Stealth takedowns.
Turn it on #
gripforge_gamekit_install { project_id: "gkp_…", id: "stealth.detection" }
gripforge_gamekit_configure { project_id: "gkp_…", id: "stealth.detection",
config: { "ambient": 0.25, "sightRate": 60, "alertRadius": 12 } }
It requires npc.behavior, npc.locomotion, input.actions and game.engine, so installing it brings them. Give an entity the perceiver component in an archetype:
{ "id": "guard", "kind": "npc", "components": { "npc": { "doc": "guard" }, "perceiver": { "fov": 100, "focus": 50, "range": 16, "group": "guards" } } }
First example #
<!-- test: kits/game_engine/host/tests/stealth_detection.rs::a_filter_answering_false_blinds_the_perceiver_and_leaves_with_its_remover --> Another kit hands a perception filter to the service with addFilter(owner, fn). While the filter answers false, visibility(perceiver, target) returns a factor of 0 and los: false; removing the filter restores the normal factor:
visibility ["e1", "e2"] -> { factor: > 0, distance: 5, los: true, ... }
(filter answers false)
visibility ["e1", "e2"] -> { factor: 0, distance: 5, los: false, ... }
removeFilter ["test.giver", fn] -> the factor is back above 0
How a perceiver sees #
visibility = distance x angle x light x posture.
- distance: 1 up to
near, then falling linearly to 0 atrange. - angle: 1 inside the
focuscone,peripheralinside thefovcone, 0 behind. - light:
minLight + (1 - minLight) x level. The level isambient, raised bylight_zonevolumes, lamps of the world lighting and lights other kits add, then multiplied by1 - depthinside ashadow_zone. - posture: the factor of
posturesfor crouch, walk or run. - 0 when an
occluderstands between the eye (eye) and the target, when the target is hidden, or when it crouches incoverfarther thanrevealaway.
Seen, suspicion grows by sightRate x visibility per second; not seen, it falls by decayRate. A noise adds loudness x (1 - d / (radius x hearing)).
| State | Entered when | Does |
|---|---|---|
| unaware | start, end of a search | its NPC behaviour (patrol) |
| suspicious | suspicion reaches suspiciousAt | stops and stares at the last known position |
| searching | nothing perceived for calmSeconds; lost the target for loseSeconds; called by an ally; a body seen | walks to the point, looks around for searchSeconds, then returns to its patrol |
| alert | suspicion 100 | chases; allies within alertRadius search the spot; within its catch distance it catches the target |
Settings and defaults #
| Setting | Default | Meaning |
|---|---|---|
ambient | 0.25 | light level where nothing says otherwise (0 dark, 1 daylight) |
minLight | 0.2 | visibility factor in total darkness |
lampFull | 2 | intensity of a lamp that lights fully at its foot |
sightRate | 60 | suspicion per second at visibility 1 |
decayRate | 15 | suspicion lost per second while nothing is perceived |
suspiciousAt | 30 | suspicion from which a perceiver stares |
calmSeconds | 2 | a suspicious perceiver perceiving nothing this long goes to check |
searchSeconds | 8 | time spent looking around the last known position |
searchTimeout | 20 | a search that cannot reach its point gives up after this long |
loseSeconds | 3 | an alert perceiver that lost the target searches after this long |
catchCooldown | 3 | seconds before the same perceiver can catch again |
bodySight | 0.15 | visibility above which a downed body is noticed |
alertRadius | 12 | allies within it search when a perceiver goes alert or finds a body |
postures | crouch 0.5, walk 1, run 1.5 | visibility factor by posture |
footsteps | crouch 0, walk 4, run 10 | footstep noise radius by posture |
stepSeconds | 0.5 | seconds between two footsteps |
footstepLoudness | 25 | suspicion a footstep adds at its source |
runAbove | 5.5 | player speed (m/s) from which the posture is run |
throwItem | empty | item spent by a throw; empty: free |
throwRange, throwRadius, throwLoudness, throwCooldown | 8, 10, 60, 1.5 | where a thrown object lands, its noise radius and loudness, its cooldown |
noiseEvents | combat.fired, radius 30, loudness 100, kind gunshot | bus events that make a noise where they happen |
defaults | empty | defaults of every perceiver (fov 100, focus 50, range 16, near 4, peripheral 0.5, hearing 1, catch 1.2) |
The per-entity components are perceiver (fov, focus, range, near, peripheral, hearing, eye, group, catch, enabled) and the volumes light_zone, shadow_zone, cover, occluder and noise_surface (a radius or a box size; level, depth, crouch, reveal, solid or factor).
Services, events, variables #
stealth.detection(service):perceivers(),perceiver(entity),visibility(perceiver, target?),lightAt(point),noise(at, radius, opts),alert(entity, at?),posture(),crouched(),setCrouch(on),throwDistraction(),setHidden(entity, on, owner),hidden(entity),addLight(owner, fn),addFilter(owner, fn),enable(entity, on),indicators(),awareness().- Inputs:
stealth.crouch(Z) andstealth.throw(O). - Events:
stealth.state(a perceiver changed state),stealth.noise(a noise was heard by some perceivers),stealth.alerted(an ally was called),stealth.body_found,stealth.caught,stealth.crouched. - Variables:
stealth.state,stealth.suspicion,stealth.light,stealth.posture,stealth.hidden,stealth.seen,stealth.caught. - Script verbs: actions
make_noiseandalert_perceivers, conditionstealth_state, NPC behavioursstealth_watch,stealth_searchandstealth_chase.
Hooks and modding #
Without a tap nothing changes. A game or a mod taps these decisions with a hooks document:
| Hook | Payload | Effect |
|---|---|---|
stealth.detect | perceiver, target, distance, angle, light, posture, multiplier | mutable multiplier; cancelled: visibility 0 (filters are taps of this hook) |
stealth.hear | x, y, z, radius, loudness, kind, source | mutable radius and loudness; cancelled: nobody hears it |
stealth.ally.call | entity, by, x, y, z | cancelled: this ally is not called |
stealth.body.find | entity, body | cancelled: the body stays unfound for now |
stealth.catch | entity, target, distance | cancelled: no catch, asked again next tick |
stealth.crouch | on | cancelled: the posture does not change |
{ "id": "dim_guards", "hook": "stealth.detect", "match": { "posture": "crouch" }, "mul": { "multiplier": 0.5 } }
A mod may override every setting above.
Engines #
The same Rust rules run in the web player and the native kit host. The line of sight and the throw are ray casts through the physics port, and an occluder is a static box on it. When audio.acoustics is available, hearing uses rooms, doors and obstacles; otherwise the legacy hearing applies. The kit draws an awareness HUD: one arrow per suspicious perceiver, filled by its suspicion.
When it does not do what you expect #
- A guard never reacts: the entity has no
perceiver, it is disabled (enable), orambientandminLightmake the target nearly invisible. - The player is seen through a wall: the wall has no
occludercomponent. - A camera never searches: an entity without
npconly raises the alarm.
Limits #
- It decides perception only: patrols, chases and attacks belong to the NPC and combat kits.
- Perceivers are checked against the player.
Versions #
Kit 1.2.0, service contract 1.0.0. 1.2.0: optional audio.acoustics hearing; 1.1.0: the game.engine hook contract; 1.0.0: first release.