World Explorer: the play log
The game keeps a log of how you play, so a game can be judged after it: where
the time went, what paid and what was bought, when the kit was complete, what
killed you, and how smooth the frames were. Pause → Download play log
saves it as world-explorer-log-<date>.json; bin/play-log <file> turns it
into a report. The code is src/js/play_log.js.
Privacy
The log is kept in this browser's storage and nowhere else. Nothing is sent;
the downloaded file is the only way it leaves the device. The pause panel says
so in one line. Clear log empties it (Undo brings it back until you
resume). The dev page (build/dev.html) and the agent's page keep logs of
their own, as they keep saves of their own.
What it records
- Sessions. A session is one game from its start (new, continue, load) to
the main menu or the page's close; past an hour of play it goes on in a new
session of the same game, which begins with the world and the kit. It carries the mode (
campaign,arcadeorrace, the main menu's Races), the difficulty, the game's version and build time, and how it began and ended. - Games (runs). A campaign is one run across all its sessions: its save
keeps the id (
campaign.log_run), so a continue next week joins the same run. Each arcade run is a run of its own. In the Races mode the races from the menu back to the menu are one run, each race arunevent in it. - Records. One record a minute of play, cut short when you change worlds, load, or die, so each record is in one world. A record holds the salvage on hand, salvage earned by source and spent by kind, XP by source, kills by raider type, damage dealt and taken (by cause), raider shots, dashes, pulses, supplies used, deaths, the lowest hull, and the frame rate.
- Events. A world entered, a purchase, a sale, the kit when it changed, a rank, a contract's steps, a flagship killed, a race, an arcade wave and a world cleared, a death with its cause, the milestones (the top ship, every module at its cap, every gun at tier 5), a load or a tow.
Only play counts toward time: flight, and the station, depot, map and inventory screens. Pause, menus, settings and the results screens do not.
Size
All stored sessions together are kept under 1.5 million characters (about
1.5 MB, some 40 hours of play); past it the oldest sessions go first. Saves
come first: when the browser's storage is full, the log's oldest sessions make
room for a save (storage_set). The pause panel says how many sessions were
dropped. A session longer than the cap would lose its oldest records, and say
how many in dropped; the hourly roll keeps sessions far below it.
Cost
In a frame the log only adds to counters it already holds: no objects, no
strings. It writes once a minute (the session being played: an hour at most,
about 45 KB, under a millisecond), on pause, at a death and when the page is
hidden or closed. bin/perf shows no change in frame time or allocation.
The file
One JSON document. Each record and each event sits on a line of its own, so it reads as text and loads as JSON.
{
"format": "world-explorer-play-log",
"format_version": 1,
"about": "…",
"exported_at": "2026-10-13T20:46:00.000Z",
"game": {"version": "0.1.0", "built": "2026-10-13T20:42:31.451Z"},
"summary": {…},
"sessions": [{…}, …]
}
format_version grows when a field changes meaning or goes away; new fields
can come without it. A reader skips fields it does not know.
Units
| What | Unit |
|---|---|
t, s, *_s, secs, played_s |
seconds of play (pause and menus not counted) |
*_at, started, ended, built |
ISO 8601, UTC |
minute (summary) |
minutes of play since the session began |
salvage, prices, earned, spent, value |
salvage (◆) |
dealt, taken, taken_by |
hull and shield points |
hull_low |
the lowest hull in the record, 0 to 1 of the ship's hull |
fps |
frames a second in flight |
frame_ms, work_ms, paint_ms |
milliseconds: the time between frames, the time a frame's work took, and the browser's time after it |
summary
| Field | |
|---|---|
sessions, play_minutes |
how many sessions, how much play |
worlds[] |
per mode and world: minutes, earned, spent, kills, deaths, taken |
salvage_curves[] |
per session: minute_salvage, at most 40 [minute, salvage on hand] points |
deaths[] |
every death: session, mode, minute, world, cause, first_in_world |
milestones[] |
top ship, all modules at cap, all guns tier 5: when, and the salvage then |
A session
| Field | |
|---|---|
id |
the session's id (its start time, UTC, and four letters) |
mode |
campaign, arcade or race (the Races mode) |
run |
the game's id: a campaign keeps it in its save across sessions |
difficulty |
chill, normal, overload; in the Races mode easy, normal, hard |
game |
version (package.json) and built (bin/build's time) |
started, ended, end_reason |
back to the main menu, page closed, another game started, an hour of play: goes on in the next session, or not closed: the page went away without notice (a crash); ended is null while it is being played |
played_s |
seconds of play |
start |
how (new, continue, load, after death, next race (the Races mode), continued (the hourly roll), after the log was cleared), world, salvage, xp, rank, ship |
screen, browser |
the window's size and pixel ratio, the user agent |
records[], events[] |
below |
dropped |
records and events dropped to keep the size (events: past 5000 a session) |
A record
{"t":60,"s":53,"at":"…","world":"Haven","flight_s":47,"docked_s":6,"salvage":970,
"earned":{"contract":290,"race":190,"sell ore":80},"spent":{"trade":24},
"xp_total":262,"xp":{"kill":14,"contract":206},"rank":"Courier",
"kills":{"chaser":2,"shooter":2,"lancer":1},"elite_kills":0,"dealt":1180,
"taken":{"shield":0,"hull":53},"taken_by":{"shot":53},"raider_shots":28,"hits_on_you":4,
"dashes":3,"pulses":1,"supplies":{"emp":1},"deaths":0,"hull_low":0.41,
"fps":59.8,"frame_ms":{"avg":16.7,"max":34,"over_33":1,"over_50":0,"over_100":0},
"work_ms":{"avg":3.1,"max":12.4},"paint_ms":{"avg":4.2,"max":15.8}}
| Field | |
|---|---|
t, s |
when the record began, and how long it covers |
at |
the wall clock at its end |
world |
the world it was played in |
flight_s, docked_s |
seconds in flight; seconds in the station, depot, map or inventory |
salvage |
on hand at its end |
earned |
by source: pickups (wreck and cache salvage), contract, story (a story chapter), race, goal, world bonus (arcade), swallowed (a raider the arcade's Eclipse hole took), sell <cargo> (ore, cells, relics, commodities), transport (a transport line's sales), refund (a structure still building when you left), unlogged |
spent |
by kind: ship, gun, gun tier, module, supply, repair (arcade), card (arcade, a card's price), drone, drone tier, builder, structure, transport line, trade (commodities bought), tow (the tug after a death), unlogged |
xp_total, xp, rank |
campaign only (null in the arcade): XP at the end, earned by source (kill, contract, unlogged), the rank |
kills |
by raider type: chaser, shooter, lancer, tank, splitter, shard, boss |
elite_kills |
of those, elites |
dealt |
damage your side dealt to raiders' shields and hulls: guns, pulse, EMP, missiles, drones, platforms, arcade chain blasts |
taken |
the damage you took, on the shield and on the hull |
taken_by |
by cause: shot, ram, flagship (its attacks that are not shots), missile (a blast), ship blast (arcade), vent (arcade event), flare, rock, debris, radiation, black hole |
raider_shots |
shots raiders fired (at you, and at what you protect) |
hits_on_you |
shots that hit you |
dashes, pulses, supplies |
turbo ignitions, pulses released, supplies used by key (medkit, emp, stasis) |
deaths, hull_low |
deaths in it; the lowest hull |
fps, frame_ms, work_ms |
in flight only; frames over 33, 50 and 100 ms are counted; work_ms is the game's own script in frame(): update, the canvas calls that record the drawing, the HUD |
paint_ms |
in flight only: from the end of frame() until the main thread is free again (a message posted then arrives): the browser's style, layout and paint, and on a software canvas the canvas's raster, which work_ms does not see. Whatever is left of frame_ms is waiting: for the screen's refresh, or for the graphics card. Missing in logs from before version 0.2.6 |
partial |
true on the last record of the session being played when the file was saved |
unlogged is salvage or XP that moved without passing a hook the log knows (a
dev cheat, or a new source in the code): the sums always match the balance,
and a non-zero unlogged says a hook is missing.
Events
Every event has t and type.
| type | Fields |
|---|---|
world |
world, how (new, continue, load, after death, continued, gate, next world), first_visit, salvage |
run |
a load or the tow inside a session: how, world, salvage, xp |
buy |
kind (as spent), item, price, salvage_left, priciest_yet (the game's dearest purchase so far), and for a gun tier tier, for trade amount |
sell |
item, amount, value, salvage_left |
kit |
ship, guns (mount by mount: id, name, tier, or null for an empty mount), modules (level by key), modules_at_cap, modules_total, modules_below_cap (those needing a launcher are marked), ships_owned, drones, rank, salvage |
rank |
rank, xp |
milestone |
what: top ship (the priciest ship owned), all modules at cap (all but those needing a launcher you do not carry), all guns tier 5; salvage |
contract |
action (taken, stage done, completed, claimed, abandoned), title, kind (the stage's), stage, stages, world, reward, story, secs (since taken), stage_secs (since the stage began), and xp when claimed |
boss |
a flagship killed: name, world, secs (from its first hit), taken (the damage you took meanwhile) |
race |
a campaign race over the line: title, world, place, of, time and par (seconds), prize |
race (mode: race) |
a race of the Races mode at its end: kind (steer, turbo), level (easy, normal, hard), world, title, end (finished, wrecked, forfeit), why (a forfeit's), place (null unless finished), of, time (yours, null unless finished), at (the race's clock at its end), par, rings, km, reached (rings you passed), crashes (hits on rocks and drifting debris; in a Turbo race, spins off the track), rock_hull (the shield and hull they took), hull_left, racers[]: name (You for you), skill (0 to 1), time, next (rings passed), out, splits (seconds since green at each ring passed: the places along the race) |
race_spin |
a Turbo race: you spun off the track: at (the race's clock), ring (the one you were heading for) |
race forfeit |
world, why |
wave |
arcade: world, wave |
world cleared |
arcade: world, secs in it, bonus |
card |
arcade: a card taken at the break (src/js/arcade_cards.js): world (the one just cleared), card (its id), kind (gamble, trade, boon), module and price when it has them |
card won |
arcade: a gamble's world cleared, its prize given: world, card |
card used |
arcade: the second wind saved you: world, card |
route |
arcade: the next world chosen at the break: from, to, offered (the worlds offered, names) |
death |
world, cause (kind as taken_by; for a shot raider, weapon, elite; for a ram raider, elite), first_in_world (in this game), in_world_s, salvage, ship, flagship_alive (its hull left, 0 to 1, or null) |
won |
the arcade's last world cleared: world, salvage |
Reading it
bin/play-log world-explorer-log-2026-10-13-2346.json
bin/play-log log.json --charts=data/play-log # PNG charts too
bin/play-log log.json --mode=campaign
bin/play-log log.json --mode=race # the Races mode only
The report has the sessions and games, per world time, income, spending and kills, income by source and spending by kind, salvage on hand over time, the kit as it changed, the priciest purchases, when the game was maxed out and what the salvage did after, combat per world, deaths, contracts, flagships, races (the Races mode's each with its place, time, par, gap to the best rival, crashes and hull left, a summary per difficulty, and your place at each ring), the arcade's worlds, the frame rate, and notes on what stands out.
In Python:
import json, pandas as pd
log = json.load(open('world-explorer-log.json'))
records = pd.json_normalize([dict(r, session=s['id'], mode=s['mode']) for s in log['sessions'] for r in s['records']])
events = pd.json_normalize([dict(e, session=s['id']) for s in log['sessions'] for e in s['events']])
With jq, every purchase over 1000:
jq -c '.sessions[].events[] | select(.type == "buy" and .price > 1000)' log.json