# Player Profiles

Profiles let several people share one machine (or one browser storage on web):
each player owns their cosmetics, lifetime tallies, tips, redeemed codes,
scores, and tutorial state, and sees their own save list. Spec:
`docs/superpowers/specs/2026-07-29-player-profiles-design.md` (all decisions
resolved; shipped across PRs #245–#249).

## The model in one paragraph

The game always has exactly one **active profile**; everything personal reads
and writes through it. Identity is chosen at the TitleScreen ("profile-first"),
never derived from a save slot — the personal state exists outside any run
(title screen, code redemption). Slots stay physically global
(`user://saves/slot_N.json`, 12 of them); each save carries an owner tag
(`run.profile_id`), and the picker filters by it. Switching profiles happens
only at the TitleScreen through one heavyweight reload point.

## Storage

`config.json` is the profile store (`config_version = 2`):

```json
{ "config_version": 2, "active_profile": "p1",
  "profiles": { "p1": { "name": "Mike", "...": "all personal keys" } } }
```

Everything about the shape, the migration-on-read (`_migrate_config`), the
profile-scoped accessors (`_profile(cfg)`), and the per-profile key inventory
lives in [Save System and Migration](/docs/underroot/save-system-and-migration)
— that article is the storage reference; this one covers behavior and UI.

Per-run: `run.cosmetics_loadout` (the run's look — profiles own the *wardrobe*
`cosmetics_owned`, runs own the *outfit*) and `run.profile_id` (owner tag,
`""` = unclaimed). Both arrived with `SAVE_VERSION = 4`.

Machine-level (`settings.json`): music, perf, offline speed, `hud_style` /
`ledger_hold` — deliberately not per-profile (the Legacy UI is transitional).

## SaveManager API

| Method | Behavior |
|---|---|
| `active_profile_id()` / `active_profile_name()` | The current profile. |
| `profiles_list()` | `[{id, name}]`. |
| `set_active_profile(id)` | Persists + emits `EventBus.profile_changed(id)`. CosmeticManager reloads on the signal; everything else re-reads on its poll tick or is rebuilt by the TitleScreen reload. |
| `create_profile(name)` | Cap `MAX_PROFILES = 8`, names ≤16 chars. |
| `rename_profile(id, name)` | — |
| `delete_profile(id)` | Deletes the profile **and its tagged saves** (design ruling: deleting your profile deletes your data). Refuses the active and the last-remaining profile. Deleted slots land in the one-deep trash (`slot_N.deleted.json`) — recoverable by renaming. |
| `slots_tagged(id)` | Owned slot indices — feeds the delete confirmation's doomed-runs list. |
| `retag_slot(idx, pid)` | Rewrites a slot's owner in place (`""` = unclaimed; unknown pid refused). Raw file write — only while no run is hydrated (picker-time), same constraint as `update_run_values`. |
| `reset_active_profile()` | Deletes the profile's tagged saves (through the trash too) and resets its blob to name-only. The gentle half of the dual-scope reset; `factory_reset()` remains the machine wipe. |

## Tagging and adoption

- New runs stamp `run.profile_id = active_profile_id()`
  (`GameManager.reset_for_new_run`).
- Pre-profile saves are **unclaimed**. Clicking one while several profiles
  exist prompts "Claim it for &lt;name&gt;?" — claiming is permanent ownership, so
  it is never silent (a silent adopt once handed a 144-day run to the wrong
  player). With a single profile, adoption stays invisible
  (`GameManager._apply_save_data` back-fills the tag on continue).
- Mis-claims are player-fixable: the ⇄ button on a slot tile opens "Whose run
  is this?" — any profile, or back to Unclaimed (via `retag_slot`).

## TitleScreen UI (`scenes/ui/TitleScreen.gd`)

- **Corner chip**, top-right, on every title state. Solo: a dim invitation —
  "More than 1 person sharing this PC?  ＋ Players" (the entry point; a chip
  hidden until a second profile exists could never create one). With ≥2:
  "Playing as: &lt;name&gt; ▾", content-sized so 16-char names fit.
- **Players menu** (modal): pick to switch (● marks active), ✎ rename,
  † delete (never offered on the active profile), ＋ New player while under
  the cap. Names via a small LineEdit dialog, Enter submits.
- **Switching** = the account-restore reload shape: cancel in-flight share
  HTTP, `clear_active_slot()`, `set_active_profile()`, capture music,
  `reload_current_scene()`. Creating a profile switches straight into it — a
  new player lands on their own empty picker, and their first run gets the
  tutorial (their `played_before` is fresh).
- **Picker filtering**: with ≥2 profiles the picker shows the active player's
  saves + unclaimed, with a "Show all (N)" header toggle; tiles then carry a
  9px owner label ("unclaimed" for untagged). Internally `_slots` stays ALL
  slots (`_visible_slots()` is display-only) so the physical 12-cap and the
  ≥2 guards stay correct.
- **Cross-profile continue**: clicking another player's run prompts "This is
  X's run. Play it as X?" — confirming switches profile and continues. Tagged
  saves are never adoptable.
- **Profile deletion** is a typed-name confirmation (ResetConfirmModal's
  shape) listing every doomed run and nudging toward a backup code first.

## Reset scopes (`scenes/ui/ResetConfirmModal.gd`)

Settings → Reset Game offers two scopes once several profiles exist — "Only
&lt;name&gt;" (default; `reset_active_profile()`) and "Everything (all players)"
(`factory_reset()`). Solo machines see the single full reset. Both keep the
typed-RESET guard and the backup Download/Copy buttons; both restart at the
intro. `settings.json` survives both.

## Consequences worth remembering

- **Lifetime tallies follow the person** — they span a profile's slots and
  lineages and never mix between players.
- **One-time codes are once per profile** — each household member can redeem
  the secret suit codes.
- **The account-bundle backup code snapshots every profile** (it embeds the
  whole config); imports of pre-profile bundles migrate on the way in.
- **Deleted slots are one rename from recovery** — every delete path routes
  through `delete_slot()`, which renames to `slot_N.deleted.json` (per-slot,
  one deep; wiped by factory reset).
- **A fresh profile is a fresh player**: tutorial, nudges, talisman-HUD
  default (unless the machine has an explicit `hud_style`), own Top Runs,
  own name rotation for default digger names.
- Website ledger submissions needed no changes — score rows already carry the
  player name, and each profile keeps its own `scores` + `shared_runs` lock.

## Key files

- `scripts/core/SaveManager.gd` — profile store, API, migration, reset scopes, slot trash.
- `scenes/ui/TitleScreen.gd` — chip, menu, picker filtering, claim/reassign/delete flows.
- `scenes/ui/ResetConfirmModal.gd` — dual-scope typed reset.
- `scripts/core/GameManager.gd` — `profile_id` stamp + adopt.
- `scripts/core/CosmeticManager.gd` — reloads on `profile_changed`.

## Related

- [Save System and Migration](/docs/underroot/save-system-and-migration)
- [Cosmetics and Digger Forms](/docs/underroot/cosmetics-and-digger-forms)
- [Codes and Redemption](/docs/underroot/codes-and-redemption)
- [GameManager and the Run Lifecycle](/docs/underroot/gamemanager-and-the-run-lifecycle)