Save player progress
Goal
The steal kit gets a daily bonus: a player who joins and has had no bonus for a day is paid 50 coins, and the wallet remembers when (bonusAt), so leaving and coming back the same day pays nothing more. The amount is a rule.
It is the whole player-data pattern in one system: read the document the room loaded at join (ctx.players.get(id).data), change a copy, hand it to ctx.data.save. The room writes it to its store, at most once per 6 s per player and always when the player leaves, so the next session starts from it.
Files you will edit
src/game.ts
Steps
Add the amount to
rulesindefineGame, beside the others.tsrules: { // ...the rules already there dailyBonus: 50, },Import the wallet helpers, beside the other imports.
walletOfreads a seated player's wallet;keepsaves a new one.tsimport { keep, walletOf } from './bank';Write the system, below the imports. It reads only this step's joins.
heist.now(ctx)is the server's clock in ms: the room stamps it on every join (PlayerHandle.joinedAt), because the guest has no clock of its own.bonusAtis a field of your own: the wallet keeps any field it does not know.ts/** A day, in ms: the server's clock counts in ms. */ const DAY_MS = 24 * 60 * 60 * 1000; /** Authority: a player who joins and has had no bonus for a day gets one, remembered in their wallet. */ function dailyBonus(ctx: GameContext): void { const events = ctx.events; for (let i = 0; i < events.length; i += 1) { const event = events[i]; if (event.tag !== 'player-joined') continue; const player = event.val.player; const wallet = walletOf(ctx, player); if (wallet === undefined) continue; const now = heist.now(ctx); const last = typeof wallet.bonusAt === 'number' ? wallet.bonusAt : 0; if (now - last < DAY_MS) continue; const coins = wallet.coins + Number(ctx.rules.dailyBonus); keep(ctx, player, { ...wallet, coins, bonusAt: now }); } }Never change the wallet
walletOfreturned in place: that object is what the room last saved, and a save is a new document.Wire it in
systems, right afterseats, which loads each joining wallet and pays for the time away first.tssystems: [ { run: seats, on: 'authority' }, { run: dailyBonus, on: 'authority' }, // ...the systems already there ],
Verify
sh
npm test
npx gameable serve --directnpm test now fails where it counts coins exactly: each test player gets the bonus on their first join. That is the change working; set dailyBonus: 0 there, or add 50 to the expected coins. In a room (http://localhost:5193/?room=new&rooms=http://localhost:8790), the HUD shows 50 coins on your first join. Close the tab and open the same address again: still 50, not 100. serve --direct resolves no identities, so it keys a document by seat (seat-0): alone, you are seat 0 again. It keeps the documents in memory for as long as it runs; with GAMEABLE_PG_URL set, in Postgres across restarts.
See also
- Play with friends: a room server beside the dev server, and two tabs in one room
- Send a message: what a page may ask the authority for, such as the shield
- Multiplayer: roles, and why only the authority writes
templates/steal/src/systems/seats.ts: the same pattern for offline income, fromsavedAttojoinedAt