# 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](guide:npcs)), and the player's side (takedowns, hiding) is in [Stealth takedowns](guide:stealth-takedown). ## Turn it on ```text 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: ```json { "id": "guard", "kind": "npc", "components": { "npc": { "doc": "guard" }, "perceiver": { "fov": 100, "focus": 50, "range": 16, "group": "guards" } } } ``` ## First example 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: ```text 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 at `range`. - angle: 1 inside the `focus` cone, `peripheral` inside the `fov` cone, 0 behind. - light: `minLight + (1 - minLight) x level`. The level is `ambient`, raised by `light_zone` volumes, lamps of the world lighting and lights other kits add, then multiplied by `1 - depth` inside a `shadow_zone`. - posture: the factor of `postures` for crouch, walk or run. - 0 when an `occluder` stands between the eye (`eye`) and the target, when the target is hidden, or when it crouches in `cover` farther than `reveal` away. 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) and `stealth.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_noise` and `alert_perceivers`, condition `stealth_state`, NPC behaviours `stealth_watch`, `stealth_search` and `stealth_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 | ```json { "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`), or `ambient` and `minLight` make the target nearly invisible. - The player is seen through a wall: the wall has no `occluder` component. - A camera never searches: an entity without `npc` only 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.