minibia-tools.html (the World Compendium page) loads data/derived/compendium.json and data/icons.json at runtime, plus the per-floor spawn map terrain images (data/derived/minimap-z*.png).
data/raw/ - only files exactly as downloaded from minibia.com, same names (the list, with where each comes from, is in data/raw/README.md):
library.json — monsters, items, npcs, spells, runes, achievements. Exported manually (the Library page is Cloudflare-gated).monster-spawns.json, npc-trades.json, definitions.json, npc-poi.json — the /play/*.json files.minimap.bin.gz — the client’s minimap, kept gzipped as served; used for the Spawn Map terrain, stairs and pathfinding.python scripts/fetch-live.py can download all but library.json in one go, if automated access is OK with the site.
Hand-made inputs (e.g. floor-connection-labels.json) live in data/manual/, not here.python scripts/diff-raw.py compares data/raw/ with origin/main - which files are updated/unchanged/missing; monsters (loot per item, attacks/damage, stats), items, NPCs, spells, runes, achievements field by field; spawn points per monster; item definitions added/removed/changed with the owner’s notes; the creatures the game files already have corpses for but that aren’t in the library or spawns yet (new creatures waiting for their place in the world); and where the map changed. It adds the update to data/derived/changes.json, shown on the site’s hidden Data versions page (the link at the bottom of the page, or #versions; every update is kept and selectable, and each changed map area has a button that shows it on the map), plus a Markdown copy in data/changes/<date>.md.python scripts/gen-minimap.py # minimap.bin.gz -> data/derived/minimap-z*.png + minimap.meta.json
python scripts/build-data.py # data/raw/* -> data/derived/compendium.json (+ merchant.json, CHANGELOG.md)
python scripts/gen-floor-plausibility.py # appends a floor bitmask to every spawnMap point (needs the two above)
python scripts/gen-floor-connections.py # detects stairs/ladders/holes and guesses which floor(s) each connects to (needs the two above)
python scripts/gen-known-floors.py path/to/tibia-export.json # optional: known spawn floors from a real-Tibia spawn export (see below)
data/raw/, data/derived/, data/changes/ and the updated CHANGELOG.md. GitHub Pages picks it up — no other build step.build-data.py also writes data/derived/merchant.json (for a merchant-ledger page this repo doesn’t have yet) and .js twins of both (window.__MINIBIA_COMPENDIUM__ = ... etc., for loading via <script src> instead of fetch() when a page needs to work over file://). data/raw/npc-trades.json and data/raw/definitions.json are currently empty placeholders ({}) — build-data.py requires them to exist, but nothing here reads merchant.json yet.
scripts/gen-floor-plausibility.py (not one of the original 4 scripts — added to restore the “Hide implausible floors” toggle, which the switch to raw-cluster spawn data had silently turned into a no-op) computes, per spawn point, which of the 16 floors are plausible from the terrain alone — void detection, then a walkability classification derived from the client’s .dat minimap-color/not-walkable data (BLOCKING_IDX in the script), checked over a window around the point rather than a single pixel. It appends the result as a 4th element to each spawnMap point ([x, y, count, floorMask]); a point with no confident floor falls back to plausible-everywhere rather than guessing wrong. See the script’s docstring for the full algorithm.
scripts/gen-floor-connections.py (also not one of the original 4 - added for the map’s “Stairs & holes” layer and the pathfinder) finds every tile the minimap draws as a stairs/ladder/hole/ropehole (a flat yellow - but the same yellow also paints some roofs and ground, so only staircase-sized patches of up to 12 tiles that aren’t rings count; that skips ~20,000 roof/ground tiles and leaves ~14,300 markers) and works out which adjacent floor each connects to, purely from the terrain. Each marker tile goes one way only (up or down, never both) and pairs with the marker directly above/below it, preferring the best shape match - a 3-wide staircase pairs with the 3-wide one under it, not a 1-tile stair it touches - else one within 2 tiles that isn’t already going the same way. A ramp is often drawn wider on one floor than the other (e.g. a 2x2 yellow block below a 1-wide top): only the tiles stacked exactly under/over the other floor’s marker change floor, so once a patch has a stacked pair its other tiles (the drawn foot of the ramp, like x32547 y32076 z7 beside the real ramp tile x32548) don’t pair by distance with their own ramp or a neighbouring stair - they’re dropped (~1,600 tiles), and the pathfinder and bot waypoints step onto the real ramp tile. Pairing is per tile, so an up and a down stair side by side in one yellow patch stay separate. A column of identical markers splits into stacked pairs from the top down, which is how ladder towers alternate ladder and hole; a plain walkable-vs-void asymmetry with no marker is a weaker guess, a light grey tile sitting exactly on top of a yellow marker that isn’t itself going down is the top of a stairs/trap door (the minimap only draws the arrival end yellow for those - ~370 in this snapshot), a marker with nothing walkable on either neighbouring floor is a dead end (dropped - the decorative/blocked holes the client also draws yellow, ~6%), and anything still ambiguous is written to a separate floorConnectionsUnknown list instead of guessed (~2% of markers, in this snapshot). The World Compendium page can step through that unknown list and let a human call each one by eye - see “Hidden map tools” below - and export the answers as JSON; drop that file at data/manual/floor-connection-labels.json and rerun this script to fold them in as confirmed, overriding the algorithm. See the script’s docstring for the full method.
The same script also writes data/derived/slopes.json: ramps and pyramid sides are drawn as bands of the stairs yellow (too big for a staircase, so never a stairs marker). Their outer tiles are walkable “pre-ramp” ground; the tiles that sit directly under something drawn on the floor above are where walking climbs onto it (on the Ankrahmun pyramids and every other yellow ramp alike - ~5,700 tiles), and the pathfinder never steps on those. It also writes data/derived/door-candidates.json, the possible doors the Find path tool uses: every tile of 1-thick grey or red wall whose two opposite walkable sides fall in different 8-connected areas of that floor (a diagonal step between two blocked tiles counts, as in the game), and can’t reach each other through other floors either - walking plus the detected stairs/ladders/holes, linked the way the pathfinder links them, both ways. Walls you could walk around, on this floor or via stairs (e.g. a walled city entered by its stairs, like the wall at x33247 y32815 z7), never qualify (~2,300 dropped this way), so a route only assumes a door where there’s no other way in. On top of that the pathfinder treats doors as a last resort per route: when start and goal are both on open ground it uses no door at all (so it can’t cut through a tower room built into a city wall either), when one end is in a small enclosed area (under 10,000 tiles reachable without a door - a house, a tower room) it only allows doors into or out of that area, and only if that finds nothing does it search again with every door allowed but each one costing 250 steps, so a route that needs one real door somewhere doesn’t also start cutting through tower rooms elsewhere. (Mountain rock and stone walls share one minimap grey, so a 1-tile rock ridge can still show up as an assumed door when nothing else connects - marked with a white square like every assumed door.)
Monster cards have a Hunting section, computed in the browser from data already in compendium.json: expected loot weight per kill (each sellable drop’s chance × average quantity × the item’s weight - empty bags and unsellable junk don’t count) and gold per oz, gold and XP per 100 HP dealt, and spawn clustering from the spawnMap points - total spawns, hunting areas (spawns chained no more than 25 tiles apart, on a shared plausible floor), and the best spot (most of that monster within 12 tiles of one point, with a map link and who else spawns there). Loot rows show each item’s weight and gp/oz. The monster list can be sorted by all of these (gold per oz, lightest loot, gold/XP per 100 HP, best spot, best spot value, biggest hunting area, spawns in world).
On the Spawn Map, clicking (or tapping) anywhere that isn’t a floor-connection marker copies that tile’s coordinates, e.g. x32480 y31644 z10; the readout next to the map buttons shows the tile under the cursor.
scripts/gen-known-floors.py deduces which floor our spawn clusters are on from a real-Tibia monster-database export (JSON with "monster": {"name"} and "spots": [{"spawns": [{"x","y","z"}]}] blocks). Minibia’s world is largely the real Tibia map, so a cluster with real spawns of the same monster within 8 tiles takes their floor(s) - only floors the terrain check (gen-floor-plausibility.py) already allows, nearest first; monsters match by name, plus a few aliases for Minibia renames (Beholder = Bonelord…). The export itself isn’t kept in the repo - only the small result, data/derived/known-floors.json ({"radius", "monsters": {name: [[x, y, floorMask], ...]}}). Where the match is exact it also keeps the real spawns’ own tiles: Minibia’s clusters are its spawns grouped, at the rounded average of their tiles, so when count real spawns of the monster within 8 tiles average to exactly the cluster’s tile (the nearest ones, or else the only such set), all on floors the terrain allows and none claimed by another cluster, the row gets their offsets and floors ([x, y, floorMask, [dx, dy, z, ...]]). Where the average is off, Minibia’s spawns sit elsewhere, so that cluster keeps only its floor. On the current export: 10,989 of the 15,219 spawn clusters of the 272 matching monsters got a known floor (9,097 a single floor), and 3,725 of those their spawn tiles (7,669 spawns); the rest are Minibia-only areas/monsters. The Spawn Map’s Real-Tibia floors toggle (on by default) shows those clusters only on their floor, with a white ring and the floor in the tooltip - and clusters with spawn tiles as one dot per monster on its own tile and floor (the tooltip names the tile and the cluster it belongs to) instead of a “cluster of N” dot. Monster cards show a per-floor count. The in-page Preview a Tibia export tool applies the same rules. Spawn dots sit in the middle of their tile.
Before every jump to another view - a tab, a cross-link to an item, NPC or monster, 📍 / “show on map”, a stairs marker, the Data versions page - the view you’re leaving is remembered: tab, search, sort and filters, open cards, scroll position, and on the map the floor, view and monster chips. A floating ← Back button (bottom centre, shown once there’s something to go back to; it moves up out of the way of the map’s own controls) puts the last one back, and its ▾ lists the last 5 to pick from - picking one also drops the ones after it. The list is kept in localStorage, so it survives a reload on that device.
The Spawn Map tab is one workspace: the map with a mode switch above it - Spawns, Find path, Route builder - and a side panel (below the map on narrow screens) for the current mode. The floor control and the cursor’s coordinates sit on the map; Layers holds the display toggles (Real-Tibia floors, Hide implausible floors, Stairs & holes). In Spawns mode the side panel has the difficulty filter and, under Data tools, the curation tools. To show particular monsters, type a name in the search box and press Enter (or pick a suggestion): it becomes a chip under the box - tap a chip to toggle it, × to remove it - and the map shows every monster whose chip is on (a full monster name matches exactly; other text matches every monster containing it, e.g. “orc”). Chips are remembered in the browser; the 📍 on a monster card switches the map to just that monster’s chip.
floorConnections on the current floor and lets you click a marker to jump to whichever floor it resolves to; ones labeled both ways by hand don’t jump, since there’s nothing to disambiguate which one you meant.floorConnections stairs/ladders/holes - never by walking over unwalkable tiles. Swamp (the minimap’s bright green) is a last resort: it mixes walkable swamp ground with swamp water and the minimap draws both alike, so a route first searches without it, and only if there’s no dry way (an end standing in swamp, or a dry patch like parts of Venore reached only across it) does it allow swamp - each swamp tile then costing 10 steps, so it crosses as little as it can. Routes never step on a ramp’s climbing tiles (slopes.json, above) and never walk across a stairs/ramp/hole tile - stepping onto one changes floor in the game, so a route only steps onto one to take it (ladders and rope spots are treated the same, since the minimap can’t tell them apart). Moves are 8-directional, and like the game a diagonal step only needs its destination walkable - you can squeeze diagonally between two trees or past a wall corner. The search is Jump Point Search A* in a Web Worker: the first use spends a few seconds decoding all 16 floors, then routes typically take milliseconds. The minimap draws most doors as plain wall, so a 1-tile-thick grey or red wall with walkable ground on both sides that are otherwise unconnected counts as a possible door (door-candidates.json, below). The route may pass through one, at a higher cost than walking around. Every one it uses is counted in the status line and marked with a white square on the map, so you can check it’s a real door. It can’t know about boats, teleports, or tiles the color table misjudges, so “no route” between islands is expected.
#bot-path in the URL - remembered on that device - and #bot-path-off to hide it again) - once a path is found, the Find path panel turns it into cave bot waypoints with no walk back: a walk on the start, a walk every N tiles, a floor-change waypoint at every stairs/hole/ladder (same placement and checks as the Route builder’s fill-in below), and a walk on the goal. The waypoints show numbered on the path. The minimap draws stairs, ladders and rope spots alike, so Pick ways up on the map steps through them one at a time: the map zooms to each with the spot ringed, a card over the map asks Stairs, Ladder or Rope, and picking moves on to the next until all are done (Back / Next to move around, ✕ or Esc to finish later; taps on the map are ignored meanwhile, pan and zoom still work). The panel then lists them as chips - tap one to look at it again. Copy JSON / Download ({ "version": 1, "route": [...] } only) appear once every one is picked. Edit in Route builder opens the waypoints there (loop off) for any other change. Turn the bot’s Cave loop off for such a route, or it heads back to waypoint 1 when it arrives.#hunt-est in the URL - remembered on that device - and #hunt-est-off to hide it again) - in the Route builder panel: which monsters a lap of the route kills and what that’s worth. Every spawn within Reach (3/5/8 tiles) of the walked path, on the same floor, counts as killed once per lap with its monster count (a cluster with real-Tibia spawn tiles tile by tile). Spawns whose floor is known (real-Tibia data) are sure; the rest only count with Count spawns whose floor isn’t confirmed on, and show as “(n?)”. It lists kills, XP and loot gold per monster (with its difficulty colour), the expected loot per lap with item icons (count and NPC value, from the design database’s drop chances), and totals per lap (or per run for a one-way route) with the step count; the counted spawns are circled on the map (solid = floor known, dashed = not). Rough by design: respawn times, monsters that follow you and anything off the path aren’t modelled.#route=..., base64 JSON - no server or storage), so “Copy share link” gives a link anyone can open: opening a shared link shows the route read-only, with “Edit a copy”. Titles and notes from a link are only ever rendered as text.
BOT_UP_EXPORT in the page names the type to export them as. Copy bot JSON / Download .json export { "version": 1, "route": [...] } (no settings, which would overwrite the character’s Cave settings), after checking the same things the bot’s importer rejects (duplicate labels, a label waypoint without a label, goto/supply-check targets that don’t exist, buy-item without an item, sell-items without rows, drop-item without an item, bad directions/modes). Import bot JSON takes a pasted route - the bot’s own Copy output, or a bare array of waypoints - and maps it out (following each leg with the pathfinder; the bot’s settings only set whether it loops, and never go into exports). Imported waypoints keep every param the bot wrote, so exporting gives back the same route; share links leave out the bot’s default param values and restore them on open. Bot fields also ride along in the share link. Waypoints are colour-coded by type on the map and in the list (with a colour key), can be dragged to a new tile while editing (clicking one opens its settings), and floor-change waypoints are checked against the detected stairs/holes/rope spots: hole/stairs down needs a “down” marker on or next to its tile, stairs/ladder up and rope an “up” marker, magic rope one on the exact tile (or on the tile its direction points at), and the next waypoint must be one floor down/up - ✓ when it matches, ? when only an unresolved marker is there, and ⚠ plus a red ring on the map when nothing is, so it’s possibly misplaced."monster": {"name"} and "spots": [{"spawns": [{"x","y","z"}]}] - and it matches each monster to ours by name: a spawn cluster with real spawns of the same monster within the match radius (default 8 tiles) takes their floor, as long as our terrain allows that floor there (nearest floors win). Matched clusters then show only on their floor, with a white ring, their tooltip names the floor, and monster cards show a per-floor count; a table lists coverage per monster. Runs entirely in the browser, for this visit only (it replaces the built-in known floors until you clear it); the small result can be downloaded. On Crypt Shamblers: 117 of 172 clusters matched (106 to a single floor); every matched floor was one the terrain check already allowed.floorConnectionsUnknown one at a time (jumping the map to each), letting you call each Up/Down/Unknown by eye. Answers save to this browser’s localStorage as you go (survives a reload) and “Export JSON” downloads them in the format gen-floor-connections.py’s manual-override loader expects.scripts/build-bundle.py packages multiple tool pages (site/index.html, merchant-ledger.html, compendium.html, accounts.html) plus the minimap PNGs into a single self-contained dist/minibia-tools.html. It’s kept here for reference but isn’t wired up — this repo only has the one compendium page, served directly via GitHub Pages rather than bundled.
data/icons.json (item icon images, base64 PNG keyed by the library item’s cid) is rendered from the game client’s sprite files, which are too big to keep in the repo (Minibia.spr is ~50MB). When new items show up without an icon, get Minibia.spr, Minibia.patch.spr and Minibia.dat (from https://minibia.com/data/760/<name>, or the browser cache after playing) and run:
python scripts/gen-icons.py --spr path/Minibia.spr --patch path/Minibia.patch.spr --dat path/Minibia.dat --virtual-items path/virtual-items.js
It only adds icons for items that don’t have one, leaving existing icons as they are (--all re-renders everything). Each item’s sid is its .dat object id. Items whose sid is 30000+ are recolors the client makes itself, defined in its src/data/virtual-items.js. Pass that file with --virtual-items to render them; only exact color-map recipes are supported. See the script’s docstring for the file formats.
Because the page fetches JSON at runtime, it needs to be served over HTTP (e.g. GitHub Pages, or python3 -m http.server locally) rather than opened directly as a file:// URL.