Offline & sync
The game is offline-first: everything playable lives on the phone. Cloud backup is
opt-in, off by default, and never required to play.
How saving works
- All game state lives in a local SQLite database (Room) on the device — playable fully offline, surviving restarts.
- Every state change (a step import, an upgrade, a trial tap, an ascension) is written as an operation with a stable unique ID, committed in the same transaction as the state it caused. A crash can neither lose an earned attack nor award it twice.
- If cloud backup is off, the app simply says “saved locally” — it never implies a backup exists.
Save states you may see
| Status | Meaning |
| Saved locally | Progress is safe on this phone; nothing has been sent anywhere. |
| Waiting to sync | Backup is on, operations are queued, the connection isn’t there yet. |
| Backed up | The server acknowledged the operations. |
| Needs attention | Something was rejected or failed; the local operation is kept visible with a recovery path — never silently discarded. |
Cloud backup (opt-in)
- Two sign-in options: Google Sign-In (Android Credential Manager) or a self-hosted email + password account (Argon2id-hashed, rate-limited, with reset flow). The two account types are never merged automatically by email.
- Sign-in tokens are stored encrypted with the Android Keystore — never in plain text.
- The phone uploads its operation log; the server replays the same deterministic rules and stores the result in PostgreSQL. Duplicate uploads award nothing twice (idempotency by operation ID).
- Restore on a new phone replays your operations — it is never a blind overwrite. Merging two separately-played profiles happens only through an explicit “Use cloud progress here” choice.
- Opt-out keeps all local progress; “Delete my cloud data” removes the server account for real.
- Sync runs over a versioned API (sync v1) with bounded retries when connectivity returns.
Operation catalogue (sync v1)
| Operation | Payload | What it does |
steps.import | bucket start, aggregate count | Reports one Health Connect interval’s high-water mark; the server credits only growth above the previous mark. |
mode.set | automatic: bool | The auto-attack switch — pause or resume step resolution. |
actions.spend | count ≥ 1 | Resolve saved steps as attacks (Strike/All buttons or resuming auto-attack). No tap-to-attack exists. |
hero.select | heroId | Select a gameplay hero; free and reversible. |
hero.upgrade.buy | heroId, upgradeId | Buy one rung of a hero’s upgrade ladder, in order. |
hero.level.buy | heroId, count 1–100 | Buy endless hero levels (rules v2+). |
ascension.perform | — | Perform the voluntary ascension; validated against the area gate. |
rules.upgrade | toVersion 2 or 3 | One-time forward conversion of a profile to a newer economy ruleset. |
cosmetic.buy | outfitId | Buy a visual outfit with play-earned gold. |
cosmetic.equip | outfitId | Equip an owned outfit; combat-neutral. |
trial.attack | count 1–1000 | Resolve intentional trial taps; committed one tap per operation by default so replays can’t double-clear. |
upgrade.buy | — | Legacy: the retired global damage ladder, still accepted for old logs. |
Privacy
- Step and health data stays on the device unless you opt in to backup; the server stores aggregated game operations, not raw activity records.
- Logs and metrics never expose raw activity values, tokens, emails or account IDs.
- A client-reported step count is not treated as cryptographic proof of walking; competitive features would need a separate trust model (and none are planned — see the roadmap).
- The server code is open in the same repository as the game; you can run your own.