Vision and fog
Compute what each team sees: sight radii, walls, thickets, fog of war, the unseen tag, wards and reveals; settings, services, events, the vision.spot, vision.lose and vision.place_ward hooks.
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_<name> 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 #
{ "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 #
<!-- test: kits/game_engine/host/tests/parity.rs::rust_host_matches_the_typescript_traces (fixture world.vision.teams) --> 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)andvisibleAt(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) andvision.ward_in(seconds to the next charge, 0 when full). - Script verbs: action
reveal_area({ x, z, radius, seconds, team? }), conditionin_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; mutableannounce; 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; mutableannounce; a cancel keeps it drawn until it has come back and left again (a mark).vision.place_ward{ team, x, z, seconds, range }: mutablesecondsandrange; a cancel places no ward and the player keeps the charge.
{ "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
startExploredis 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, orlineOfSightis false. - A ward does nothing: no charge left (
vision.wardsis 0), the point is beyondwardReach, or avision.place_wardtap 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.