# Team vision and fog of war (world.vision) ## What it is for `world.vision` computes what each team sees and puts a fog over the rest. `updateHz` times a second, every living entity that has a team (`combat.health`, or a `team_` tag) lights a disc around it, with ward beacons and scripted reveals; each team's grid is cast against the walls a world kit serves (`world.occluders`) and merged into its fog. The ground is visible, explored (seen before, dimmed) or unexplored (dark). Entities of another team that the drawn team does not see get the tag `unseen` (not drawn, absent from the mini-map, not targeted by the other team's AI), except those tagged with one of `alwaysVisible`. Use it for lane arenas, strategy and stealth games with teams. The presentation module draws the fog as a post pass after tone mapping, and the wards of the drawn team. ## Turn it on ```json { "id": "world.vision", "config": { "range": 11, "startExplored": true, "wardCharges": 2 } } ``` It requires `game.engine`. It reads the ground from `world.ground`, walls from `world.occluders` and bushes from `world.thickets`; `moba.lanes` serves both. The input action `vision.ward` places a ward (key 4 by default). ## First example A blue player-hero at `(-20, -20)` with a 16-cell grid and `range: 9`, red units around it, a wall and two thickets served by a test kit. After one step, `visible(entity)` answers per entity: a red minion within `ranges.minion` and in the clear is seen, a red boss behind the wall is not, and one in a thicket is seen only from close. Scripts can ask the same with the condition `in_sight`, and `reveal_area` lights a disc for a few seconds. ## Settings and defaults | Setting | Type | Default | Meaning | |---|---|---|---| | `range` | number 1-200 | `11` | Sight radius (m) when no tag of `ranges` matches. | | `ranges` | object | hero 14, player 14, structure 15, minion 9, ward 11 | Radius by entity tag, first tag found wins. | | `cells` | integer 16-256 | `112` | Cells per side of the vision grid. | | `size` | number | `0` | Side of the ground (m); 0 is the extent of `world.ground`. | | `updateHz` | number 1-30 | `8` | Vision updates per second. | | `lineOfSight` | boolean | `true` | Walls stop the sight. | | `thicketReveal` | number 0-20 | `4` | A unit in a thicket is seen from this close (m), or from inside the same one. | | `startExplored` | boolean | `false` | The whole map is known (dimmed) from the start. | | `alwaysVisible` | array | `["structure", "static"]` | Tags that stay drawn under the fog. | | `neutralTeams` | array | `["neutral"]` | Teams that give sight to nobody. | | `team` | string | `""` | The team whose sight is drawn; empty is the player's team. | | `announce` | array | `["hero", "boss"]` | Tags whose sightings emit `vision.spotted` and `vision.lost`. | | `color` | string | `#04060b` | Fog colour. | | `exploredAlpha` | number 0-1 | `0.52` | Opacity over ground seen before. | | `unexploredAlpha` | number 0-1 | `0.9` | Opacity over ground never seen. | | `wardCharges` | integer 0-9 | `2` | Wards the player holds; 0 is none. | | `wardSeconds` | number | `90` | Life of a ward. | | `wardRange` | number | `11` | Sight radius of a ward (m). | | `wardRecharge` | number | `45` | Seconds to regain a charge. | | `wardReach` | number | `7` | How far from the player a ward can be placed (m). | ## Services, events, variables - `world.vision` (service): `team()` is the drawn team; `visible(entity, team)` and `visibleAt(x, z, team)` (the drawn team by default); `fog(team)` returns `{ size, cells, data, version }` (0 unexplored, 1 explored, 2 visible) or null before the first update; `reveal({ at, radius, seconds, team })`; `placeWard(at, team)` (false when refused or out of charges); `wards()`; `look()`; `view()` for a renderer. - `vision.spotted` (event): `{ entity, team }`, an announced enemy came into sight. - `vision.lost` (event): `{ entity, team }`, it went out of sight. - `vision.ward` (event): `{ team, x, z, seconds }`, a ward was placed. - `scene.entered` (listened): the grid, explored ground, wards and reveals start over. - Variables: `vision.wards` (charges held) and `vision.ward_in` (seconds to the next charge, 0 when full). - Script verbs: action `reveal_area` (`{ x, z, radius, seconds, team? }`), condition `in_sight` (`{ entity, team? }`). What was explored and the wards are saved, and the time of the next charge too. ## Hooks and modding All three are cancellable and asked once per change, not on every update: - `vision.spot` `{ entity, team, announce }`: before an entity comes into sight; mutable `announce`; a cancel keeps it unseen until it has gone out of sight and come back (a cloak). - `vision.lose` `{ entity, team, announce }`: before it goes out of sight; mutable `announce`; a cancel keeps it drawn until it has come back and left again (a mark). - `vision.place_ward` `{ team, x, z, seconds, range }`: mutable `seconds` and `range`; a cancel places no ward and the player keeps the charge. ```json { "id": "cloak", "hook": "vision.spot", "match": { "entity": "e7" }, "cancel": true, "reason": "cloaked" } ``` A mod may override the sight, ward and look settings (all but `cells`, `size` and `team`). ## Engines The rules are the same Rust logic on the web and the native player, which draw the fog. A world kit serves `world.occluders` (`walls()`: points and thickness) and `world.thickets` (`thickets()`: position and radius). ## When it does not do what you expect - Everything is dark: no entity has a team, or `startExplored` is false and nothing has moved yet. - An enemy is never hidden: it carries a tag of `alwaysVisible`, or its team is not different from the drawn team. - Walls do not block sight: no kit serves `world.occluders`, or `lineOfSight` is false. - A ward does nothing: no charge left (`vision.wards` is 0), the point is beyond `wardReach`, or a `vision.place_ward` tap cancels. ## Limits It draws no minimap and makes no combat decisions; the `unseen` tag is what other kits read. Resolution is limited by `cells`. ## Versions Kit 1.1.2; it provides `world.vision` 1.0.0 and requires `game.engine@^1`. The hooks date from 1.1.0.