hallway8 / docs /COLLECTION.md
alvations's picture
Deploy Hallway 8 (multi-arc memory game)
d67855c verified
|
Raw
History Blame Contribute Delete
9.27 kB

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 the ACHS array (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 is i18n.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.