hallway8 / docs /COLLECTION.md
alvations's picture
Deploy Hallway 8 (multi-arc memory game)
d67855c verified
|
Raw
History Blame Contribute Delete
9.27 kB
<!-- SPDX-License-Identifier: Apache-2.0 -->
<!-- Copyright 2026 alvations (Melon Lab) -->
# 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`)
```js
{ 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`](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.