Appearance
Offline progress
Credit away-time on reload and on tab return, with a "welcome back" summary. Three pieces: pageLifecycle (detects and measures backgrounding), offline-progress (caps and advances), and the save's savedAt timestamp (measures reloads).
Wiring
ts
import { createEngine, SaveManager, formatDuration, formatNumber } from "@idlekitjs/core";
import { LocalStorageAdapter } from "@idlekitjs/storage/local-storage";
import { autosave } from "@idlekitjs/plugins/autosave";
import { offlineProgress } from "@idlekitjs/plugins/offline-progress";
import { pageLifecycle } from "@idlekitjs/browser/page-lifecycle";
const MAX_OFFLINE_MS = 8 * 60 * 60 * 1000; // 8 h cap — game design, yours to pick
const engine = createEngine<State>({ initialState /* , renderer, scheduler */ });
engine.addSystem((state, dt) => {
state.coins += state.rate * dt;
});
const save = new SaveManager<State>({
key: "my-game",
version: 1,
adapter: new LocalStorageAdapter(),
});
engine.use(pageLifecycle());
engine.use(offlineProgress({ maxMs: MAX_OFFLINE_MS }));
engine.use(autosave({ manager: save, getState: () => engine.state, intervalMs: 15_000 }));That's functionally complete: hiding the tab pauses the loop; returning (or reloading) advances the simulation by the capped elapsed time, in the same fixed steps as real time — so every mechanic behaves exactly as if the player had stayed.
The "welcome back" summary
The plugin advances the state but doesn't narrate. Snapshot around the catch-up points yourself.
Reload catch-up happens inside engine.load:
ts
const before = engine.state.coins;
const hadSave = await engine.load(save);
const gained = engine.state.coins - before;
if (hadSave && gained > 0) {
showToast(`While you were away: +${formatNumber(gained)} coins`);
}
engine.start();Same-session catch-up is announced by the resume event — note that extensions registered before offlineProgress see the event first, so snapshot in a handler registered before it, or simply report the elapsed time:
ts
engine.events.on("resume", (elapsedMs) => {
if (elapsedMs > 60_000) {
showToast(`Welcome back! You were away ${formatDuration(elapsedMs)}`);
}
});Testing it quickly
Simulate a stale save instead of waiting an hour:
ts
import { MemoryAdapter } from "@idlekitjs/storage/memory";
const adapter = new MemoryAdapter();
adapter.write(
"my-game",
JSON.stringify({
version: 1,
savedAt: Date.now() - 3_600_000, // "saved" an hour ago
state: { coins: 0, rate: 2 },
}),
);
// load through a SaveManager on this adapter -> 2 coins/s × capped 1 h(That's exactly how the toolkit's own tests exercise it.)
Gotchas
- Both pieces, always. Without
pageLifecycle, backgrounding gives you a throttled half-simulation instead of a clean pause + credit. WithoutofflineProgress, elapsed time is measured and thrown away. - The cap is per catch-up, not per day — pick it as a design decision and say it in-game.
- Boosts and crafting jobs run down during catch-up (consistent with the production they affect) — see the boosts note if you want wall-clock exceptions.