The collection: Memory (achievements + recollections), save/load, codes
The Memory overlay is the game's persistence and replayability layer. It
turns the things a player does across runs (escaping arcs, finding alternate
endings, uncovering flashback fragments, how they won) into a record they can
see fill in, carry between devices, and extend with codes. It is reached from a
quiet corner button (#ach-open, in the top-right cluster with the sound and
settings toggles, shown on the selection screen only) so it lives off the
landing's main path; the overlay's header is Memory (mem_title). It is
engine-wide: arcs supply flashback/ending content, the engine supplies the board.
Everything here is client-side. Progress lives in localStorage under
h8_mem; the server never sees or scores it. This is the deliberate opposite of
the game's core answer state (which is server-only), a collection is the
player's to keep, so it stays on the player's machine.
Layout
Top to bottom (device controls first, then the record):
- Save / Load and a redeem Code row (see below).
- Achievements: a grid of tiles, each a drawn line glyph in the game's
thin, single-weight noir hand. Only earned achievements appear, populated
as they unlock; there is no wall of padlocks and no
{n} of {total}count (it would spoil how many exist). Order follows theACHSarray (grouped). - Recollections (
col_section): the flashback fragment ledger, a quieter inset panel: one row per arc, a dot per fragment, filled in the arc's skin colour as it is seen. A seen dot is a button: tap it to recall that fragment's own words in a fleeting italic line under the ledger (see Fragment recall).
All of it is driven from renderAchievements() in static/game.js.
The badges
The board includes the play achievements, the records (how a run was won:
steady, cold, nerve, streak, see below), the hard-mode escapes, and one
hidden studio badge. Only earned badges are shown, so a new player sees an empty
board (an ach_empty line) that fills in as they play; there is no count and no
locked tiles. The studio badge, like all the rest, simply appears once redeemed.
Badge (id) |
Unlocked by |
|---|---|
out_hallway-eight / out_stairway8 / out_coach8 |
Escape that arc once |
out_all |
Escape all three arcs |
flawless |
Escape an arc with no reset (clean first run) |
eight_times |
Escape one arc eight times |
end_hallway-eight / end_stairway8 / end_coach8 |
Find that arc's alternate (false-exit) ending |
npc_knows |
Be acknowledged by the loop-aware NPC |
shared |
Pass the place on (any share target) |
opening_night |
Play on release day (unlocks the day after) |
flash_hallway-eight / flash_stairway8 / flash_coach8 |
See every flashback fragment in that arc |
total_recall |
See every fragment, in every arc |
melon (hidden) |
Melon Supporter: redeemed with a studio code |
Badge criteria live as {id, test()} in the ACHS array; the test reads
mem. Names and one-line descriptions are localized UI strings
(ach_n_<id> / ach_d_<id>), with an English fallback in ACH_TEXT.
Descriptions are written to avoid spoiling the specific detail a player must
watch, they name the achievement, never the anomaly.
The Melon Supporter badge is the one deliberate style break
Every other icon is monochrome line-work. The studio badge is a warm, full-colour watermelon slice: the single intentional break from the noir palette, a small thank-you that pops. Its name is Melon Supporter and its caption is a thank-you note, not a task. It exists only once redeemed, so it reads as a bonus, never as something withheld.
Fragment recall
When a player uncovers a flashback during Act 2, the client already receives the
fragment's idx and its localized line. recordFlashback(arc, idx, line)
stores the index in mem.flash[arc] and the text in mem.flashText[arc],
so the collection can quote the fragment back later. In the ledger, tapping a
filled dot surfaces its stored words (col_recall is the button label;
col_recall_empty is the fallback for a fragment collected before text was
kept). The recalled line fades in, and a ring marks the chosen dot.
The text is captured in whatever locale it was collected in, a memory as it was seen. Only collected fragments are ever quoted; uncollected ones stay dots.
Records (how a run was won): now achievements
How a run was won is graded and surfaced as achievements on the board (not a
separate strip), so, like every other badge, they stay hidden until earned. Each
escape's winning climb (the unbroken run from level 0 to the exit) is graded on
marks tracked client-side in a live run object and stored in mem.records;
matching ACHS entries turn each into a badge:
- Steady (
steady): won without ever admitting a guess (no low-confidence re-decide on the winning climb). - Cold read (
cold): won without a single touch or second look (pure memory, no Act 2 assistance). - Nerve (
nerve): the final call was a correct turn-back on a real change (the hardest, clutch read), not a walk-on. - On a roll (
streak): a best clean streak of three consecutive escapes made without a reset.
The run object resets whenever a climb starts fresh (entering a corridor, a
wrong call, or a fold), so the marks always describe the actual winning climb.
None of this changes the win logic; it only observes it.
On the win screen, the strongest mark of the just-finished climb also surfaces as
a fleeting read record, one in-world line (read_steady / read_cold /
read_nerve) tucked under the existing "look back" run-count line and fading with
it. It is the in-the-moment skill signal, in the game's own quiet voice, no
numbers.
Save and load (obfuscated, not encrypted)
The Save button exports eight-save.json; Load reads one back. The file
holds three blobs, the player's memory (NPC habits + collection), the computed
achievement state, and the settings (difficulty, reading register, text
size, brightness, reduce-motion, language), each passed through encBlob, a
keystream XOR (FNV-1a seed → xorshift stream) seeded with the phrase melon8.
This is light obfuscation, not cryptography: it stops a save from being
human-readable and stops the number of achievements being eyeballed from the
JSON, nothing more. Loading a file decodes the memory back into mem and
restores the settings to localStorage (re-applying them live), so a device
swap carries the whole setup, not just progress. A wrong or corrupt file is
ignored silently.
Promo codes
An 8-character code redeems a reward (redeemPromo in static/game.js). The
engine is extensible (arcs or features could be gated later); today two families
exist:
MELONADE→ unlocks the Melon Supporter badge.I18N-XX(XX = a short ISO code, e.g.I18N-TH) → unlocks one of the 21 supported languages, adds it to the picker, and switches to it. The map of short code → locale isi18n.PROMO_LOCALE, exposed to the client via/api/langs(unlock_map()), restricted to locales that actually have a compiled catalog.
Redeemed codes are recorded in mem.promos and persist through save/load,
so an unlocked language or the studio badge survives export/import and reappears
in the picker on load (rebuildLangPicker()).
Memory shape (blankMem)
{ v, flash:{arc:[idx…]}, flashText:{arc:{idx:line}}, wins:{arc:n},
clean:{arc:bool}, ends:{arc:bool}, npc:{arc:{prop:true}}, shared:bool,
releaseDay:bool, plays:n, promos:[code…], melon:bool, langs:[locale…],
records:{ steady:bool, cold:bool, nerve:bool, streak:n, best:n } }
Recording happens at the natural moments: recordWin/noteAnomaly on a win,
recordEnding on a false exit, recordFlashback on a touch, recordShare on a
share, recordPlay on entering a loop. saveMem() writes through and re-renders
the board if it is open.
Localization
All collection strings go through the same pipeline as the rest of the game
(see LOCALIZATION.md): the two section headers
(ach_section, col_section), the 34 badge strings (ach_n_* / ach_d_*),
and the two recall strings (col_recall, col_recall_empty). Arc names and
"Melon Labs" are kept verbatim across locales; metaphoric lines are translated
by meaning. All are translated and compiled for the eight exposed base locales,
with English fallback per key for the hidden ones.
Conventions to keep
- The collection stays client-side. Never move answer state here, and never send collection state to the server for scoring.
- Descriptions never spoil. A badge names the feat, not the detail to watch.
- Only earned badges show, no count. The board fills in as achievements
unlock; do not reintroduce locked tiles or a
{n} of {total}denominator (it spoils how many exist). - One colour break, only the melon. Every other icon stays monochrome line-work in the chrome/accent tier.