Repository navigation
Grid Cell Data
When you export a map to a Foundry Scene with a grid enabled (set in the Cartography tool's Grid section), Filroden's World Map Builder can also write a structured summary of every grid cell straight onto that Scene, for other modules and game systems to read. This page explains what that is in plain terms, documents its exact shape for anyone building against it, and gives a working example macro.
Available from version 2.6.0.
Alongside the background image and journal a normal export already creates, the module writes a hidden summary of every cell on the map's grid: its dominant biome, terrain, moisture and temperature, whether a river runs through it or it borders the coast, and any of your regions, pins or routes that touch it.
You won't see this anywhere in Foundry, and it has no effect on how your map looks or plays. It's written purely so other modules and game systems can react to where a token actually is on your map, without having to work that out for themselves. The motivating example is a system that rolls a different random encounter depending on the cell a party's token has just entered (e.g., a swampy lowland forest suggesting something different from an open desert) but anything that wants to know "what's at this grid cell" can use it: refusing to spawn a monster inside a settlement you've marked as a Region, for instance, or colouring a hex-crawl overlay by terrain.
A few things worth knowing before you rely on it:
- It's calculated fresh every time you export to a Scene, from the map as it stood at that export. Re-export after editing the map if you want the data to catch up.
- It doesn't track anything live. There's no token position, no "has this cell been visited," no encounter logic baked in. All of that is left to whatever module or system reads it.
- A map exported with no grid has no cells to summarise, so this is skipped entirely for that Scene.
If you're not building a module or game system yourself, there's nothing here you need to do. This section exists for developers, and the rest of this page is written for them.
The data is written to the exported Scene as a Foundry flag under this module's ID:
const gridData = scene.getFlag("filrodens-world-map-builder", "gridData");gridData is undefined on any Scene the module hasn't exported grid data to, either because it predates this feature or because the map it came from had no grid.
Cells are addressed the same way Foundry itself addresses a grid cell: an offset pair { i, j }, where i is the row and j is the column. This is the same structure Foundry uses for square grids and both hexagonal orientations, and it's the same coordinate you'd get back from Foundry's own grid API (canvas.grid.getOffset(point)) for a token's position on that Scene, so converting "where is this token" into a lookup key needs no maths of its own.
In the data, a cell's offset is the object key "i,j" (for example, "3,7" for row 3, column 7).
{
"schemaVersion": 1,
"generatedAt": "2026-09-22T12:00:00Z",
"grid": {
"type": 2,
"typeName": "HEXODDR",
"size": 100
},
"bounds": { "iMin": 0, "iMax": 19, "jMin": 0, "jMax": 24 },
"cells": {
"3,7": { "...": "see below" }
}
}| Field | Type | Notes |
|---|---|---|
schemaVersion |
number | Increments only on a breaking change to this shape. Check this before parsing. |
generatedAt |
string | ISO timestamp of when this data was computed, that is, when the map was last exported to this Scene. |
grid.type |
number | Foundry's own CONST.GRID_TYPES value, identical to scene.grid.type on this Scene. Use this, not typeName, if your code branches on grid shape. |
grid.typeName |
string | The same value as a human-readable label, for convenience: "SQUARE", "HEXODDR" or "HEXODDQ" (this module only ever generates odd-offset hex grids, never even). |
grid.size |
number | Pixels per cell, matching scene.grid.size. Never below 20, the smallest grid Foundry allows. |
bounds |
object | The offset range covered by cells, inclusive. Iterate this to walk the whole map without guessing extents. |
cells |
object | See below. Every offset within bounds is always present. |
"3,7": {
"biome": { "id": "GRASSLAND", "name": "Grassland", "code": null },
"terrainBand": "lowland",
"moistureBand": "moderate",
"temperatureBand": "temperate",
"hasRiver": false,
"rivers": [ { "id": "Qw7Nf2Rt8Lc5Xp1V", "name": "The Silverrun" } ],
"isCoastal": true,
"regions": [
{ "id": "TR8k2mFq9xLp3Zc7", "name": "The Whispering Woods", "layer": { "id": "aWn4Vb1sQd6Yh0Ku", "name": "Forests" }, "visibility": "all", "coverage": 0.82 }
],
"infrastructure": {
"pins": [ { "id": "Mh5Nx8Rt2Jf0Wc4P", "name": "Millbrook", "icon": "dot", "visibility": "all", "style": { "id": "t60jVG828BRD7juf", "name": "Market town" }, "kind": "town", "tier": "marketTown", "population": 729 } ],
"routes": [ { "id": "Bq3Ls7Vg1Kn9Zy6D", "name": "King's Road", "visibility": "gm", "transport": "road", "class": "main", "style": { "id": "route-style-main-road", "name": "Main road" } } ]
}
}| Field | Type | Notes |
|---|---|---|
biome.id |
string | Stable identifier for the dominant biome in this cell (the majority of its area). Built-in biomes use a fixed set of keys (GRASSLAND, DECIDUOUS_FOREST, DEEP_OCEAN, and so on); a GM's own custom biome gets its own id. Always a string, always present. |
biome.name |
string | Human-readable display name, including for custom biomes. |
biome.code |
string | null | The GM's own optional short code for a custom biome (for example, a terrain code from their game system). null for a built-in biome, or a custom biome with no code set. |
terrainBand |
string | One of deepOcean, shallowOcean, lowland, upland, highland, mountain. No raw elevation value is exposed (see the band table below). The ocean bands always agree with biome.id: a DEEP_OCEAN-biome cell is always deepOcean, and a SHALLOW_OCEAN one always shallowOcean. |
moistureBand |
string | One of dry, moderate, wet. No raw moisture value is exposed. |
temperatureBand |
string | One of frigid, cold, temperate, warm, scorching. No raw temperature value is exposed. |
hasRiver |
boolean | A river (procedural or hand-drawn) is drawn in this cell, or a lake formed by one lies in it. This follows the river as the map draws it, at its full width, with its meanders and deltas: a wide river counts in every cell it covers. A river is only drawn on land (and across small pools), so where it reaches the sea or a lake the water is what the cell shows, not the river. |
rivers |
array | Every hand-drawn (custom) river whose line passes through this cell, by id and name. A procedural river (one traced automatically from a spring pin) is never named, so it's only ever reflected in hasRiver above, never listed here. Empty array if no named river passes through, even when hasRiver is true. Rivers have no visibility of their own: unlike regions, pins and routes, a river is carved into the terrain itself, not an annotation that can be shown or hidden. |
rivers[].id / .name
|
string | Stable id and the GM-given name (for example, "The Silverrun"), the same name shown in the Rivers and faults tool's Custom rivers list. |
isCoastal |
boolean | This cell borders ocean. |
regions |
array | Every Region drawn with the Regions tool that covers any part of this cell, visible to everyone or to the GM only (see regions[].visibility below). Sorted by coverage descending, so regions[0] is always the region covering the most of the cell. Empty array if none. A region set to hidden from everyone, or belonging to a layer or layer set hidden from everyone, is excluded, as neither ever appears on the exported Scene either. |
regions[].id / .name
|
string | Stable id and the GM-given name (for example, "The Whispering Woods"). |
regions[].layer |
object |
{ id, name } of the layer this region belongs to. Filroden's supports several named layers per map, grouped into layer sets (for example, "Forests" versus "Political Borders"), so this lets a consumer tell a political state from a forest rather than seeing only individual named regions with no grouping. |
regions[].visibility |
string |
all or gm. This is the region's effective visibility, not only its own setting: a region set to player-visible under a GM-only layer or layer set is reported as gm, matching what actually happens in Foundry once the layer itself overrides it. |
regions[].coverage |
number | Fraction (0–1, never 0) of this cell's area that falls inside this region. A cell can sit inside more than one region at once; this is what lets a consumer decide which one "counts" for a given purpose instead of guessing. |
infrastructure.pins |
array | Every pin (settlement, landmark and so on) located in this cell, visible to everyone or to the GM only; a pin hidden from everyone is excluded, as for regions. A pin with no icon (the internal marker a procedural river's source gets, rather than a GM-placed pin) is excluded too, the same way it's already left out of the journal the rest of the export creates. Empty array if none. |
infrastructure.pins[].id / .name / .icon / .visibility
|
string | Pin identity, its icon key (useful for classification, for example telling a settlement from a ruin), and its all/gm visibility. |
infrastructure.pins[].style |
object or null |
{ id, name } of the map's pin style the pin uses (for example, "Market town"), or null for a pin styled by hand. |
infrastructure.pins[].kind |
string or null | For a place the generator made, what it is: town (any settlement, from a capital to a village), castle, ruin or abandonedCastle. null for a pin you placed yourself. |
infrastructure.pins[].tier |
string or null | For a place the generator made, its size or type within its kind: capital, city, largeTown, town, marketTown, largeVillage, village, port, fortifiedTown and so on. null for a pin you placed yourself. |
infrastructure.pins[].population |
number or null | A generated settlement's population, where it has one. null otherwise. |
infrastructure.routes |
array | Every route (road, path and so on) passing through this cell, visible to everyone or to the GM only (a route hidden from everyone is excluded). Empty array if none. |
infrastructure.routes[].id / .name / .visibility
|
string | Route identity and its all/gm visibility. Generated routes have no name, so use the fields below to tell them apart. |
infrastructure.routes[].transport |
string |
road, river (a river trade route) or sea (a sea lane). |
infrastructure.routes[].class |
string or null |
trunk, main or minor, or null for a route with no class. |
infrastructure.routes[].style |
object or null |
{ id, name } of the map's route style the route uses (for example, "Lane" for the lanes Fill in adds between villages), or null for a route styled by hand. |
Pins and routes are listed whatever the map's detail level: a village hidden at the Regional level is still in its cell's data.
On the id format: every id above (regions, pins, routes, custom rivers, layers) is this module's own internal id for that object: a 16-character alphanumeric string with no prefix. biome.id is different: either one of the fixed built-in biome keys (GRASSLAND, and so on) or a custom biome's own id, always given as a string.
Land terrain bands are defined relative to the map's own sea level, with a lower threshold only, open-ended at the top, since hand-edited terrain (brush strokes, fault lines, carved rivers) can push elevation arbitrarily high:
| Band | Elevation range |
|---|---|
lowland |
sea level and above |
upland |
sea level + 0.10 and above |
highland |
sea level + 0.22 and above |
mountain |
sea level + 0.38 and above, with no upper bound |
A cell's band is whichever of these it qualifies for highest: an elevation of sea level + 0.5 is mountain, not highland, even though it also clears the lower thresholds.
Moisture and temperature are flat bands across the full underlying [0, 1] range, not relative to any per-map setting:
| Band | Moisture range |
|---|---|
dry |
below 0.33 |
moderate |
0.33 – 0.66 |
wet |
above 0.66 |
| Band | Temperature range |
|---|---|
frigid |
0.0 – 0.2 |
cold |
0.2 – 0.4 |
temperate |
0.4 – 0.6 |
warm |
0.6 – 0.8 |
scorching |
0.8 – 1.0 |
A map exported with no grid has no cell geometry to summarise, so gridData (and this feature entirely) is simply absent from that Scene. There's nothing to parse and nothing to fall back to; check for gridData before doing anything else with it.
Once you're building against this, treat it as a small public contract: schemaVersion exists so a future breaking change is detectable rather than silently breaking your code, and an additive change (a new optional field) won't bump it. If you rely on this data, checking schemaVersion before parsing is worth the one extra line.
The macro below is a template, not a finished tool: copy it into your own macro, module or system and adapt it, rather than using it unmodified. It shows the basic pattern any consumer of this data needs: confirm the Scene actually has grid data, resolve a token's position into the same {i, j} offset the data is keyed by, then read whatever fields you're interested in.
Paste it into a new Foundry macro of type Script, open a Scene this module has exported with a grid, select exactly one token, and run it. It reports that token's cell as a formatted table in chat.
/**
* Filroden's World Map Builder: Grid Cell Lookup (example macro)
*
* Demonstrates how a macro, module, or game system can read the grid-cell exploration data FWMB
* writes to a Scene at export time. See the Grid Cell Data wiki page for the full field reference.
* This macro only reads a handful of the available fields to keep the example short; there's more
* in there (regions, infrastructure, bands) than what's shown here.
*
* This is a template, not a finished tool: it's meant to be copied into your own macro, module,
* or system and adapted, not run unmodified as a real piece of game tooling. Paste this into a
* new Foundry macro of type "Script" and run it with a Scene FWMB has exported open, and exactly
* one token selected on it.
*
* What it does:
* 1. Confirms the current Scene actually carries FWMB grid data, so it does nothing on a scene
* FWMB never exported (or exported before this feature existed, or exported gridless).
* 2. Confirms exactly one token is selected: the lookup is "where is this token", so zero or
* several selected tokens is treated as a user error rather than guessed at.
* 3. Converts that token's canvas position into the same {i, j} grid offset FWMB's data is
* keyed by, and looks up the matching cell.
* 4. Posts a formatted table of that cell's data to chat, as that token's speaker.
*
* Note: gridData reflects the map as of its last FWMB export. If the GM has changed the grid size
* or regenerated the map in FWMB since exporting, re-export before trusting this data.
*/
// --- 1. The current Scene must carry FWMB grid data ---------------------------------------------
const FWMB_MODULE_ID = "filrodens-world-map-builder";
const FWMB_GRID_DATA_FLAG = "gridData";
const scene = canvas.scene;
const gridData = scene?.getFlag(FWMB_MODULE_ID, FWMB_GRID_DATA_FLAG);
if (!gridData) {
ui.notifications.warn(`"${scene?.name ?? "This scene"}" has no Filroden's World Map Builder grid data. Export the map from FWMB with a grid enabled, then try again.`);
return;
}
// --- 2. Exactly one token must be selected -------------------------------------------------------
const selectedTokens = canvas.tokens.controlled;
if (selectedTokens.length === 0) {
ui.notifications.warn("Select a token first.");
return;
}
if (selectedTokens.length > 1) {
ui.notifications.warn("Select only one token.");
return;
}
const token = selectedTokens[0];
// --- 3. Resolve the token's grid cell --------------------------------------------------------------
// canvas.grid is the Scene's own live Grid instance, the same {i, j} offset system gridData's
// cells are keyed by, whatever the grid's shape (square or either hex orientation).
const offset = canvas.grid.getOffset(token.center);
const cellKey = `${offset.i},${offset.j}`;
const cell = gridData.cells[cellKey];
if (!cell) {
ui.notifications.warn(`${token.document.name} is at grid cell ${cellKey}, which has no data (outside the map's grid bounds).`);
return;
}
// --- 4. Build and post the chat table ----------------------------------------------------------
const formatVisibility = (visibility) => (visibility === "gm" ? "GM only" : "All players");
// Every list field (regions, pins, routes) follows the same shape: an array that's empty when
// nothing overlaps the cell, so one small helper renders all three consistently.
const formatEntityList = (entities, describe) => (entities.length ? entities.map(describe).join("<br>") : "—");
const regionsHtml = formatEntityList(cell.regions, (region) => `${region.name} <i>(${region.layer.name}, ${Math.round(region.coverage * 100)}% of cell, ${formatVisibility(region.visibility)})</i>`);
const pinsHtml = formatEntityList(cell.infrastructure.pins, (pin) => `${pin.name} <i>(${pin.style?.name ?? pin.icon}, ${formatVisibility(pin.visibility)})</i>`);
// A generated route has no name, so fall back to its style's name, then its transport
const routesHtml = formatEntityList(cell.infrastructure.routes, (route) => `${route.name || route.style?.name || route.transport} <i>(${formatVisibility(route.visibility)})</i>`);
// Only named (hand-drawn) rivers show up here: a procedural river traced automatically from a
// spring pin is never named, so it's only ever reflected in the "Has river" row above.
const riversHtml = formatEntityList(cell.rivers, (river) => river.name);
const rows = [
["Grid cell", `{ i: ${offset.i}, j: ${offset.j} }`],
["Biome", cell.biome.code ? `${cell.biome.name} (${cell.biome.code})` : cell.biome.name],
["Terrain", cell.terrainBand],
["Moisture", cell.moistureBand],
["Temperature", cell.temperatureBand],
["Has river", cell.hasRiver ? "Yes" : "No"],
["Named rivers", riversHtml],
["Coastal", cell.isCoastal ? "Yes" : "No"],
["Regions", regionsHtml],
["Pins", pinsHtml],
["Routes", routesHtml],
];
const tableRows = rows.map(([label, value]) => `<tr><td style="font-weight: bold; padding: 2px 10px 2px 0; vertical-align: top; white-space: nowrap;">${label}</td><td style="padding: 2px 0;">${value}</td></tr>`).join("");
const content = `
<div style="border: 1px solid #782e22; border-radius: 4px; padding: 4px 8px;">
<h3 style="margin: 4px 0; border-bottom: 1px groove #782e22;">${token.document.name} — Grid Cell Data</h3>
<table style="width: 100%; border-collapse: collapse;">${tableRows}</table>
</div>
`;
ChatMessage.create({
content,
speaker: ChatMessage.getSpeaker({ token: token.document }),
});See also: Export for exporting to a Scene, and Rivers and faults for naming a custom river.
Tutorial
- A new world
- Shaping the land
- Climate and biomes
- Rivers and faults
- Populating the world
- Realms and provinces
- Roads and sea lanes
- Places and labels
- Exploring the world
- Finishing and sharing
- A regional map
- Further tips
Shape
Populate
Finish
Explore
More tools
Release notes