Playing alone, together, saved games
One game file plays alone or together. The lobby has Play alone (a private room of one: it starts by itself, and keeps the saved profile and saved game) when the game allows one player (min 1, no cup; multiplayer.solo: false hides it), and Play with others in a room of one (before it starts or after it ends). While playing alone the bar has End (tap twice) back to that lobby. A room of one is joined with alone: true (room.js join({alone}), relay j.alone): the hub keeps its world save under the player (SavePort who = "alone:<seat key>"), so a game alone and the shared game played together never read or overwrite each other, and nobody else can join it by code. The profile is the same in both. Invite anyone shows in a room for anyone when the host app offers it (relay option inviteAnyone, info anyone: true, room.js inviteAnyone(), port message invite-anyone): the app shows its own way in for people without an account (for example a share dialog with a public link). The public wrapper and the view page do not offer it. With no host app around the page it runs as the one-player game. Write for g.players (1 to max) and it works both ways.
Saving needs a host app that gives the hub a storage port (new RoomHub({ saves }), SavePort in hub.ts: load(scope, slot, who) gives {at, data, v?} or null, store(scope, slot, who, json | null, {session?, v?})) and a stable slot per game in the socket’s identity (not the page hash, so editing a game keeps its saves). Without them everything below is off and g.saves is false.
- Player profile (
g.profile,g.profile.set({...})): one small document per person and game, for stats, level, items, weapons, outfits, hp, mp, lives left. Loaded beforesetup(the game waits up to 3 s); merges like shared writes; written within seconds and when the person leaves. A profile that arrives after that wait is taken only if the game has not written its own yet; otherwise this page does not save and says “Progress is not being saved in this game”. A public link’s visitor keeps theirs in their own browser (relay.jsoptionprofile). - World save (
multiplayer.save: true): the host’sg.shared(map level, monsters and their stats, progress) is written while it changes and at the end of a match. A new room finds it: the host sees Continue and New game (New game asks first: it replaces the save); Continue starts from the saved state (g.resumed, writes tog.sharedinsidesetupare ignored),g.clearSave()ends the saved game. Shell keys (teams, seats, cup) are saved with it. One room at a time plays a given save: a second room of the same game (another group, a second tab alone) plays unsaved (saveBusyin welcome; the lobby says so and offers no Continue), and the next room after the first one closes continues it. A save that could not be read is never written over: until it is read the room writes and clears nothing (a Start waits up to 3 s for it, so Continue is still offered); a failed read is tried again after 2, 5, 15 s, then every 30 s, and meanwhile the lobby says “The saved game could not be opened, so this game is not being saved” (saveErrorin welcome,savestate {error}when it changes). A match that started without the save stays unsaved; the next start offers Continue. - Save versions (
multiplayer.saveVersion, a whole number, default 1): kept with every world save and profile (a save without one counts as 1). When a change to the game makes old saves unreadable, raise it and givemigrateSave(old, fromVersion)(andmigrateProfile(old, fromVersion)) returning the new shape: the host runs it once beforesetup()and its result replaces the game’s own keys (shell keys stay). WithoutmigrateSave, or for a save from a newer version, the lobby says the save was made by another version and offers no Continue; an old profile withoutmigrateProfileis used as it is. Pages with differentsaveVersionnever share a room (the newer one is told to reload). - Stores:
memorySaves()andfileSaves(dir)innode.ts(the demo server uses files:TICKCLI_SAVES, default~/.tickcli-saves). A host app can bring its ownSavePort; keeping earlier copies of each world save (the save before a session’s first write, one a day) helps with support.fileSaveswrites async (temp file, fsync, rename), keeps no earlier copies, does not queue writes per file, and reads a damaged file as no save (after moving it to.bad-<time>). The store hears one session id per room and the save format (store(..., {session, v})). - Example:
examples/games/delve.html(levels, xp, gold, potions, weapons and outfits by level, hp and mp, team lives, depth).