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):

{ "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 — 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

TitleScreen UI (scenes/ui/TitleScreen.gd)

Reset scopes (scenes/ui/ResetConfirmModal.gd)

Settings → Reset Game offers two scopes once several profiles exist — "Only <name>" (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

Key files