la-li-lu-le-lo/AGENTS.md
rain 346d08ad6a Correct the deploy timing: Pages is cached, not instant
Observed the public URL serving the previous build for ~3.5 minutes after a
push. Responses carry x-pages-cache: true and cache-control: max-age=600, and
a cache-busting query does not bypass it. The earlier claim of ~15s was wrong.

AGENTS.md and deploy.sh now say to check the repo raw file FIRST: if that
matches the local build the push landed and only the cache is behind, so don't
start changing things.
2026-09-15 17:45:37 -04:00

179 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# AGENTS.md
Orientation for anyone (human or agent) picking this repo up cold. Read this before changing anything.
## What this is
A single-page companion for 100%-ing **Metal Gear Solid 2: Sons of Liberty — Master Collection Version** (Steam appid `2131640`). It contains a full walkthrough, all 51 achievements, all 394 dog tags, rebuilt guard-position maps, location screenshots, and live Steam achievement sync.
- **Live:** <https://rain.pages.melonbread.xyz/la-li-lu-le-lo/>
- **Repo:** <https://git.melonbread.xyz/rain/la-li-lu-le-lo> (branch `pages`, which is the default branch)
- **Owner:** `rain` / `rain@melonbread.xyz`
## Golden rules
1. **Never edit `index.html`.** It is generated. Your change will be silently overwritten on the next build. Edit `build/` and run `./deploy.sh`.
2. **Run `node tools/verify.js` before deploying.** It catches most of the ways this project breaks.
3. **Run `python3 tools/theme-contrast.py` after touching any colour or theme.**
4. The build has assertions. If one fires, **fix the data, do not delete the assertion.**
## Layout
```
index.html generated - do not edit
steam-sync.py generated copy of build/steam-sync.py
steam-proxy-worker.js generated copy of build/steam-proxy-worker.js
deploy.sh build + commit + push
tools/verify.js headless smoke test (stubbed DOM, no browser)
tools/theme-contrast.py WCAG contrast check across all themes
build/ ALL the sources
build.py the build script
template.html the page: CSS + JS + markup, with __PLACEHOLDER__ slots
achievements_src.py hand-written how-to text for all 51 achievements
roadmap_src.py hand-written walkthrough steps + Big Boss content
*.json extracted data (see Provenance)
```
`build.py` writes to `OUT = os.path.dirname(HERE)`, i.e. the repo root, and copies the two helper files up from `build/`. So the helpers are edited in `build/` and published to the root.
## How the page works
One self-contained HTML file. No framework, no build step at runtime, no network needed except for hot-linked photos.
`build.py` injects five JSON blobs into `template.html`:
| Blob | What |
|---|---|
| `ACHIEVEMENTS` | 51 entries: name, desc, official Steam icon, category, missable flag, guide HTML, photos |
| `ROADMAP` | 7 phases / 48 steps, each with body HTML, `gets`, `areas`, `photos` |
| `DOGTAGS` | 10 lists (chapter × difficulty), 394 tags with name, area, conditional note |
| `MAPS` | 10 spreadsheet sheets: palette, RLE fill runs, text cells, merges, col widths, row heights |
| `BIGBOSS` | criteria, heal points, time checkpoints, boss strategies |
All interactive state lives in **one `localStorage` key**, `mgs2mc-guide-v1`:
```js
S = { ach:{id:true}, tags:{'listId:index':true}, road:{stepId:true},
items:{'stepId:slug':true}, hideDone, tab, roadDiff,
steam:{profile,id64,persona,unlocked:{api:true},lastSync} }
```
There is a **separate** key `mgs2-theme` for the colour theme, so it is not affected by Export/Import.
## Build pipeline
Order matters — later stages depend on earlier ones:
1. **achievements** — merge `achievements_src.py` with `steam_official.json` (names/descriptions/icons from Steam's own XML).
2. **screenshots** — assemble per-achievement photo lists from the two guides, drop perceptual duplicates, pin multi-location photos to steps, attach `st.photos`.
3. **colour coding** — classify every `<b>`/`<i>` in the roadmap as tag / item / achievement / missable / boss / area.
4. **step sub-checklists** — extract `gets` from the colourised HTML, map tag areas (`TAGMAP`), attach `st.areas`.
5. **dog tags**, then **inject** everything into `template.html`.
### Assertions (do not remove)
- every dog-tag area is claimed by exactly one step (`TAGMAP`) — nothing silently unmapped
- every `PHOTO_STEP` key matches at least one real photo — catches typos in the 40-char hashes
- every photo of a split achievement is pinned to a step
- no achievement whose photos span several steps is left unpinned
## Verifying changes
**`node tools/verify.js`** loads `index.html`, runs its script against a stubbed DOM/canvas and checks: boot does not throw, all 9 tabs render, all 10 maps draw without non-finite geometry, no label is squashed, dog-tag sub-checkboxes cover every tag in every list, no photo appears twice, every theme has a CSS block, and no personal data shipped.
**`python3 tools/theme-contrast.py`** checks every foreground against `--panel` plus `--on-acc` against `--acc`, across all themes. Fails under 3.0:1.
**There is no browser tooling available.** `browser_screenshot` crashes the harness GUI process (SIGSEGV in the Electron/sharp path) — do not call it. The Node stub approach above is the substitute; it verifies logic, not layout, so **layout changes cannot be verified here** and should be stated as unverified.
Quick manual checks worth knowing:
```bash
grep -c 'class="lb"' index.html # photos present in the build
node -e "..." # see tools/verify.js for the stub pattern
```
## Data provenance
All third-party content is credited in the page's Sources tab and in README.md.
| Data | Source | Notes |
|---|---|---|
| Achievement list, icons, apinames | Steam community XML | `steam_official.json` — 51 entries, `ach_002_001``ach_002_051` |
| Achievement how-to text | Steam guide by Cole ヴ Viper + Dayngl's Guides | hand-written into `achievements_src.py` |
| 394 dog tags | Video Chums checklist | `dogtags_vc.json` (names + areas) |
| Conditional-spawn notes | u/Spikeyroxas spreadsheet | `dogtag_notes.json` (81 notes) |
| Area maps | same spreadsheet | `maps.json` — the sheets are cell-grid art, not images |
| Location photos | Steam guide + Dayngl's Guides | hot-linked, never downloaded into the repo |
| Achievement removal check | TrueSteamAchievements | confirmed 51, incl. `Hold Up-aholic` which the Steam guide skips |
**Refreshing a dataset** means re-extracting into `build/*.json` and re-running the build. The extraction was originally done with ad-hoc Python against the source pages; if a source changes shape, re-derive rather than patching the JSON by hand.
## Traps already hit (don't repeat)
- **Canvas `fillText` with `maxWidth` condenses text, it does not truncate.** Narrow clip widths turn labels into an unreadable smear. `drawMap` measures each label and steps the font down (to 7px), then lets it overflow rather than squashing. `tools/verify.js` fails if any label comes out condensed.
- **Excel does not clip overflowing text at a coloured cell.** Only a cell with actual content stops it. Clipping map labels at fill cells made them tiny; clip at text cells only.
- **`data-sub` must hold the handler key, not the CSS class.** It was set to the class, so missable achievements emitted `data-sub="ach miss"` and their tick boxes did nothing — and missables are the whole point.
- **Per-achievement photo caps break per-step distribution.** Capping at 4 dropped exactly the photos that needed spreading across steps. Deduplicate globally, distribute per step, cap per step.
- **Normalize both sides of a name lookup.** An items list written with spaces (`"stun grenades"`) was compared against space-stripped text, so no multi-word item ever matched — 4 spans instead of 31.
- **`pkill -f "steam-sync"` kills the invoking shell**, because that shell's own command line contains the pattern. Use `pkill -f "[s]team-sync"` **in a call that does not also contain the literal string**, or split kill and start into separate calls.
- **A bad slice edit duplicated half of `build.py`.** The tell was the build printing every stage twice — check build output length when editing that file.
- **Code that uses a computed value must run after it is computed.** The photo-to-step assignment needed `gets`, which is built later in the script.
## Steam sync
Steam's community XML sends no CORS headers, so a browser cannot read it directly. Two ways around it:
**Local helper (what is deployed uses).** `steam-sync.py` serves the folder and proxies `/api/steam?profile=…`. A hosted HTTPS page calls it at `http://127.0.0.1:8765`, so **the helper must be running on the viewer's machine**:
```bash
nohup python3 ~/Desktop/la-li-lu-le-lo/steam-sync.py > /tmp/steam-sync.log 2>&1 &
```
It answers `Access-Control-Allow-Private-Network: true` on the preflight — Chrome blocks a public page from reaching localhost without it.
**Cloudflare Worker.** `steam-proxy-worker.js` does the same job with no local process. Deploy it and set `STEAM_PROXY` near the top of the page script. `ALLOW_ORIGIN` is pinned to the Pages origin.
`syncEndpoint()` resolves: localhost → same-origin `/api/steam`; `STEAM_PROXY` set → that; otherwise → the local helper.
Parsing notes: match achievements by **apiname** (`ach_002_0xx`), never by display name. The XML has **no `<steamID>` persona field** — fall back to `customURL`, then to what the user typed.
## Themes
Ten dark themes, switched from the header and applied by a small script in `<head>` before first paint (no flash). Everything is themed through CSS custom properties on `:root` and `html[data-theme="…"]`. **Any new colour must go through a variable**, otherwise it will be wrong in nine themes. Tints use `color-mix()` so they adapt automatically.
Two deliberate choices: the **area maps keep a white background** (they reproduce a spreadsheet; inverting them would make the colour legend lie), and there are **no light themes** (removed on request).
## Deployment
```bash
./deploy.sh "what changed"
```
Builds, commits, pushes to `pages`.
**Do not expect the live site to update immediately.** Pages sits behind a cache
(`x-pages-cache: true`, `cache-control: public, max-age=600`), so the public URL can serve the
previous build for several minutes — observed between ~15 seconds and ~3.5 minutes. A
cache-busting query string does not bypass it.
When the live copy looks stale, **check the repo before debugging the deployment**:
```bash
# 1. did the push actually land? (this is the source of truth)
curl -s https://git.melonbread.xyz/rain/la-li-lu-le-lo/raw/branch/pages/index.html | sha256sum
sha256sum index.html
# 2. then, once those match, wait out the cache on the public URL
curl -s -L https://rain.pages.melonbread.xyz/la-li-lu-le-lo/ | sha256sum
```
If (1) matches and (2) does not, it is the cache — wait, do not start changing things. If (1)
does not match, the push failed and that is the real problem.
## Conventions
- British-ish plain English in user-facing copy; no marketing voice.
- Comments explain **why**, not what, and are only added where the reasoning is non-obvious.
- Content lives in the `*_src.py` files as HTML strings; structural/page changes live in `template.html`.
- Keep the page dependency-free. Every external thing is a hot-linked image; nothing else loads off-origin.
- Do not commit personal data. `tools/verify.js` fails if `DEFAULT_PROFILE` is non-empty.