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.

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

Also read offline — CLI: grip-forge docs world-visionMCP tool: gripforge_docs { "topic": "world-vision" }

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 #

SettingTypeDefaultMeaning
rangenumber 1-20011Sight radius (m) when no tag of ranges matches.
rangesobjecthero 14, player 14, structure 15, minion 9, ward 11Radius by entity tag, first tag found wins.
cellsinteger 16-256112Cells per side of the vision grid.
sizenumber0Side of the ground (m); 0 is the extent of world.ground.
updateHznumber 1-308Vision updates per second.
lineOfSightbooleantrueWalls stop the sight.
thicketRevealnumber 0-204A unit in a thicket is seen from this close (m), or from inside the same one.
startExploredbooleanfalseThe whole map is known (dimmed) from the start.
alwaysVisiblearray["structure", "static"]Tags that stay drawn under the fog.
neutralTeamsarray["neutral"]Teams that give sight to nobody.
teamstring""The team whose sight is drawn; empty is the player's team.
announcearray["hero", "boss"]Tags whose sightings emit vision.spotted and vision.lost.
colorstring#04060bFog colour.
exploredAlphanumber 0-10.52Opacity over ground seen before.
unexploredAlphanumber 0-10.9Opacity over ground never seen.
wardChargesinteger 0-92Wards the player holds; 0 is none.
wardSecondsnumber90Life of a ward.
wardRangenumber11Sight radius of a ward (m).
wardRechargenumber45Seconds to regain a charge.
wardReachnumber7How 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.
{ "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.