diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
new file mode 100644
index 0000000..71d7bea
--- /dev/null
+++ b/.github/workflows/tests.yml
@@ -0,0 +1,61 @@
+# Every push and pull request runs the whole suite in a real browser -- Chromium,
+# WebKit (Safari's engine) and Firefox -- the same ./tests/run.sh as a local `npm test`,
+# so a PR shows green or red.
+#
+# Also checked: mod.js parses, the committed userscript was rebuilt from it, and
+# the author's two files are untouched (his changelog byte for byte, index.html
+# his plus the one script tag) against his repository, fetched read-only.
+name: tests
+
+on:
+ push:
+ pull_request:
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+ timeout-minutes: 60
+ # WebKit is the engine Safari is built on: it is what caught the skill rows
+ # wrapping under a classic scrollbar, which Chromium alone never showed.
+ strategy:
+ fail-fast: false
+ matrix:
+ browser: [chromium, webkit, firefox]
+ name: tests (${{ matrix.browser }})
+ env:
+ BROWSER: ${{ matrix.browser }}
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: npm
+
+ # The browsers are ~100-200 MB each and downloaded on every run. Keyed on the
+ # lockfile (which pins the Playwright version, and so the browser build);
+ # `install` below is a no-op for the download when this hits, but still
+ # adds the OS libraries, which are not in the cache.
+ - name: Cache the browsers
+ uses: actions/cache@v4
+ with:
+ path: ~/.cache/ms-playwright
+ key: playwright-${{ runner.os }}-${{ matrix.browser }}-${{ hashFiles('package-lock.json') }}
+
+ - name: Install
+ run: |
+ npm ci
+ npx playwright install --with-deps ${{ matrix.browser }}
+
+ - name: mod.js parses, userscript current
+ run: npm run check
+
+ - name: The author's files are his
+ run: ./tests/authorfiles.sh
+
+ - name: Test suite
+ run: npm test
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..c2658d7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+node_modules/
diff --git a/.nojekyll b/.nojekyll
new file mode 100644
index 0000000..e69de29
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..6221889
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,991 @@
+# proto23 mod — working notes for Claude
+
+## What this is
+
+A mod for a single-file idle RPG. The game is `index.html`: 837 KB, ~14,800
+lines, **not minified**, every global declared with `var` at top level, no
+strict mode. That is what makes the mod possible.
+
+`mod.js` is the entire mod, loaded by one appended script tag at the very
+bottom of `index.html`:
+
+```html
+
+
+```
+
+Because it loads last and shares global scope, it can read and wrap anything
+the game defines. That single tag is the **only** change to `index.html` —
+keep it that way, so `git checkout index.html` is always a clean undo.
+
+`MOD-NOTES.md` is the full design record, section by section, with the
+reasoning behind each decision and what was tested. Read the relevant section
+before changing anything in that area.
+
+## How to change things
+
+Wrap, don't edit. The pattern used throughout:
+
+```js
+var MOD_thing_original = thing;
+thing = function () {
+ var r = MOD_thing_original.apply(this, arguments);
+ /* extra behaviour */
+ return r; // always preserve the return value
+};
+```
+
+Preserving the return matters: `allbuff` does `let dm = skl.fgt.use()` and
+adds the result to stats, so a wrapper that swallows a return silently
+changes the game's math.
+
+## Save format constraints — read before adding content
+
+The game's save/load is positional in places, which means some things are
+append-only. Getting this wrong corrupts existing saves.
+
+- **Milestone "granted" flags are stored by array index.** Milestones must be
+ appended, never inserted or reordered, and their levels must stay ascending.
+ `tests/audit.mjs` checks this.
+- **Skill `exp` and `p` are stored positionally** over `for..in skl`. New
+ skills go at the **end** of `skl`, which happens naturally since `mod.js`
+ runs after the game script.
+- **Actions are stored by id**, so those are safe to add anywhere.
+- On load, milestones re-fire **before** `p` is restored. So a perk must never
+ do `sk.p += 0.1` — the restore would wipe it. Use a declarative flag on the
+ milestone (`xpBonus`, `sectionXp`) read at point of use instead.
+- `you.res` and `you.mods` are saved. Any mutation of them needs delta
+ tracking in `global.flags` or it compounds on every load. See
+ `MOD_applyMoneyDrops` for the pattern.
+
+## The bottom bar
+
+Buttons added to `dom.sl` must match the game's own: plain `inline`, `padding:3px`,
+no `display:inline-block`. An inline-block forces the line box to grow, which
+made the fixed bar 32px instead of 24px and covered the bottom of the panel
+above it. The bar is `position:fixed`, so any height it gains eats game screen.
+
+## DOM constraints in the skill panel
+
+The game's per-second updater reads fixed child indices **within** each skill
+row: `children[0]`, `children[1]`, `children[2].children[0]`. So:
+
+- `dom.skcon.children[m]` must stay aligned with `you.skls[m]`.
+- Never insert an element as a row's first child. Section headers are appended
+ **last** with `order:-1; flex:0 0 100%` — `.skwmmc` is `display:flex`, so
+ that puts them visually first without shifting indices.
+
+## Balance
+
+Enemy dials on `MOD_ENEMY`, live in the browser console:
+
+```js
+kill: 8, die: 20, hit: 0.45, margin: 1.5,
+hpSpread: 0.6, atkSpread: 0.3, bossKill: 2.0, bossDie: 0.7, armor: 1.0
+```
+
+**The skill exp curve is the author's below level 10, and the mod's above.**
+The base game's `expnext = 50 + (lvl+1)^log(9*lvl+1)` is super-exponential —
+109→110 alone costs ~1.2e14 xp, so the cap ladder is unclimbable by ~10¹³. But
+the author only designed skills up to 15 (his 22 perk ladders end between 1 and
+15, median 10), and below that his curve is fine. So section 19 is in two parts:
+
+```js
+MOD_XP = { vanillaTo: 10, total, ratio } // levels < 10: HIS formula, exactly
+ // from 10: his level-9 cost * ratio^n
+```
+
+`ratio` (~1.029) is **solved**, not chosen: it is whatever makes 0→110 cost the
+same wall-clock time the old `50 * 1.106^lvl` curve took at its 2x multiplier.
+The end is exactly as far away; the time is spread evenly instead of the old
+curve's "cap 60 in under an hour, then months on the last twenty levels".
+
+- **Why 10 and not 15.** His level 15 alone costs 823k; 95 more levels even at
+ that flat price is 78M xp, 2.5x the whole budget. 10 is the highest start that
+ still leaves a rising ramp. `setXpCurve({vanillaTo})` accepts 0-12.
+- **The curve is installed twice — at section 19 and at the very END of mod.js.**
+ It used to run once, and every skill created later kept the author's curve:
+ section 29's six masteries cost 7,474,050 xp at level 20 instead of 375, so
+ their ladders to 110 stopped climbing at ~20. Keep the final call last.
+- **The defaults are the original's:** `MOD.skill_xp_mult` 1 (was 2) and
+ `MOD_MONEY.chance` 0 (was 0.15 — the author only turns the coin drop on through
+ the Coin Ring and Ring of Greed, and a 15% base made both rounding errors).
+ Both are settings boxes persisted in localStorage, so a value the player set
+ still wins.
+
+`tests/vanilla.mjs` holds all of this against the author's own page — it blocks
+`mod.js` so the one script tag loads nothing — and asserts every level below 10
+costs exactly what HIS `expnext` returns, not a reimplementation of it.
+
+**Pacing presets (section 45)**: `setPacing('original' | 'fast')`. `fast` is the
+pre-4.2 pacing exactly — `MOD_XP.fixedRatio` 1.106 with `vanillaTo` 0 makes every
+level cost `50 * 1.106^lvl`, plus 2x exp and a 15% coin drop. The curve is not
+persisted on its own, so the chosen preset is stored (`p23_mod_pacing`) and its
+curve re-applied at load; the exp and coin boxes persist on their own keys.
+Since v4.6 each preset also sets Fights (`original` / `scaled`), which persists on
+`p23_mod_fights`. Anything else reads `custom`. `tests/pacing.mjs` checks each
+preset is exact.
+
+The full ladder 10/15/20/30/40/50/60/75/90/110 is genuinely reachable, so
+**model the player AT the cap** — that is what the tests do.
+
+### Fights: Original by default, Scaled on request (section 47, v4.6)
+
+At equal progression the original's opening route is nearly free: median 1.3
+swings to kill and ~20,000 to die, because its creatures are fixed and you
+out-level them. The mod's enemy model (section 8) exists to remove that and
+holds every fight near 8.8 / 22.
+
+**`MOD_FIGHTS.mode` chooses.** On `original`, the default, `MOD_scaleEnemy`
+returns at once for any spawn whose `global.current_z` is one of the author's
+areas, and `MOD_applyAreaLevels()` puts his level ranges back (section 8 keeps
+both: `pop[i]._modLv0` his, `_modLvS` the x1.4). On `scaled` it is section 8
+everywhere, as before 4.6.
+
+- **"The author's areas" is `area[k]._modAuthor`**, set on every area that exists
+ when section 8 starts, before the mod adds any. Areas are not saved, so marking
+ the object is safe. The mod's own — Hollow, Spire, Vigil, the ten rank duels —
+ are always scaled: there is no original to match, and the rank ladder needs
+ `_modKillT`.
+- **The value model reads `_modLvS` whatever the mode**, so an item's price does
+ not change when the setting does.
+- **Raw Meat follows the mode**: his 6% on Original (`_modChance0`), 18% on
+ Scaled. The raise only ever made up for the scaled kill times, and
+ `MOD_MEAT.killSecOriginal` (5s) is the matching assumption.
+- **Balance scripts measure section 8, so they call `setFights('scaled')` first**
+ — allareas, earlybal, combat, fightsmoke, fullbal, targets, baseline and
+ vanilla's gap table. A new script that measures the model must too, or on the
+ default it measures the author's creatures and reports nonsense.
+- `tests/fights.mjs` holds Original against his page: all 70 spawns in his areas
+ identical stat for stat (random() seeded the same on both sides), and every
+ step of his route past the tutorial winnable at arrival. His own game loses
+ Tutorial fight 1 at skill 0, and so does Original — that is his design.
+
+An earlier comparison showed the original as a wall of unwinnable fights. That
+was the old skill curve: it handed the original's player far lower skills for
+the same xp. Compare at equal skill levels, which the matched curve now gives.
+
+### The enemy model, and why it looks the way it does
+
+`dmg_calc` is **subtractive in both directions**: an attack does
+`attacker.str - defender.str` (plus weapon, plus 1, plus crits). The defender's
+STR *is* their armour. Four consequences, all load-bearing:
+
+* **Never scale enemy `strm`.** Raising it raises offence and armour together,
+ and once armour passes the attacker's STR the damage does not get small, it
+ clamps to **zero**. `MOD_ENEMY.armor` exists so this stays visible; leave it
+ at 1. The same applies to `aglm`, which is the denominator of `hit_calc(1)`.
+* **Never judge a fight by a stat ratio.** `enemyHP/playerSTR` and
+ `playerHP/enemySTR` both ignore the subtraction and read several times off.
+ That proxy is what hid a tutorial fight needing 63,000 swings and a whole
+ ladder of `kill 1 / die 1`. Measure through `dmg_calc` and `hit_calc`.
+* **Nothing is replicated, everything is measured.** The base game's defence
+ term goes *negative* at high skill levels (`shdc` at 110 makes its last
+ multiplier −12.3, so your defence is added to the enemy's damage), and it is
+ ill-conditioned besides. Each spawn runs the real `dmg_calc` a few times in
+ both directions inside `MOD_probe` — which parks `giveSkExp`, the crit flag
+ and the DOM node it jiggles — and scales against what actually came out.
+* **The dials are two-sided.** Flooring every multiplier at 1 so nothing is
+ weaker than vanilla leaves pockets the base game itself made unwinnable.
+
+Scaling is anchored to the player's power at spawn, not fitted to a level,
+because within tier 0 alone the player goes from STR 1 to STR ~265. Variety
+comes from the area's level band and each creature's own `stat_p`, and
+`margin` guarantees `kill < die` however those compound.
+
+Damage is heavy-tailed at the top of the ladder: crit rate reaches 33% and a
+crit is about 8x a normal swing, so crits carry roughly three quarters of all
+damage. `MOD_meanDamage` therefore strata on the crit roll rather than taking a
+plain sample mean — the variance of a plain mean is almost entirely "how many
+crits landed", and enemy HP comes straight off that number. `fightsmoke` fights
+nine times per area and reports a median for the same reason.
+
+**Section 46 corrects three lines of the author's combat and death code, and
+the luck fix lives INSIDE `MOD_dmg_calc_original`.** That variable is rebound to a
+version that applies his luck crit multiplier (capped at 50% from luck,
+`MOD_LUCK_CAP`) and floors the Shield Mastery defence bracket at zero. Section 8's
+probe and its incoming-damage wrapper both call through the variable, so enemies
+are sized against the fixed player. **Anything that samples your damage must go
+through `dmg_calc` or `MOD_dmg_calc_original`, never a reference captured before
+section 46**, or it measures a player without luck. `MOD_critRate(att, stt)`
+includes the luck bonus. The bonus rides on `you.mods.crflt` for one call and is
+restored to its exact prior value — `crflt` is saved, and add-then-subtract
+would leave float residue. `tests/basefixes.mjs` holds all three.
+
+**Section 51 is a fourth: every creature held ONE weapon.** `Creature` starts with
+`eqp = [eqp.dummy, eqp.dummy]` and the author wrote 32 per-creature `aff`/`cls`
+lines straight onto it, so all 48 carried the last writer's, and the player's bare
+fists (the same object) leak `lvl/4` and `lvl/5` into every creature's damage.
+Each creature now owns its slots, with his values read back from his source; the
+fists stay the original object. `tests/creaturegear.mjs` holds it. A creature
+built later by the mod gets the same treatment only if it exists when section 51
+runs.
+
+**Any script that samples `dmg_calc` must park `giveSkExp` first** (`quiet()` in
+the test scripts, `MOD_probe` in the mod). `dmg_calc` grants skill exp, so
+sampling it levels the player mid-measurement.
+
+**`tests/baselines/balance.json` is committed on purpose.** `tests/baseline.mjs`
+records the curve, the dials, realm and tribulation costs, prices and every
+area's per-spawn fight targets, and fails on any difference. When a change is
+MEANT to move balance, run `UPDATE=1 PORT=8080 node tests/baseline.mjs` and
+commit the file with the change — the diff is the review.
+
+Run `./tests/run.sh earlybal combat fightsmoke` after any balance change,
+`allareas` (all 3,720 matchups: every area x every creature x every tier x
+four skill builds, the fourth with every section folded) before calling one finished, and `capreach` after touching the curve or any xp rate.
+What matters:
+
+- **`kill < die` at every matchup** — every balance script fails on this.
+- **`whiff%` at 0** — anything above means damage is being eaten.
+- `epow` on `MOD_TIERS` no longer feeds difficulty (the player anchor replaced
+ it) but `modCaps()` and the tests still read it. Tests select a tier by
+ **index** (`setTier(9)`), not by cap number.
+
+### Uncertainty: `tests/lib/stats.mjs`
+
+Every balance number here is a sample mean of something heavy-tailed, so a
+point estimate cannot say whether a gap is real. `summarize()` gives a 95% CI,
+`withinBand(samples, target, tolPct)` asks the question that usually matters —
+is it *practically* on target — and `fightsmoke` and `targets.mjs` both use it.
+
+- **Student's t, not z.** `fightsmoke` fights nine times per area; z at n=9 is
+ ~15% too narrow. Between table rows `tCrit95` takes the lower-df (wider)
+ value, never the narrower.
+- **Compare against the per-spawn target, never the dial.** `MOD_ENEMY.kill` is
+ the centre of a distribution: `killT = kill * band^hpSpread * creatureShape()`,
+ clamped. A basement rat is legitimately a five-swing kill. Use `_modKillT` /
+ `_modDieT`, which `MOD_scaleEnemy` leaves on the spawn.
+- **Fold in the hit rate.** The targets count swings *including misses*, while
+ `abl.default.f` returns damage per landed hit and never rolls to-hit. Miss
+ this and every ratio comes back multiplied by `MOD_ENEMY.hit` — a die ratio of
+ exactly 0.45 is the tell.
+- **Equivalence, not significance.** At n=45 the model tracks its targets to
+ ±0.1%, so "does the CI contain the target" fails on a 1% quantisation bias
+ that means nothing in play. `targets.mjs` uses a ±5% band.
+- **An empty stratum is topped up, never dropped.** Every stratified estimator
+ (mod.js `MOD_meanDamage` and the five test copies) used to fall back to a plain
+ mean when n swings rolled no crit. That does not average the crits badly — it
+ DROPS them, and at cap 30 they are 30% of all damage. In the mod it sized ~5%
+ of spawns ~20% weak (Tallyman p5 kill/target 0.795 → 0.968 after); in
+ `allareas` it read a fight ~40% long 0.15% of the time, which across 2,790
+ matchups intermittently failed the rank duels, whose margin was only 1.25
+ (now `MOD_RANK_MARGIN` 1.4, since v4.3).
+ They now keep sampling (up to 8n) until both strata show.
+- And the crit stratification the section above demands applies here too: a
+ plain sample mean put two of forty-five rank-duel spawns the wrong side of
+ `kill < die` on noise alone.
+
+### The body maximums: write the multiplier, not the total — FIXED
+
+`hpTrack` (sections 9/10) used to **assign** `you.hpmax` at the end of the
+`allbuff` wrapper. The assignment did not survive: `stat_r` recomputes hpmax as
+`(hp_r + hpa) * hpm * hpe`, and the tick, `fght`, `update_d` and `allbuff`
+itself all call it constantly. Measured on a cap-60 character, one `stat_r()`
+took hpmax from **1,492,947 to 72,377** — a 20x collapse — and sampling every
+137ms across six seconds of real play found the low number in all 43 samples.
+The HP bar agreed: `hp: 2,438/10,050`.
+
+Every balance script reads `you.hpmax` in the instant after `allbuff` while the
+assignment still stands, and so does `MOD_scaleEnemy` when it sizes `_modDmg`.
+So the enemy's damage was built for a character with ~18x the health the
+character actually had, and all 2,790 `allareas` matchups were validating
+against a number the player never held.
+
+It now writes `you.hpm`, which IS an input to `stat_r` and therefore survives.
+Three things that took a correction each:
+
+- **`hpm` is saved** (`hpm:you.hpm` in the save object), so the mod's factor is
+ tracked in `global.flags.mod_hpm` and divided back out before re-applying —
+ the same delta pairing `MOD_applyMoneyDrops` uses, and for the same reason
+ `you.res` once compounded on every load.
+- **The recovered base is floored at 1.** Anything that resets `hpm` without
+ clearing the flag — a test harness rebuilding the player, a new game keeping
+ flags — otherwise recovers `1/18.86` and max HP collapses. Flooring is an
+ invariant, not a guess: only milestones touch `hpm`, and they add. Without
+ it `allareas` reported the player at **12 max HP** and six rank duels flipped
+ to `kill > die`.
+- **`Math.ceil`, not `Math.round`** — `stat_r` ceils, and the two have to agree
+ to the unit or this call and the next report max HP one apart.
+
+`stat_r()` is deliberately **not** called from inside the wrapper: the mod's
+skill `use()` calls add straight into `you.str` and friends, and `stat_r` would
+recompute those from `(r + a) * m * e` and wipe them. Setting `hpmax` by hand
+alongside `hpm` keeps the current call right and lets the next `stat_r` reach
+the same number on its own. `tests/targets.mjs` checks all of it, and fails
+against the old behaviour.
+
+**The same bug was still in the realm code**, found while extending it (v4.0).
+Section 28 raised max HP *and max energy* by assigning `you.hpmax`/`you.satmax`,
+and `stat_r` recomputes **both**:
+
+```js
+hpmax = ceil((hp_r + hpa ) * hpm * hpe )
+satmax = ceil((sat_r + sata) * satm * sate)
+```
+
+Measured on a realm-10 character: `hpmax` 332 after `allbuff` and **39** after
+one `stat_r`; `satmax` 1700, then **200**. The ×8.5 the top realm advertised
+lasted one call, so ten realms of body were decoration.
+
+**Nothing writes `hpm` or `satm` any more.** Two contributors wanted the same two
+fields, and two delta-trackers on one field cannot coexist — each recovers its
+base by dividing out *its own* factor and finds the other's sitting inside that
+base. So contributors publish a factor to **`MOD_BODY`** and `MOD_applyBody()`
+writes the product, once, with one delta flag per axis (`mod_hpm`, `mod_satm`).
+
+- To add a contributor: give it a key on `MOD_BODY.hp` / `MOD_BODY.sat`, set that
+ key **every call** for `you` — to 1 when it does not apply, or last call's
+ factor lingers — and never touch `hpm`, `satm`, `hpmax` or `satmax` yourself.
+- `MOD_applyBody()` is **idempotent**, which is what lets more than one wrapper
+ call it. Sections 28 and 38 both do. `tests/cultivation.mjs` asserts it.
+- `satm` is saved (`satm:you.satm`) exactly like `hpm`, so it needs the delta
+ pairing for the same reason `you.res` did.
+- The recovered base is floored at 1 on both axes, for the reason above.
+
+Sections 6, 7 and 9 once each had an `allbuff` wrapper of their own (the section
+9 one assigned `you.hpmax` directly), but section 10 re-anchors on
+`MOD_allbuff_original`, so none of them ever ran after load. They were removed in
+v4.3. `tests/polish.mjs` asserts the live chain: the game's own `allbuff` runs
+once per call, and a mod skill's `use()` once.
+
+## How big numbers are written (section 40)
+
+The mod's numbers are enormous next to the original's, which READS like a balance
+problem but is not one — section 8 scales enemies to the player, so a fight is
+the same length at STR 50 or 5,000,000. A `Number format` settings row (and
+`setNumberFormat()`) picks `game` / `short` / `sci` / `myriad`, default `short`.
+
+- **Up to 9,999 every mode prints exactly what the original prints** — commas
+ where he used `format3`, bare where he used `Math.round`. The threshold is his:
+ `printDamageNumber` does `if(ddmg>9999) formatw(ddmg);` and **discards the
+ result**, so his damage log never compacted anything. The wrapper hands it an
+ already-formatted string, which makes his `>9999` test false (NaN).
+- `myriad` (万 10⁴, 亿 10⁸ …) is his notation: `m_update` draws the wallet with a
+ 10⁸ coin, `㊧`, over 10⁴ groups.
+- `game` mode is the original byte for byte, bugs included — past 10²¹ his
+ `format3` inserts commas into exponential notation (`7,.7e,+47`).
+- Every site is wrapped, not replaced: `formatw`, `update_db`, `update_m`, the
+ three bars, `dom.d6.update` (after section 34's gold held-rank paint, which it
+ leaves alone), `m_update`, `printDamageNumber`. Choose decimals AFTER rounding
+ (`MOD_sigFig`) or 99,996 prints as "100.0K".
+
+## The screen: the rank line, the skill panel, and What next (sections 42-43)
+
+- **The player panel is a fixed 310px box and it is full** (307px used). Never
+ give it a new line. The realm lives at the end of the rank line (`dom.d6`),
+ nowrap + ellipsis, as ONE persistent span re-appended after each
+ `dom.d6.update` rewrite (so its tooltip listeners survive). At a bottleneck the
+ span is the breakthrough button and calls `MOD_breakthrough` unchanged.
+- **A tooltip whose text is a function needs `true` as addDesc's sixth
+ argument** — `addDesc(el, null, 2, title, fn, true)`. Without it the game
+ prints the function's source: the realm on the rank line showed a page of code
+ until v4.7. `tests/tooltips.mjs` renders all ~1,370 tooltips and fails on code
+ or `undefined`/`NaN`.
+- **ASCII only on that line.** The game's font is MS Gothic; where it is missing,
+ a symbol like ▲ or ⚠ falls back to a font with a taller line box — measured,
+ ▲ grew the line 1px and ⚠ 6px, pushing the panel past 310px. `½` is safe.
+ `tests/polish.mjs` asserts the longest possible line keeps panel height.
+- **The skill panel's per-second updater writes only what changed** (v4.6),
+ through `MOD_setIfChanged`, and skips hidden rows. An innerHTML write
+ re-parses and dirties layout even with identical text; the game's version
+ wrote three cells on every row every second (329 DOM changes a pass at 110
+ rows, 3.8ms on WebKit; now 2 changes and 0.3ms).
+- **The skill panel's per-second updater is the mod's now.** The game's captures
+ `sklsize` once and never updates it, so after the list first grows it rebuilt
+ every row every second forever (265 redraws in 5s at 53 rows), and it never
+ noticed a shrink. `MOD_skillUpdater` replaces it in the same `timers.sklupdate`
+ slot the game clears on close, keeps the row update line for line, and redraws
+ on any change of length.
+- Rarity filter and sort go through section 13's hide branch and
+ `MOD_skillCmp`, like everything else in that panel.
+- **`dom.ch_1` is not the first line of a screen.** `chs()` reassigns it on every
+ call, so it holds the LAST line drawn. To test which screen is up, read
+ `dom.ctr_2.children[0]`.
+- **What next** (`MOD_whatNext`, wiki page `next`, console `modNext()`) calls the
+ same gate each system uses — `MOD_tierFlag`, `MOD_atBottleneck`,
+ `MOD_hollowCleared`, the crafting tiers' `gate()` — and never restates a
+ threshold. A new progression track should add a line there.
+
+## Content gating
+
+- The three added areas (Sunken Hollow / Ashen Spire / Long Vigil) are hidden
+ until `global.flags.trne4e1` — golem arena IV cleared, the base game's last
+ normal area — and then open in order on the same kill counts that drive their
+ caps. `tests/areagate.mjs` guards this.
+- **The catacombs are reachable now** (section 30). All 26 rooms are the
+ author's; the mod adds only the Village Center entrance, which is where their
+ own exit already led. Gated on `mod_t_deep` **for a reason**: entering sets
+ `mod_t_cata`, the cap-40 rung, so an ungated door would hand a fresh character
+ cap 40 straight out of the tutorial, past the forest's 20 and the deep
+ forest's 30. Any new area that sets a tier flag needs the same care.
+- **The Pill Tower** is `chss.mod_pltwr`, deliberately not `chss.pltwr1` — the
+ author wrote that choice on the Village Center and commented it out, and the
+ two must not collide if they ever finish theirs.
+- `chs(txt, true, ...)` calls `clr_chs()` and wipes everything already drawn for
+ that location. Only the *first* line of an `sl()` may pass `true`; every later
+ line, including greyed-out hints, must pass `false`.
+
+## Cultivation
+
+Realms are **derived from `MOD_RANK_AT`** — the same thresholds title ranks use
+— so realm N is the level that earns a rank N title. Never restate the levels;
+retuning the ranks must move the realms with them. `tests/cultivation.mjs`
+asserts the alignment.
+
+**Every breakthrough pill must have a source.** The ladder shipped once with
+none of them obtainable — the items existed and nothing gave, dropped or sold
+them, so realms were console-only. Realm 1 comes from the instructor at the Qi
+unlock, 2-5 from `vendor.pha1` (the Herbalist), 6-10 from the dojo's
+continuation rungs. `tests/cultivation.mjs` fails if any realm's pill is
+orphaned. Realm 1 must not come from the marketplace: that is gated behind the
+Paper Boy at 40% a visit, and realm 1 opens long before it.
+
+Realms live in `global.flags.mod_realm` and the bonus is applied **in
+`allbuff`** — never written into `you.stra`/`strm`, or it would compound on
+every load. Reaching a Qi Circulation level does not advance the realm; it opens
+a bottleneck that costs a breakthrough pill and can fail.
+
+**The wall is sized to the climb (v4.6).** `MOD_realmClimbSec(n)` is the
+Circulate Qi time from realm n−1's threshold to n's on the live curve.
+`MOD_insightNeed(n)` is `wallShare` (5%) of that at the meditation rate —
+never below the old 1, 3, 5 … floor or the wall below it. `MOD_secludeFull(n)` is
+the seclusion that earns that insight on average (floor 900s), and
+`MOD_devMinutes(n)` equals it (floor 720). Never restate these as numbers: they
+move with the curve and the ladder. Meditation grants insight **only at a wall**
+— on the way up the same Circulate Qi that climbed the level used to pay for
+every wall long before it. `tests/road.mjs` prints the table.
+
+**The Qi Settling Pill costs the breakthrough pill of the realm at the wall**
+(`MOD_settlePrice`, section 48, never under 5,200), through a getter on its
+Herbalist listing plus a per-tick reprice of rows already stocked — vendor stock
+keeps its own copy of the price from restock time.
+
+**Every price the ladder requires is held by `tests/econ.mjs`** to 10% of the
+climb it gates, in hours of the best income open at that tier (fighting each
+area through `dmg_calc`, gathering each node), on the Original preset. The Pill
+Tower's realm 8-10 pills were cut to pass it when the coin drop went to 0.
+
+Elemental techniques are a **proc inside a wrapped `you.battle_ai`**, because
+combat is automatic and there is no ability picker. They use `stt: 2` so they
+route through the INT branch of `dmg_calc`. The wrapper must keep swallowing
+errors and falling through to the ordinary attack — a bug in a proc must never
+cost a swing.
+
+**A skill created after section 10/11 is invisible to their maps.** Register it:
+`MOD_KEY_BY_ID[sk.id] = key` (cap system) and
+`MOD_PARENT_OF[key] = MOD_PARENT_KEY[MOD_sectionOf(sk)]` (panel grouping).
+Nothing errors if you forget — the skill just silently loses its cap and its
+section.
+
+## The dojo
+
+`chss.t3.sl` is a long `else` chain with several one-time screens inside it —
+the first skillbook choice, the accessory gift, the named-manual choice — plus
+a **"Level Advancement"** ladder drawn by an anonymous handler nested inside it.
+Read all of it before adding anything: a first attempt at extending the dojo
+missed both Level Advancement and the second grade of skillbook that already
+existed.
+
+Section 27 continues that ladder from 35 to 110 in fives, as its own lobby entry
+gated on `dj1rw6` (the nested handler cannot be appended to). Spirit pills are
+sized against `4*lvl^3 + lvl^2`, the character exp curve — a level at 110 costs
+5.3M, so the base game's best pill is worth 0.3% of one.
+
+## Titles
+
+Rank (`rar`) runs **1-10 and is derived from the level that earns the title**
+(`MOD_RANK_AT`), never hand-assigned — the base game gave rank 3 at skill level
+8. Every skill grants five titles, at levels 25/50/75/90/110, folded into the
+milestone already at that level (a second milestone at the same level is a
+duplicate the save cannot tell apart by index).
+
+- A title's exp bonus writes to `skl..p`, which **is saved and is restored
+ after milestones fire**. Never set it from milestone code; it is reconciled on
+ the tick against `global.flags.mod_ttlxp`, like section 13 does for `you.res`.
+- Colours come from `MOD_RANK_COLOUR` (ten ranks). The game's own rank 7
+ tooltip branch **throws** — it sets `this.dl`, undefined in a type 5 call —
+ so `dscr` is wrapped to park the rank at 1 (a branch-less value) and paint
+ the label afterwards. Do not remove that wrapper.
+- Only the **worn** title applies, until renown level N makes rank ≤ N passive.
+ Base-game `talent`s are a separate, already-permanent mechanism — don't
+ conflate them.
+
+### How a title finds its group
+
+`MOD_TITLE_SKILL` attributes a title to a skill in four passes, each derived
+from something already stated — never a per-title table:
+
+1. `MOD_SKILL_TITLES` — the generated five per skill know their own skill.
+2. `MOD_FLAGSHIP` — section 17 already pairs the skill with the title it
+ grants. Read the pairing; the milestone closure captures the row in a
+ variable, so pass 3's source scan cannot see a literal to match.
+3. Scanning each milestone's `f` source for a `giveTitle(ttl.x)` /
+ `MOD_title('x')` literal.
+4. **The author's numbered tiers**, keyed `` — `tghs1/2/3`,
+ `srd3/srd4`, `dth4`, `rtr1`, `axc3`. Most are granted by nothing at all, so
+ pass 3 cannot see them. **The trailing digit is load-bearing**: without it
+ the stem of `ttl.thr` ("Thrasher", for smashing the dojo's equipment) is
+ `thr`, which is `skl.thr`, Throwing. Only the numbered convention is safe to
+ read this way, and an ambiguous stem prefix is left alone.
+
+What no skill claims is bucketed by the same digit-stripped stem into
+**ladders** — `kill1..5`, `geti1..4`, `ttsttl1..4`, `mod_realm1..10` and so on.
+Membership is derived; `MOD_TITLE_FAMILY` holds only a *label per family*, with
+the lowest-ranked member's name as the fallback. A family of one is not a
+family — it falls back to a plain row. The picker and the wiki's title page
+both read this, so they cannot drift apart.
+
+- The picker **skips** a title with no usable name; the wiki **keeps** it and
+ tags it "unfinished". The author left five. Section 41 finished the four
+ whose intent is in his code — `shpt2`/`shpt3` at the 5,000 / 10,000
+ purchases his commented-out checks name, `mone2`/`mone3` at 10 / 100 gold
+ (his checks were placeholders at `GOLD`, his other ladders step 5-10x). The
+ new checks are pushed onto his own `global.shptchk` / `global.monchk`, so
+ they run at his call sites. **`ttl.ddcd` is left alone on purpose**: name
+ and desc are `"null"` and nothing says what it was for. It is NOT the
+ missing first rung of the stay-home ladder — that exists as `ttl.neet`.
+- `tests/wiki.mjs` caps the tallest page at 6,000px, which is why the ladders
+ nest under one "Ladders of their own" group rather than sitting beside the
+ skills — eleven more top-level summaries cost ~530px on the page that is
+ already the tallest.
+
+## Perks and the changelog
+
+Every skill has perks at `10/25/50/60/75/90/110` (section 22). New ones are
+**appended above the skill's current highest level** — never inserted — because
+"granted" flags are stored by array index. `tests/perkcoverage.mjs` fails if any
+skill tops out below the highest story cap.
+
+- A perk must never touch `skl..p`. Skill xp multipliers are restored from
+ the save *after* milestones fire, so the bonus is overwritten on the load that
+ first grants it and `g` is true forever after. Stats restore *before*
+ milestones, so those are safe.
+- Absorption perks use `you.caff`, not `you.res` — section 13 already drives
+ `you.res` continuously for those skills and reconciles it through
+ `global.flags.mod_aff`.
+
+**`changelog/mod-changelog.html` gets an entry for EVERY change**, including
+ones that are not gameplay — a README, a build script, repo housekeeping. Mark
+those as such rather than leaving them out; the rule is literal on purpose, so
+there is never a judgement call about what counts. Newest first.
+
+That file is the mod's own. **`changelog/changelog.html` is the AUTHOR'S and must
+stay byte-identical to `upstream/main`** — the mod's entries used to be prepended
+to it, and were moved out precisely so the single script tag in `index.html` is
+the only change the mod makes to anything of his. Verify before committing:
+
+```
+git diff upstream/main -- changelog/changelog.html # must be empty
+./tests/authorfiles.sh # both rules, and CI runs it
+```
+
+**Remotes:** `origin` is the fork (`Xeus24/23html.github.io`) and is where pushes
+go; `upstream` is the author's game (`23html/23html.github.io`), which this
+account cannot push to. They were renamed so the desktop app's cloud move had a
+writable `origin` — compare the author's files against `upstream`, never
+`origin`.
+
+In game the `changelog` button opens the mod's file; the version number opens
+his.
+
+## The rank ladder
+
+`you.rank()` is the base game's **Power rank** — the number under the portrait,
+lowest is strongest. It is **derived from live stats on every read**, not
+stored, so there is no field to award. Rank 1 is reachable on stats alone, but
+only with **every** skill at 110: measured, the whole of ranks 1-10 lives inside
+the last fifteen skill levels (at 108 across the board you are still rank 3),
+after a game in which the number is an unreadable ten-digit blob.
+
+Section 34 adds a second way to hold one. Ten challengers in the Hall of the
+First Gate, off the Old Path trailhead.
+
+- **The rank is a flag, and `you.rank` is wrapped to floor at it.** Never write
+ it into a stat — `you.res` and `skl.p` are the cautionary tales. It floors,
+ never caps: hold rank 6 with rank-900 stats and it shows 6; pass it and it
+ shows the better number. `you = new You()` runs once at game init and `load()`
+ restores fields into that instance rather than replacing it, which is the only
+ reason the wrapper survives a load. `tests/ranks.mjs` asserts both directions.
+- **Difficulty steps into fight length, and the margin is re-derived.** Each
+ duel is a `protected` one-creature `size 1` area, which is exactly what
+ `MOD_scaleEnemy` reads as an arena boss, so it is already anchored to the
+ player. `MOD_scaleRankDuel` then reads back the `_modKillT`/`_modDieT` it
+ stored, multiplies the length, and **re-asserts `kill < die` itself**. Do not
+ "just make it harder" by scaling both targets in opposite directions: margin
+ 1.5 over step^1.5 is already under 1.2 by a step of 1.16, so the guarantee is
+ gone by rung three.
+- **Challenger `rnk` stays ≤ 14.** That is the monster danger grade, not the
+ ladder rank — the coin drop is `rand(lvl, lvl/4) ** (1 + rnk/5 << 0)`, so
+ rnk 15 pushes the exponent from 3 to 4 and multiplies an endgame drop by a
+ hundred. The base game's toughest creature sits at rnk 10, in the same bucket.
+- Areas 981-990, creatures 990-999, the hall at `chss.mod_hall` 979. Adding a
+ rung means adding a row to `MOD_RANK_LADDER`; the hall, the wiki page and
+ `modRankLadder()` are all drawn from it.
+
+## Crafting
+
+The base game's crafting stops at two stars in practice. Measured across all 62
+recipes, by output rarity and by whether any `giveRcp` call can hand the recipe
+over: 35/43 reachable at 1★, 7/17 at 2★, **0/1 at 3★** (the only one is
+`rcp.trr`, Trinity), 1/1 at 4★ (the Clover Pin), nothing at 5★. **Nineteen
+recipes have no `giveRcp` anywhere** — defined, priced, complete, unteachable.
+Section 50 teaches the fifteen dishes by Cooking level (`MOD_DISHES`, simplest
+first by the cooking exp each grants); `test`, Trinity, Star Shell and Grand
+Gelatin stay unteachable, reported in the wiki: they are not dishes, and which
+the author gated is his call.
+
+Section 35 adds four rungs, 2★ to 5★, each a full set (weapon, body armour,
+shield, accessory, tonic) from twelve gathered materials along three lines, at
+four nodes behind one door on the Village Center. Each rung consumes the rung
+below it.
+
+- **Ids are blocked by namespace and `load()` depends on it.** A saved item id
+ resolves through `itemgroup[(id+1)/10000<<0]`, `itemgroup` being
+ `[item, wpn, eqp, sld, acc]`. An id in the wrong block restores as the wrong
+ object or not at all. Items 9200+, weapons 10101+, armour 20101+, shields
+ 30101+, accessories 40101+.
+- **`rar` must not exceed 6.** `equip()` does
+ `w.wc = global.text.wecs[w.rar][0]` and `wecs` has seven entries, so a 7★
+ anything throws the moment it is worn.
+- **Armour `str` is nearly decorative.** `stat_r` adds only `eqp[0].str` (the
+ weapon) into `str_d`; `int`/`agl`/`spd` are summed over every slot. Armour
+ `str` shows in the DEF tooltips and nowhere in `dmg_calc`.
+- **Re-index the value model after adding recipes.** Section 15 builds
+ `MOD_VAL.madeBy` in an IIFE at load. Without the re-index at the end of
+ section 35, new gear falls past rule 2 (price from inputs) to rule 3 (price
+ from stats) — a 5★ sword came out at 1,161 while each of the eight ingots it
+ eats came out at 13,000. Clear `MOD_VAL.cache` too.
+- **Materials are anchored, not derived.** Rule 4 would price them
+ `stypeBase[5] * rarMult[5]` = 3,250 each, because `rarMult` is calibrated
+ against equipment rarity. `MOD_CRAFT.matValue` sets 6/18/54/162, sized so
+ gathering at the cap pays about six coin a second after the 25% sell rate
+ against roughly one a second from endgame combat.
+- Nodes set **no tier flag** — they are gathering locations, not fight areas.
+ See the catacombs note above for why that matters.
+
+## Ids are the save's primary key
+
+**No two things in a namespace may share an id, and nothing enforced this until
+`tests/ids.mjs`.** The game matches ids in loops with no `break`:
+
+```js
+for (a in a6) for (b in skl) if (a6[a].id === skl[b].id) you.skls.push(skl[b])
+for (gg in chss) if (chss[gg].id === global.lst_loc) chss[gg].sl()
+```
+
+So a duplicate is not a clash, it is corruption that compounds. Section 29 gave
+Fire Mastery `2010 + 0`; section 11 had already given the Companions parent
+`2000 + 10`. Every save/load pushed both skills for both entries and the sheet
+**doubled each cycle** — 1, 2, 4, 8, 16 — until a played-in character had a
+hundred of each. `MOD_KEY_BY_ID` is id-keyed too, so the clash also silently
+took the Companions parent's level cap.
+
+- **An `item` id is a CLASS as well as a namespace.** `dscr` type 1 reads the
+ category straight off the number, and there is no field for it —
+ `< 3000` Food (plus a "Tried:" footer), `3000-4999` Medicine/Tool,
+ `5000-8999` Material/Misc, `>= 9000` Book (plus a "Read:" footer). `stype` is
+ **not** it: the author's stype 4 spans Food, Medicine and Book. Everything the
+ mod added started at 9100+, so twenty-six items — ore, hides, essences, every
+ pill — were labelled "Book" with a "Read: Never" line under them. Pick from
+ `MOD_ITEM_IDS`; `tests/ids.mjs` checks the class against the mod's own tables.
+- **Moving an item id costs the player what they were holding.** The inventory
+ saves `{id, am, data}` and restores by matching id, so an entry whose id no
+ longer exists is dropped without a word. Section 37 reads the blob *before*
+ the game's load runs, notes what the moved ids were carrying, and gives it
+ back afterwards. It never rewrites the save — a migration that fails is
+ better than a load that does.
+- Ranges in use: parents `2000+section` (2001-2010), masteries 2011-2016,
+ Renown 2100, converged skills 2201-2210. Mod locations 973-990 (973 the Damp
+ cellar, 974 the Slip Archive, 979 the hall, 981-990 the rank duels and nodes). **Check `tests/ids.mjs` before picking.**
+- Prefer an explicit table to arithmetic when two schemes could ever meet:
+ `MOD_ELEM_IDS` lists the six rather than computing them, and says why fire is
+ out of line.
+- Changing an id loses that skill's saved level, so move the *newest* claimant.
+- `MOD_dedupeSkills` (section 33) prunes `you.skls` on load. It repairs damage
+ already written into saves; it is not a licence to leave a duplicate id.
+- **`global.titles` has the same shape of bug, from the other direction.**
+ `giveTitle` pushes each title onto *both* `global.titles` and
+ `global.titlese`, and `load()` rebuilds `global.titles` from the save by index
+ and then appends the whole of `titlese` on top — so everything earned that
+ session lands in the array twice (241 titles measured as 481 after one load,
+ 241 distinct). It does not compound, but `save()` writes the inflated array
+ back out, and both the game's title screen and the mod's picker iterate it.
+ `MOD_dedupeTitles` repairs it on load, by identity. **Do not "fix" it at the
+ source** — `titlese` is the author's mechanism. Anything the mod adds that
+ reads `global.titles` should still prefer counting from `ttl`, as
+ `MOD_titleCount` does.
+
+## Skill rarity and convergence (section 39)
+
+**Rarity is derived:** `MOD_skillRank(sk)` is `MOD_rankForLevel(sk.lvl)` — the
+rank of the level the player has taken it to, against `MOD_RANK_AT`, painted
+with `MOD_RANK_COLOUR`. Never hand-assign a rarity to a skill.
+
+The colour goes on the row's `children[0]` **style**, never into the name. The
+game's per-second updater rewrites `children[0].innerHTML` every tick, which
+wipes markup but leaves the element's own inline style alone. Names are a
+primary key and must stay plain.
+
+**Folding HIDES a skill; it must never remove it from `you.skls`.** A Jade Slip
+marks the skill in `global.flags.mod_folded` and section 13's `renderSkl`
+wrapper hides the row through the same branch that hides collapsed groups and
+maxed skills. Shortening `you.skls` instead breaks three things at once:
+
+- the game's panel only redraws when the list **grows**
+ (`let sklsize=you.skls.length; ... if(sklsize= 1` destroys
+`amount * randf(rot[2],rot[3]) + 1`. So a perishable stock **converges** rather
+than accumulating:
+
+```
+A = (gain_per_day * days_between_rot - 1) / lossFraction
+```
+
+The mod never touched the drop table. It made a rabbit take ten swings instead
+of one (`MOD_ENEMY.kill` targets eight landed swings; a quest-stage rabbit went
+from 93 HP to 3,574), and ten times less meat an hour against an unchanged rot
+clock dropped the summer steady state to **4 against the 10 required**. Feasible
+in three seasons and impossible in the fourth is a bug, not a difficulty choice.
+
+On Fights: Original (the default since v4.6) his fights are back, so is his 6%,
+and `MOD_MEAT.killSecOriginal` (5s) replaces the 15s — see section 47. On Scaled,
+section 36 raises the drop 6% → 18%, solved backwards from a steady state of 25
+in the worst season and the first hunting area, and applies it to **every**
+creature carrying `rwmt1` found by scanning the drop tables rather than by
+naming the rabbit and the wolf. `MOD_meatSteadyState()` is the shared formula;
+`modMeat()` prints the table. **Any change to kill times has to re-check this** —
+it is the one quest whose difficulty is set by the clock rather than by the
+fight.
+
+## Choice lines have a fixed height
+
+`.chs` is `height:22px` with no overflow rule, so a choice that wraps does not
+grow its row — it spills over the next one and off the bottom of the panel.
+Roughly 55 characters fit. Keep an added line under 50 and put the sentence in
+`addDesc(node, null, 2, title, text)`, which is what the game does. Use
+`MOD_chsAboveBack()` so the line lands above the location's `"<= Return"`; all
+85 of the game's back-choices start with `"<=`.
+
+## Selling and key items
+
+Section 15 added selling, which the base game does not have, so **the mod owns
+the question of what must never be sold.** `MOD_KEY_ITEMS` (section 32) derives
+it: the game is one inline `
+```
+
+Delete that line and the game is stock again. `git checkout index.html` also
+works, and is the only reason it is the sole change to that file.
+
+## Playing it on the author's site, with nothing to download
+
+Install [Tampermonkey](https://www.tampermonkey.net/) or
+[Violentmonkey](https://violentmonkey.github.io/) in your browser — on Safari,
+Tampermonkey or the free [Userscripts](https://github.com/quoid/userscripts)
+extension — then open
+[`userscript/proto23-mod.user.js`](https://raw.githubusercontent.com/Xeus24/23html.github.io/main/userscript/proto23-mod.user.js)
+and accept the install. The next time you open
+[23html.github.io](https://23html.github.io/), the whole mod runs on it, and your
+save there is backed up before the mod first touches it (`modBackups()` in the
+console lists the backups). The manager checks the same address for updates.
+
+Turning the userscript off gives you the author's game back, but a save the mod
+has written to keeps the mod's extra fields. Download a backup from the saves
+panel first if you want to go back and forth.
+
+**On Safari, download a backup now and then.** Safari can clear a site's stored
+data when you have not visited it for a while (seven days of browsing, under its
+tracking prevention), and the saves and the automatic backups both live there.
+A downloaded backup file is the copy that survives it. The mod reminds you, at
+most once a day, until you have downloaded one that week.
+
+## Running it
+
+Open `index.html` in a browser, or serve the folder:
+
+```bash
+npx http-server -p 8080 -c-1 .
+```
+
+`-c-1` disables caching. Without it the browser holds `mod.js` for an hour and
+edits appear not to take, which is confusing enough to be worth the flag.
+
+## Playing it from the fork's own site
+
+The repository root is the game with the mod's one script tag already in it, so
+GitHub Pages can serve it as it stands: in the fork's **Settings > Pages**, set
+the source to **Deploy from a branch**, branch `main`, folder `/ (root)`. It is
+then at `https://xeus24.github.io/23html.github.io/`, with no userscript and no
+download. `.nojekyll` at the root tells Pages to serve the files as they are.
+
+Saves live in the browser per site, so this address is a separate save from
+[23html.github.io](https://23html.github.io/) and from a local copy: move a
+character between them with the saves panel's export and import.
+
+## What it changes
+
+Fifty-one sections, roughly in the order they were built. The design record
+for every one of them, including the reasoning and what was measured, is in
+[MOD-NOTES.md](MOD-NOTES.md). Version 4.7.
+
+**Measured against the original**
+
+The mod is checked against the author's untouched game: a test loads it twice,
+once with `mod.js` blocked, and runs the same measurements on both.
+
+- Skill exp costs **exactly what the original charges below level 10**. That is
+ the range he designed — all 22 of his perk ladders end between level 1 and 15.
+ From level 10 the cost rises about 3% a level, at a rate solved so level 110
+ is reachable in the same time as before. His own curve cannot be kept past
+ that: 109→110 alone costs about 10¹⁴ xp.
+- Skill exp multiplier 1 and enemy coin drops off, the original's defaults —
+ both adjustable in settings.
+- **Fights in the author's areas are his**, creature for creature, so you
+ out-grow them the way you do in his game. A *Fights* setting switches to
+ *Scaled*, where every fight is sized to you.
+- Every price the progression asks for is checked against what you can earn
+ at that point with no coin drop.
+
+**Progression**
+
+- Skill level caps tied to story progress, 10 through 110.
+- Perks at 10/25/50/60/75/90/110 on every skill.
+- The dojo's "Level Advancement" ladder carried from level 30 to 110, which is
+ what the instructor promises and the base game stops delivering.
+
+**Combat and places**
+
+- An enemy model for everything the mod adds, and for the whole game on the
+ *Scaled* setting. It is solved per spawn against your measured power — about
+ 8 swings to kill, 20 to die — rather than fitted to a level. See the note
+ below on why the obvious approach does not work here.
+- Three areas past the base game's last — the Sunken Hollow, the Ashen Spire and
+ the Long Vigil — opening in order once the golem arena is cleared. An entrance
+ to the author's catacombs, the Pill Tower, and the Damp cellar, which he wrote
+ and never connected.
+- **The rank ladder.** The Power rank under your portrait is computed from your
+ stats, and rank 1 was only reachable by taking every single skill to 110. The
+ Hall of the First Gate adds ten named challengers, fought in order; beating
+ one takes that rank, and the rank you hold floors the rank you are shown
+ without ever capping it.
+- **Crafting to five stars.** The base game's crafting stops at two in practice.
+ Four new rungs add a full set each — weapon, armour, shield, accessory, tonic —
+ from twelve gathered materials at four resource nodes that open with the story.
+
+**Cultivation**
+
+- **Ten realms above Mortal**, Qi Refining through Tribulation Transcendence,
+ advanced by breaking through a bottleneck. Each is worth a compounding
+ multiplier — up to ×6 on every stat and ×8.5 on health and energy.
+- **The road between them**, modelled on the genre's own glossary: a Spiritual
+ Root rolled once, nine layers to every realm, insight earned by meditating,
+ by fights you nearly lost and by going somewhere new, Closed Door Training to
+ consolidate and shelter an attempt, Qi Deviation for failing in the open, and
+ a Heavenly Tribulation at the top. Each wall is sized to the climb to it:
+ about 5% of the time it took.
+- Six elemental mastery skills whose techniques fire during combat.
+- Your realm sits on the rank line, and at a bottleneck it is the breakthrough
+ button.
+
+**Skills, titles and items**
+
+- Skills of the mod's own, five earned actions, and effects for the skills and
+ affinities the base game left inert.
+- **Skill rarity** — ten grades, from the level you have taken a skill to —
+ shown in the list and tooltips, with a sort and a filter.
+- **Folding.** A Jade Slip folds a skill into its section; fold a whole section
+ and it is one line. Folded skills keep every level, perk and buff.
+- Title ranks 1–10, derived from the level that earns the title rather than
+ assigned by feel; five titles per skill; titles that do something, worn or
+ made passive by Renown; and four of the author's unfinished titles finished.
+- Selling, with anything that is the only way to unlock something protected.
+- Three bugs in the author's own code fixed: the Death skill made dying cost
+ *more* energy as it levelled, Shield Mastery could turn your defence into
+ extra damage taken, and luck never reached the crit roll.
+
+**Quality of life**
+
+- Three save slots, with a "start new save" button, and **automatic backups**:
+ before a new version of the mod loads, every slot is copied first, and again
+ once a day. The saves panel restores any of them, or downloads it as a file.
+- A **Pacing** setting — *Original* (the default) or *Mod before 4.2*, the
+ faster pacing and scaled fights the mod had before it was matched to the
+ original, kept as a legacy option.
+- **Settings travel with the save**: a new browser or device takes them from it,
+ and one with settings of its own keeps those.
+- Settings boxes for game speed, skill exp, coin drops and **number format** —
+ short (4.56M), scientific, myriads (5.6亿) or as the original. Up to 9,999
+ every format prints exactly what the original prints.
+- An optional "unrestricted actions" toggle — several actions at once, started
+ anywhere. Off by default; both are deliberate limits.
+- Collapsible skill sections, live effect descriptions, and a title picker
+ grouped by skill.
+- **A wiki**, on the bottom bar and in settings. Fifteen pages covering every
+ skill, area, item, title, action and realm, generated from the live game data
+ each time it opens and reading your save — including a skill handbook and a
+ **What next** page that says what you can do right now.
+
+Every change is logged in `changelog/mod-changelog.html`, reachable in game from
+the `changelog` button in the bottom bar. The version number next to it opens the
+game's own changelog, which the mod does not touch.
+
+## The one thing to know before changing anything
+
+`dmg_calc` is **subtractive in both directions**: an attack does
+`attacker.str − defender.str`. A defender's STR *is* their armour. So scaling an
+enemy's STR raises its offence and its armour together, and once that armour
+passes your STR your damage does not get small — it clamps to zero.
+
+An earlier version of the mod did exactly that, fitted with a grid search scored
+on `enemyHP / playerSTR`, a ratio that ignores the subtraction entirely.
+Measured through the game's real combat math, the tutorial needed 63,000 swings
+and killed you in one, and every tier from the southern forest onward was
+kill-in-1 / die-in-1. Judge a fight by driving `dmg_calc` and `hit_calc`, never
+by a stat ratio.
+
+## Tests
+
+```bash
+npm install && npx playwright install chromium # once
+npm test # all 48 scripts, four at a time
+./tests/run.sh audit combat # a subset
+JOBS=1 npm test # one at a time, output live
+BROWSER=webkit npm test # on Safari's engine (npx playwright install webkit)
+```
+
+They drive a real browser against a served copy of the game, so they test the
+actual thing rather than a model of it. [tests/README.md](tests/README.md)
+explains what each one covers. The broadest is `allareas.mjs`: every area, every
+creature, both ends of every level band, ten story tiers, four skill builds —
+3,720 matchups, all of which must be winnable. `vanilla.mjs` holds the mod to
+the original game wherever it claims to match it, and `baseline.mjs` keeps the
+numbers that define the balance in a committed file, so any change to them shows
+up as a diff. `fights.mjs` holds the author's areas to his page spawn for spawn,
+and `econ.mjs` holds every required price to what that stage pays.
+
+The same suite runs on GitHub for every push and pull request, on Chromium and on
+WebKit (the engine Safari is built on), along with a check that the author's
+files are untouched (`tests/authorfiles.sh`).
+
+**Several tests write to the save.** Export one first, or run them against a
+copy of the folder.
+
+## Layout
+
+| | |
+|---|---|
+| `mod.js` | the entire mod, ~13,800 lines |
+| `userscript/` | the mod as a userscript for the hosted game — generated, `npm run build:userscript` |
+| `tools/` | the userscript build |
+| `MOD-NOTES.md` | the design record, section by section, with the reasoning |
+| `CLAUDE.md` | the constraints that are easy to violate by accident |
+| `tests/` | 48 Playwright scripts, `lib/stats.mjs`, the balance baseline, `authorfiles.sh` |
+| `.github/workflows/` | the suite on every push and pull request |
+| `changelog/mod-changelog.html` | what changed, every time |
+| `index.html` | the author's game, plus one script tag — the only edit to anything of his |
+
+The checkout's `origin` is this fork; the author's repository is `upstream`.
+
+## Credit
+
+The game is truezhangwei's. Everything in `mod.js`, `userscript/`, `tools/`,
+`tests/`, `MOD-NOTES.md` and this file is a modification of it, and none of it
+is endorsed by the author. The userscript runs on the author's site in your own
+browser; it changes nothing on the site itself.
diff --git a/changelog/mod-changelog.html b/changelog/mod-changelog.html
new file mode 100644
index 0000000..2dfeed9
--- /dev/null
+++ b/changelog/mod-changelog.html
@@ -0,0 +1,862 @@
+
+
+
+proto23 mod — changelog
+
+
+
+PROTO23 MOD — CHANGELOG
+Changes made by the local mod.js. None of this is the work of the
+game's author — the game's own changelog is here.
+Remove the <script src="mod.js"> line from index.html and none of it applies.
+
+
+----MOD 4.7 (a pill for the wall):
+[2026/10/07]
+Stuck at a realm 6-10 wall without the pill? The instructor keeps one back. The dojo's pills for those realms are
+ paid at character levels (45, 60, 75, 90, 105), while the walls are Circulate Qi levels, so a wall could come
+ before the level that pays for it. "A pill for the wall" on the dojo screen gives that realm's pill once; the level
+ reward for the same realm then skips its pill
+
+
+----MOD 4.7 (creature gear):
+[2026/10/07]
+Creatures no longer share one weapon. In the game every creature held the same object, and the affinities the author
+ wrote for 16 of them were all written onto it, so each creature carried whichever was written last. Your bare fists
+ were that same object too, so your level raised physical and blunt creatures' damage. Each creature now has its own
+ gear with the values he wrote for it, and your fists are your own
+
+
+----MOD 4.7 (dishes, a screen fix):
+[2026/10/07]
+Fifteen dishes nothing could teach are now taught by Cooking. Bacon and Eggs, Morning Set, Lunch Set, Onion Rings,
+ Cooked Fish, Batter Fried Fish, Fried Nori, Steamed Bun, Hippo Cookie, Steak, Cinnamon Bun, Broth, Egg Soup, Creamy
+ Chowder and Mushroom Soup were in the game and unreachable. You work each out at a Cooking level from 6 to 44, simplest
+ first. Trinity, Star Shell and Grand Gelatin (his late accessories) are still unteachable
+The gate's two locked-door hints no longer wrap over the line below them; the full sentence is in the tooltip
+ Tooling, not gameplay: a check that draws every mod screen with a capped character and a very long name; the
+ step from the author's last area to the Hollow is now held winnable with margin
+
+
+----MOD 4.7 (background tabs):
+[2026/10/07]
+A game left in a background tab catches up. Browsers slow a hidden tab's timers to about once a minute, so the
+ game nearly stopped while you were away. When it next runs, the mod replays the missed time, up to an hour: the
+ clock, restocks and weather, and the seconds of the action you were doing. Fights are never replayed, so being
+ away cannot kill you. setCatchUp(seconds) changes the cap, 0 turns it off; it travels with your save
+
+
+----MOD 4.7 (wiki search):
+[2026/10/07]
+The wiki search covers every page. The page list shows how many entries match on each page, dims the ones with none,
+ and Enter jumps to the first page that has a match
+ Tooling, not gameplay: package-lock.json is now committed (it was ignored, so every GitHub run failed at install)
+
+
+----MOD 4.7 (pills and hosting):
+[2026/09/30]
+The Pill Tower stocks the top realm pills sooner. A day is 24 real minutes and the Tower restocks every three, so
+ realm 10's pill at 6% took about twenty hours on average. Realms 8, 9 and 10 now stock at 26%, 20% and 15%: about
+ 4.6, 6 and 8 hours, rising with the realm
+ Tooling, not gameplay: instructions in the README for playing from the fork's own GitHub Pages site, and a
+ .nojekyll file so Pages serves the files as they are; a check that every Tower pill is in stock within 8 hours
+
+
+----MOD 4.7 (small):
+[2026/09/30]
+Skill rarity has a grade marker. A small number after a skill's name (2 to 10) gives its rarity rank, so it reads
+ without colour. Rank 1 shows nothing
+ Tooling, not gameplay: GitHub caches the downloaded test browsers between runs; a check for the marker
+
+
+----MOD 4.7 (housekeeping):
+[2026/09/29]
+Switching save slots cannot pull in another slot's settings. A browser now takes settings from a save once, on
+ its first load; a later load (a slot switch) leaves the browser's own settings alone
+ Tooling, not gameplay: a check that the docs' stated numbers (sections, scripts, version, suite time) match the
+ source; the tests now run on Firefox in GitHub too; the docs' script count is 42
+ (the suite takes about 75 seconds)
+
+
+----MOD 4.7:
+[2026/09/28]
+The Qi Settling Pill costs what failing cost. Qi Deviation now lasts as long as the seclusion you skipped —
+ up to 91 in-game days at the top — and a flat 5,200 made it ten minutes of late-game coin. It now costs the
+ breakthrough pill of the realm you failed at, never less than before
+The rank line shows the wall: at a bottleneck it reads, for example, "½ step to Nascent Soul >> 40/104" — the
+ insight you have against what it asks for
+Your settings travel with your save. A browser with no settings of its own takes pacing, fights, number format,
+ speed and the rest from the save, so moving to a new browser or device brings them. One that has its own keeps them
+After an update the log says so, once, with the changelog a click away
+On Safari, a reminder to download a backup, at most once a day and not after you have downloaded one. Safari can
+ clear a site's saved data after a week unvisited, backups included
+The userscript says so in the game when the author's game is a version the mod was not tested on
+Near-death insight, explained: the wiki says where fights you nearly lost actually happen. On Original fights his
+ areas are no threat once you reach them; the three past the golem arena and the rank duels bring you there within a
+ few fights. No wall needs it — each can be met by sitting alone
+Small visuals: the mod's number boxes lose the spinner arrows that sat on their digits in Safari, and the wiki uses a
+ proper monospace font where the game's is missing, so tables wrap the same everywhere. The old pacing preset is
+ labelled legacy: it is kept exactly as it was, and its prices come cheap
+ Tooling: every tooltip in the game (1,367 of them) is rendered and read in a new check, so a description
+ that shows code fails the suite; the spirit pills and the Root Cleansing Pill are priced against income too; and the
+ tests wait for the game to tick instead of a flat 4.5 seconds — the whole suite is about 75 seconds. 40 checks.
+
+----MOD 4.6:
+[2026/09/28]
+Fights as the author wrote them. A new Fights setting, and Original is the default: every area the author
+ made has his creatures exactly as he made them — his level ranges, his HP, his exp — so you out-grow
+ them the way you do in his game. The forest goes back to a swing or two a kill
+ The mod's own areas (the three past the golem arena, and the rank duels) are still sized to you, since
+ there is no original of them. Scaled is the old behaviour everywhere, and the Mod before 4.2 preset picks it
+ Raw Meat is back to the author's 6% on Original: the raise to 18% only made up for the mod's longer fights
+ Checked against the author's own page: all 70 spawns in his areas come out stat for stat the same, and
+ every step of his route past the tutorial is winnable at the point you first reach it.
+Breakthrough walls sized to the climb. Realms 3 to 10 take days to months of Circulate Qi on the current skill
+ curve, but the insight they asked for (5 to 19) and the fifteen minutes of seclusion were sized for when
+ they took minutes, and meditating on the way up paid for every wall long before you reached it
+ Now a wall asks for about 5% of the climb to it, and meditating only earns insight while you are at one.
+ Realm 3's wall is 60 insight, about 1.4 hours in seclusion; realm 10's is 1,580, about 37 hours
+ Full consolidation takes as long as seclusion takes to earn that insight, so sitting it out finishes both,
+ and failing in the open leaves you deviated for as long as the seclusion you skipped. The wiki's cultivation
+ page has the table. Realms 1 and 2 are as they were
+The Pill Tower's top pills cost what you can earn. Realm 8, 9 and 10 were 900K, 2.4M and 7M, priced when
+ enemies dropped coin. With the original's no coin drop, they are 540K, 560K and 6.5M
+ Measured: the best income at every stage, from fighting in each area and gathering at each node, against
+ every price the ladder requires. The rule is that none costs more than 10% of the climb it gates. The
+ Herbalist's pills and the Jade Slips already passed; all hundred slips come to about 4% of what the game
+ pays after the Slip Archive opens.
+Play it on the author's hosted game, with no local copy: userscript/proto23-mod.user.js, for Tampermonkey or
+ Violentmonkey. It runs the whole mod on https://23html.github.io/, and your save there is backed up before the
+ mod first touches it
+ Tooling, not gameplay: the tests run on GitHub for every push and pull request; a check that the author's
+ two files are still his (his changelog byte for byte, index.html his plus the one script tag); and the test
+ runner reports seconds per script. allareas, listed as the slow one, now takes about seven seconds, so it
+ was left as it is. New checks: fights.mjs and userscript.mjs; econ.mjs is now a real check rather than a
+ printout. 38 in all.
+Safari. The skill list no longer wraps each exp bar onto a second line (Safari's scrollbar left the
+ row a few pixels short, which also made the list scroll sideways); the mod's dropdowns in settings match
+ the panel instead of showing as grey system buttons; and clicking a section header no longer selects its text
+The realm tooltip on the rank line showed a page of program code instead of your realm, root, insight and
+ breakthrough odds. It shows those now
+The skill list does far less work while it is open: it only redraws what changed each second, rather than
+ every line — about ten times cheaper, in every browser
+ Tooling: every check can now run on WebKit, the engine Safari is built on, and GitHub runs them on
+ both. The Safari problems above were invisible in Chrome's test browser, which has no scrollbar width.
+ On Safari, download a backup now and then: it can clear a site's saved data after a week unvisited.
+
+----MOD 4.5:
+[2026/09/28]
+Three bugs in the author's own code, fixed.
+ Death made dying worse the more you levelled it. You kept 55% of your energy at level 0, none at
+ level 10, and went into NEGATIVE energy from level 11 — the opposite of what the skill says. It now
+ keeps 55% at level 0, exactly as before, rising to 78% at 10 and 96% at 110
+ Shield Mastery could turn your defence into extra damage. At high levels one bracket in the enemy's
+ damage went below zero and your defence was added to the hit instead of taken off it. That bracket
+ now stops at zero. Nothing changes where it was already fine
+ Luck never touched your crit chance. The author wrote a luck multiplier and then declared it where
+ the crit roll could not see it. It works now, up to a 50% crit chance from luck
+ Why the cap: the mod's perks grant far more luck than the author's game ever did, and through his
+ formula a capped-out character would crit on every swing. Enemies are sized with it included.
+Breakthrough history on the What next page: your last attempts, their odds, and how each ended
+Backups: once a day as well, in their own place so they cannot push out an update's backup; and any
+ backup can be downloaded as a file and loaded back, so a save survives clearing the browser
+Shop prices read: the mod's own stock (pills to seven million) follows your number format. The base
+ game's prices, all under 10,000, are untouched
+ Tooling: tests for the cultivation road (every rule, which until now had only been checked by hand),
+ for the three fixes, and a balance baseline — the numbers that define the balance are kept in
+ tests/baselines/balance.json, so any change to them shows as a diff. 36 checks, all passing.
+
+----MOD 4.4:
+[2026/09/27]
+Your saves are backed up before a new version of the mod loads. Every slot, copied before the game
+ has even read its save. Three are kept. The saves panel lists them, restores one, or backs up on demand
+ It is the very first thing the mod does when it loads, and depends on nothing after it: an
+ update that breaks is exactly when you want the backup, and a broken section stops everything
+ below it while the game goes on loading and autosaving. Restoring puts every slot back, and backs
+ up what you had first, so it can be undone. It restores your saves, not the mod's code.
+A Pacing setting: Original, the default since 4.2, or Mod before 4.2 — skills much faster early,
+ skill exp 2x and a 15% coin drop, exactly as the mod played before. Level 110 is the same distance
+ away in both, and fights are the same. Change one of those boxes yourself and it reads Custom
+ New checks: tests/backups.mjs and tests/pacing.mjs. The backup check caught a real bug before it
+ shipped: when there was no room, it cleared every older backup to make space and then lost them
+ anyway when the copy still did not fit. It now frees only the one it is replacing.
+
+----MOD 4.3:
+[2026/09/27]
+Documentation, not gameplay: the notes and READMEs brought up to date.
+ MOD-NOTES.md opens with an overview of the mod as it stands at 4.3, and gains the missing
+ chapters for 4.0 to 4.3. Older passages that later versions changed now carry a dated note
+ beside them rather than being rewritten, since the reasoning is the point of that file
+ Two corrections along the way: the notes gave the top realm as "Ascendant" (it is Tribulation
+ Transcendence), and the README listed seven realms, Mortal through Ascendant (there are ten above
+ Mortal). Both READMEs now describe 43 sections, 31 test scripts and 3,720 matchups.
+ Repository: the checkout's remotes were renamed — origin is the fork, upstream is the author's
+ game — so the author's-changelog check is now against upstream/main.
+modHelp() lists every console command — fifty-four, grouped by purpose. It listed six, from the first
+ version, and said skill exp resets on reload, which stopped being true when the settings box
+ began persisting it. A check now fails if a new command is left out of it
+ Housekeeping: the code comments in sections 38 and 39, and two lines of the 4.0 entry below,
+ quoted Wuxiaworld's glossary at length. They paraphrase and cite it now. CLAUDE.md brought up to
+ date as well: the remotes, five actions, the dead wrappers now removed, and current counts.
+
+[2026/09/26]
+Your realm is on the main screen, and it is the breakthrough button.
+ The rank line now reads "rank: 135T · Core Formation 5/9". At a bottleneck it says "½ step to
+ Nascent Soul >>" and clicking it breaks through, with the same checks, odds and tribulation as ever.
+ Its tooltip shows your root, your insight, whether you hold the pill, your odds, and whether
+ you are sheltered in seclusion
+ The player panel is a fixed-height box and already full, so this shares the rank line rather
+ than adding one. Plain text only: a ▲ and a ⚠ each made the panel taller, measured.
+A What next page in the wiki, right after Start here. It reads your save and says what is ready
+ now, what you are working on, and what opens the rest: story cap, cultivation, locked actions,
+ the dojo, the late areas, the rank ladder, crafting, convergence, and odd jobs
+Rarity reads as a word, not only a colour. The skill tooltip names it. The skill panel sorts by rarity
+ and can show only one rarity and above
+The Damp cellar is finally somewhere. It is the author's area, but it had no spawn weights (nothing could
+ appear), no way in, and an exit into two screens that were never written. It is Notice #1 on the
+ Message Board now, once the dojo has taught you to fight. The first clear pays
+Four unfinished titles finished: Second-Rate and First-Rate Shopper at 5,000 and 10,000 purchases (the
+ author's own numbers, commented out in his code), and Peasant and Merchant at 10 and 100 gold
+ The fifth, named "null", is left alone: nothing says what it was for.
+Fixed: the skill list rebuilt itself every second, forever, once it had grown with the panel open: every
+ row, every second, until you closed it. It redraws once per change now, and notices a skill leaving
+Rank duels have more headroom (1.25 to 1.4). The top challengers hit about 12% softer
+Every enemy has been checked against a fully merged character too, 3,720 fights in all, and all are winnable
+ Housekeeping: three unused copies of the stat-refresh code removed. Tutorial fight 1 was on the list
+ as a fight you could lose; that is only true of the original's numbers, and here it was already
+ winnable (8 swings to win, 23 to lose), so nothing changed. New check: tests/polish.mjs.
+
+----MOD 4.2:
+[2026/09/26]
+Measured against the original game, and brought back in line with it.
+ A new check (tests/vanilla.mjs) loads the game twice, once with this mod blocked, which is
+ the author's game exactly, and runs the same measurements on both.
+ Skills cost exactly what the original charges, for the first ten levels. The mod's old curve
+ was 12x faster than the author's at level 5 and 364x faster at level 10, which is exactly the
+ range he designed: all 22 of his perk ladders end between level 1 and 15. From level 10 it
+ rises about 3% a level, at a rate worked out so level 110 is exactly as far away as it was
+ Why not match him to 15, his last perk? His level 15 alone costs 823,099 xp, and ninety-five
+ more levels even at that flat price is two and a half times the whole budget.
+ Skill exp is 1x and enemy coin drops are off, the original's values. Both were raised by this
+ mod (2x, 15%). The coin drop is the author's own mechanism, switched on only by the Coin Ring
+ and the Ring of Greed; at 15% under them those two rings were worthless. Both settings
+ boxes still work, and anything you set there yourself still wins
+ Fixed: the six elemental masteries were stuck on the original's curve. The mod installed its
+ curve before they existed, so level 20 cost 7,474,050 xp instead of 375 and their perks to
+ 110 could not be reached
+ Fixed: about one spawn in twenty came out much weaker than intended. Enemy strength is set by
+ sampling 32 of your swings, and when none of them crit the crits were not averaged badly, they
+ were left out entirely, and they are 30% of your damage. Those spawns were ~20% short. Now
+ 98% of spawns land within 4% of their intended fight length
+ The same bug in the balance tests is also why they sometimes failed on a rank duel and
+ passed on a re-run: 0.15% of the time they read a fight 40% longer than it was.
+ Not matched, on purpose: fight length. At equal progression the original's early fights take
+ one or two swings and almost never hurt you, because you out-level its fixed creatures. This
+ mod holds every fight near 9 swings and 22 to die. The test prints that gap
+
+Big numbers are readable: Short (4.56M), Scientific (1.23e7) or Myriads (5.6亿).
+ A Number format row in settings. Stats, health, energy, exp, damage, Power rank and money all use it
+ Up to 9,999 every format prints exactly what the original prints. That threshold is the author's.
+ His damage log checks for numbers over 9,999 and calls his own compactor, then throws the result
+ away, so damage was never compacted. It is now
+ Myriads are his notation too: his wallet already counts in 10^8 coins (㋧) over groups of
+ 10^4, which is 亿 and 万. "As the original" leaves every number exactly as he draws it.
+
+----MOD 4.1:
+[2026/09/26]
+Skill rarity, and convergence — fold a whole section into one skill.
+ Every skill has a rarity now, ten of them from Novice to Transcendent, and the
+ skill list paints each row in its colour
+ It is the rank of the level you have taken the skill to — the same thresholds and
+ the same ten colours titles use. Derived, not assigned: a hand-picked rarity per skill
+ would be a hundred numbers to keep in step with a ladder that moves, and would say
+ nothing about your character.
+ Jade Slips (玉简) fold a skill into its section. Fold every skill in a section and it
+ is one line on your sheet instead of up to twenty-five — Warfare, Constitution,
+ Pathfinding, Livelihood, Workmanship, Fortitude, Attunement, Provision, Maintenance,
+ Fellowship
+ The Slip Archive is on the dojo lobby, beside Level Advancement. It sells the slips;
+ the price rises with every one you have spent, so converging all hundred is a project
+ Folding costs you nothing but the slip. A folded skill keeps its level, keeps earning
+ exp, keeps every perk and keeps applying every buff it ever applied. It is hidden, not
+ removed, which is also why it can be unpicked at any time
+ Hiding rather than removing is the whole design. Shortening the skill list breaks three
+ things at once: the game's panel only redraws when the list GROWS, so removed rows would
+ stay on screen forever; the game pushes a skill back onto the list on its next level-up,
+ banner and all, so a fold would undo itself; and the list is saved by id. The mod's
+ buffs were always applied from the skills themselves, never from the list — so "all
+ of the buffs" is literally true rather than re-implemented. Measured: every stat after
+ folding all hundred is exactly its unfolded value times the convergence bonus.
+ A converged section is worth x1.03 on every stat, compounding — x1.34 with all ten.
+ A partly folded one pays a share in proportion, so it rewards you as you go
+ A Skill handbook in the wiki, beside the skills page: the rarity ladder, which skills
+ fold into which, where you stand, and what the slips cost
+ New: modConverge(), modRarity(), modFold(section), modUnfold(section).
+ tests/convergence.mjs, 43 checks. tests/perkcoverage.mjs no longer grades the ten
+ converged skills — they are never trained, so a perk ladder on them could never be
+ climbed, the same reason Renown was already exempt.
+
+----MOD 4.0:
+[2026/09/25]
+The cultivator's road — the realm ladder finally has a road between its rungs.
+ Sourced, not invented. Wuxiaworld's "General Glossary of Terms in Wuxia, Xianxia &
+ Xuanhuan Novels" is the genre's own reference work and it names every piece below.
+ Its entry on bottlenecks was the design brief: breaking one can take new insight, a
+ medicinal pill, or harsher training.
+ The mod shipped with one of those three. All three are here now.
+ Spiritual Root (灵根) — rolled once, when the dojo finishes teaching you to
+ fight. Five grades by purity, commonest worst. Never a dead end: the worst costs 15%
+ cultivation speed, not access, and a Root Cleansing Pill moves you up a grade
+ Nine layers to every realm (层) — derived from where Qi Circulation sits between
+ this realm's threshold and the next, so breaking through lands you at the first layer
+ of the new one on its own. You are now "Core Formation, 5th layer (middle stage)", or
+ at the Peak of it, or a half step to Nascent Soul — which is the genre's own name
+ for the state this mod has been calling a bottleneck
+ Insight (参悟) — the half of the price that cannot be bought, from the three
+ sources the glossary names and no fourth of our own: meditating, a kill made under
+ 20% health, and the first time you set foot in an area. Spent whether the attempt
+ works or not, because what you spend is understanding
+ Closed Door Training (闭关) — a fifth action, and the only one that costs you
+ something: it holds the action timer, and it refuses to start unless you are actually
+ at a wall. Fifteen minutes in it is worth +30% on the attempt
+ And it shelters you. The glossary gives seclusion two purposes, breaking a
+ bottleneck and not being disturbed at the crucial moment and suffering a backlash
+ — so a breakthrough attempted while you are still sitting in it cannot
+ deviate, only fail. Sit and be safe, or push it in the open and risk it.
+ Qi Deviation (走火入魔) — failing in the open now leaves your cultivation base
+ unstable for half an in-game day: stats at x0.55, body at x0.70, no second attempt
+ until it passes. Clears on its own or at once with a Qi Settling Pill. Never kills
+ Heavenly Tribulation (天劫) — realm 10 was called Tribulation Transcendence and
+ there was no tribulation. From realm 8 up, a successful breakthrough is answered by
+ lightning: 3, 5 then 7 bolts costing 55/70/85% of your max HP. A gate on walking in
+ healthy, anchored to the player like every other number here. Fall and the realm
+ does not open
+ Impurities (杂质) — something black and foul-smelling leaves you on the way up
+ New: modRoad() and modRoots() in the console; two pills at the Herbalist; the wiki's
+ Cultivation page carries all of it, generated from the live tables as ever.
+
+Fixed: a realm's body multiplier did nothing at all.
+ Found while building the above. Section 28 raised max HP and max energy by ASSIGNING
+ you.hpmax and you.satmax — the exact mistake v3.9 fixed for hpTrack, still sitting
+ in the realm code. stat_r recomputes BOTH from their inputs, so both were undone on
+ the next call. Measured on a realm-10 character: hpmax 332 after allbuff and 39 after
+ one stat_r; satmax 1700, then 200. The x8.5 the top realm advertised lasted one call.
+ Ten realms of health and energy you were never given, you now have. Both go
+ through hpm and satm, which are inputs to stat_r and therefore survive
+ Two contributors wanted the same two fields, and two delta-trackers on one field
+ cannot work — each recovers its base by dividing out its own factor and finds the
+ other's sitting in it. So nothing writes hpm/satm any more: contributors publish a
+ factor to MOD_BODY and one idempotent applier writes the product, one delta flag per
+ axis. Verified: survives stat_r, zero drift over 200 calls, Mortal untouched.
+
+----MOD 3.9:
+[2026/09/23]
+Fixed: you never actually had the max HP the game was giving you.
+ The mod raises max HP with your offence (hpTrack). It did that by ASSIGNING you.hpmax —
+ and stat_r recomputes hpmax from (hp_r + hpa) * hpm * hpe on the very next call, which
+ the tick, combat, and allbuff itself all make constantly. Measured on a cap-60
+ character: one stat_r() took max HP from 1,492,947 to 72,377. Sampled every 137ms
+ across six seconds of play, the low number appeared in all 43 samples.
+ Enemy damage was sized against the number you never held. MOD_scaleEnemy reads
+ you.hpmax in the instant after allbuff, while the assignment still stands, so every
+ fight was built for a character with ~18x the health you actually had. Harmless early,
+ worst at the cap
+ It writes the multiplier now, not the total — hpm is an input to stat_r, so it
+ survives. Max HP holds through 200 consecutive stat_r calls
+ hpm is SAVED, so the mod's factor is tracked in global.flags and divided back out
+ rather than multiplied in again — the same pairing the coin-drop rate uses, and the
+ same trap you.res once fell into. Verified across six save/load cycles and 20,000
+ allbuff calls: zero drift.
+ Your fights are now as deadly as they were always meant to be, which is to say
+ roughly eighteen times less so than they have been
+Two corrections found by the tests, not by reading:
+ the recovered base needed flooring at 1 — anything resetting hpm without clearing the
+ flag recovered 1/18.86 instead, and allareas immediately reported the player at 12 max
+ HP with six rank duels flipped to kill > die; and Math.ceil rather than Math.round, to
+ agree with stat_r to the unit
+Also: tests/run.sh now serves with -c-1. Without it http-server sends max-age=3600 and
+ a browser can be handed a mod.js from before the edit being tested — which showed up
+ once during this work as an allareas failure that would not reproduce standalone.
+ A flaky safety net is worse than a failing one.
+Tests. tests/targets.mjs gains 5 checks — max HP survives one stat_r and two hundred,
+ the factor lives in global.flags rather than being written as a total, hpm never drops
+ below the game's own value whatever resets it, and 5,000 allbuff calls do not compound
+ it. Verified they fail against the old behaviour
+ All 2,790 allareas matchups still pass — they were measuring the right number all
+ along. It was play that had the wrong one.
+
+----MOD 3.8:
+[2026/09/23]
+Tooling, not gameplay: the balance numbers now come with error bars.
+ Every balance figure in the suite is a sample mean of something heavy-tailed — crit rate
+ reaches 33% and a crit is ~8x a normal swing. Until now the scripts reported point
+ estimates: fightsmoke medians nine duels, allareas prints one number per matchup, and
+ nothing said how much of a gap between measurement and target was real.
+ tests/lib/stats.mjs — the first shared code in tests/. Confidence intervals on
+ Student's t (z at n=9 is 15% too narrow, and fightsmoke fights nine times), plus a
+ practical-equivalence check that fails two ways — real drift, or an interval too wide
+ to conclude anything — and says which
+ fightsmoke now prints a CI beside the median. Immediately useful: The Long Vigil's
+ median of 2 swings carries an interval of [0.46, 4.87], ±83%. That number was being
+ read as a measurement and it is noise. Flagged in place rather than failed
+New: tests/targets.mjs. Every other balance script checks a RELATION — kill < die,
+ whiff 0. None checked whether the model hits its own dials, which a single matchup
+ cannot tell you. 45 spawns an area, compared against the target THAT SPAWN was solved
+ for, with an interval. Catches the model drifting without breaking the ordering
+ Verified it fails: a 15% drift injected into _modDmg is caught at 0.869 of target.
+ Result: the model is on its dials — every area tracks its per-spawn kill and die
+ targets to within 1%, and kill < die holds on 45/45 spawns in all four areas sampled
+Four corrections along the way, all to the new test, none to the mod:
+ comparing against the flat dial instead of the per-spawn target (a basement rat is
+ legitimately a five-swing kill); omitting the hit rate, which pinned every die ratio at
+ exactly 0.45 — MOD_ENEMY.hit, the tell; a plain sample mean instead of the crit
+ stratification CLAUDE.md already demands; and testing significance rather than
+ equivalence, which at ±0.1% precision fails on a 1% quantisation bias that means
+ nothing in play
+Not gameplay. No change to mod.js, index.html, or any save.
+
+----MOD 3.7:
+[2026/09/16]
+Fixed: every item the mod added was labelled "Book".
+ An item's id is two schemes at once. One decides which list it belongs to; the OTHER
+ decides its class, read straight off the number with no field for it — under 3000 Food,
+ 3000-4999 Medicine/Tool, 5000-8999 Material/Misc, 9000+ Book. stype is not a proxy:
+ the author's stype 4 spans Food, Medicine and Book.
+ Everything the mod added sat at 9100+ to stay clear of the author's maxima, so
+ 26 of its 32 items read as Books, each with a "Read: Never" line under it — ore,
+ hides, essences, tonics, and all fourteen pills. Only the six skillbook manuals,
+ which really are books, were right
+ Tonics are 4201-4204, breakthrough pills 4210-4219, spirit pills 4220-4223, crafting
+ materials 5200-5211. The manuals stay where they were
+ MOD_ITEM_IDS now holds the ranges with the class table written beside them, so the
+ next addition picks from it instead of from "whatever is free above his highest"
+Your items survive the move. The inventory is saved BY id and restored by matching it,
+ so an entry whose id no longer exists is dropped without a word — and a Tribulation
+ Pill is seven million coin. The save is read BEFORE the game loads it, the moved ids
+ are noted, and those stacks are given back afterwards under their new ids
+ The save itself is never rewritten. Anything unexpected in it and the migration does
+ nothing at all — a migration that fails is better than a load that fails.
+Tests. tests/ids.mjs gains 7 checks: the class of every added item, taken from the mod's
+ own tables rather than from the ids; that nothing which is not a book gets a "Read:"
+ footer; and a round trip through a save deliberately rewritten back to 3.6's ids, which
+ must come back with the same amounts and must not double-grant on the next save.
+ Verified they fail before the fix
+ modItems() prints every added item with the class its id gives it
+
+----MOD 3.6:
+[2026/09/16]
+Fixed: two skills were both called "Foraging".
+ The base game has skl.hvt, "Foraging". MOD 1.x added skl.frg and called it the same
+ thing — two identical rows in the skill panel, and TEN identically named titles,
+ because a title's name is derived from its skill's. Nothing errored.
+ The mod's is Wildcraft now. Its key and id are unchanged, so nobody loses a level —
+ names are not saved, ids are
+ Swept the rest: all 140 things the mod names are unique, including across the whole
+ inventory bag (item / wpn / eqp / sld / acc share one list)
+ The author's own repeats are reported and left alone: "Chashu Ramen" twice, "Bandage"
+ on both an item and an armour, two "Blue Slime", two "Nameless", nine "Training Grounds".
+Fixed: the hunter's quest had become impossible in summer.
+ Yamato wants ten Raw Meat HELD AT ONCE, and raw meat rots on a daily timer that runs
+ twice as fast in summer. A perishable stock converges instead of accumulating, so there
+ is a ceiling and the quest needs that ceiling to be above ten.
+ The mod never touched the drop table — it changed how long a rabbit takes to kill.
+ At the point the quest is offered a Wild Rabbit now has 3,574 HP against the base
+ game's 93, so one that died in a single hit takes ten. Ten times less meat an hour
+ against an unchanged rot clock
+ Measured at the old 6%: steady state 15.6 in a normal season, 51 in winter, and
+ 4.3 in summer — against the ten required
+ Raw meat now drops at 18%, solved backwards from the requirement rather than picked:
+ a steady state of 25 in the worst season and the first hunting area. Applied to every
+ creature carrying it, found by scanning the drop tables rather than by naming the
+ rabbit and the wolf. Worst case is now 26 against 10
+ Not an income: a kill in the first hunting area yields 0.28 coin of meat.
+ modMeat() prints the table per area and season
+Tests. tests/names.mjs — 12 checks. Verified both halves fail without their fix: the
+ name half reports the skill clash and all five title collisions, and the meat half
+ reports a summer steady state of 4 against 10 at the old drop rate
+
+----MOD 3.5:
+[2026/09/15]
+Crafting goes to five stars.
+ It stopped at two. Measured across all 62 recipes, by what they produce AND by whether
+ any giveRcp call can actually teach them: 35/43 reachable at 1★, 7/17 at 2★, 0 of 1 at
+ 3★, 1 of 1 at 4★, and nothing whatsoever at 5★. The only 3-star recipe in the game is
+ Trinity, and nothing can teach it; the one 4-star result is a Clover Pin you get for
+ holding seven clovers.
+ Four new rungs, 2★ to 5★, each a full set — weapon, body armour, shield,
+ accessory and tonic. Twenty craftable things
+ Twelve materials along three lines (ore, weave, essence), one set per rung
+ Four resource nodes — the Stone Hollow, the Iron Seam, Ashfall Quarry and the
+ Deep Vein — behind one door on the Village Center, opening one at a time with the
+ story. They set no tier flag: a gathering spot that hands out a level cap on arrival
+ is the catacombs bug over again
+ A Gather action that turns up at the first node you walk into. It trains Mining,
+ Geology and Foraging — three skills the base game defines and then barely uses —
+ and the first thing out of a seam teaches that rung's five blueprints
+ Each rung eats the rung below it, so it is a ladder and not four shopping lists.
+ The 4★ blade lands beside the game's own best (Fate Cutters, str 108) and the 5★ one
+ goes past it, since that tier has nothing else in it
+Found: 19 of the 62 base-game recipes cannot be learned at all. They are defined,
+ priced and complete, and no giveRcp anywhere hands them over — trr, lnch1/2/3, orgs,
+ ffsh1/2, fnori, cbun1, sshl, hpck, steak, cnmnb, brth, eggsp, crmchd, msoop, jln4.
+ Listed in the wiki rather than wired up; which were gated on purpose is not our call
+Economy. The first pass came out backwards — a 5★ sword priced at 1,161 while each of
+ the eight ingots it eats priced at 13,000, so selling the ore paid ninety times better
+ than using it. The value model is re-indexed after the new recipes exist, and the
+ materials are anchored at 6/18/54/162 instead of being priced on an equipment scale.
+ Gathering now pays about 6 coin a second sold, against roughly 1 from endgame combat
+Wiki. A new Crafting page, after Items: what can be made at each rarity, which
+ base-game recipes are unreachable, every rung with its costs, and the gather rates
+Tests. tests/crafting.mjs — 27 checks, including the two traps this had to avoid:
+ every id must sit in the block load() restores its namespace from, and no rarity may
+ pass 6 or equip() runs off the end of global.text.wecs the moment you wear it
+
+----MOD 3.4:
+[2026/09/15]
+The title picker groups the titles it used to leave in a heap.
+ 595 titles, 512 grouped by skill — and 83 in a flat list at the bottom, which is the
+ wall of rows the grouping existed to remove, just smaller. Reading them, almost none
+ were really one-offs.
+ +19 folded into the skill they belong to. The flagship titles (Executioner,
+ Deathless, Sage...) are paired with their skill in the mod's own table, and the
+ author's numbered tiers are keyed <skill><n> — tghs1/2/3 under Toughness, srd3/srd4
+ under Swordsmanship, dth4 under Death, rtr1 under Retreating
+ Most of those numbered tiers are granted by NOTHING — giveTitle never mentions them
+ anywhere in the game. Unfinished content, like the Damp cellar. They group correctly
+ whether or not they are ever earned.
+ +40 are ladders of their own and now collapse like a skill does: the ten cultivation
+ realms, five for creatures killed, four for items gathered, four for titles collected,
+ four for punch power, three each for time indoors / shields / jobs, and more
+ Each ladder says what it is on the row — "Nameless (4)" could be punch power or
+ titles collected, so the label sits beside it rather than only in the tooltip
+ 595 titles held now renders as 126 rows, and the flat tail is down from 83 to 23 —
+ Nobody, Initiate, Thrasher, Wolf Slayer, Safehouse and friends, which really are
+ one-off story titles with nothing to group under
+ Membership is derived from the key, not typed out: only a LABEL per ladder is written
+ down, eleven of them, and a ladder with no label falls back to its lowest title's name.
+The wiki's title page uses the same grouping, so the two cannot drift apart. It had the
+ same problem solved worse — everything skill-less under "From the story", split into
+ rank buckets. The ladders nest under "Ladders of their own"
+Found: five titles the author never finished. ttl.ddcd has the literal string "null"
+ for its name AND its description; shpt2, shpt3 and mone3 are blank. Nothing grants any
+ of them. The picker skips them rather than drawing a blank row; the wiki lists them and
+ tags them "unfinished", the same call the Damp cellar got
+Tests. tests/titlepicker.mjs gains 8 checks — the ladders group, every one has a
+ readable label, no label names a ladder that does not exist, and "Thrasher" is NOT
+ filed under Throwing on a bare stem match (which is what the trailing-digit rule is
+ there to prevent). Its group counts are derived now instead of hardcoded
+
+----MOD 3.3:
+[2026/09/15]
+Fixed: every title was listed twice after loading a save.
+ Found by auditing, not reported. giveTitle pushes each earned title onto TWO arrays —
+ global.titles and global.titlese — and load() rebuilds global.titles from the save
+ BY INDEX and then appends the whole of titlese on top. So everything earned that
+ session ends up in the array twice. Measured on a character holding 241 titles: 241
+ before a load, 481 after, 241 distinct.
+ It does not compound — the next load finds titlese empty — but one load is enough,
+ and save() writes the inflated array straight back out
+ Both title screens are affected: the game's own, and the mod's grouped picker. Both
+ iterate global.titles, so each title got two rows
+ A base-game bug, but the mod is what makes it loud: vanilla hands out around a hundred
+ titles and section 24 grants five per skill, so the doubling is hundreds of rows.
+ Existing saves are repaired on load, by identity, the same way the skill sheet is —
+ global.titles holds references to the objects in ttl, so the same object twice is
+ always wrong whatever put it there. Not fixed at the source: titlese is the author's
+ mechanism and the mod has no business changing what it means
+Hardened: the rank-duel scaler now refuses the player and creature.default explicitly,
+ matching MOD_scaleEnemy, instead of relying on their not carrying a spawn target
+Tests. tests/ids.mjs gains 8 checks for the title list: distinct before a load, distinct
+ after one, distinct after two, no holes, the worn title still present, and an
+ already-doubled list repaired. Verified they fail without the fix (85 entries for 43
+ distinct) and pass with it
+Audit, no change needed. All 75 locations draw without throwing, no choice row wraps its
+ 22px line, all 12 wiki pages build, no id clashes in any namespace, the wiki has no
+ horizontal overflow at 375/768/1200px, no interval leaks over 15s at 20x speed, and
+ acts/you.skls/furn/qsts all stay distinct across repeated save/load cycles
+
+----MOD 3.2:
+[2026/09/11]
+Rank 1 is reachable. It just never felt like it.
+ The number under your portrait is the base game's Power rank, and it is DERIVED from
+ your stats every time it is drawn — there is no stored field. Measured on a character
+ built to each rung of the ladder: rank 21,617,349,975 at tier 0, 65,544 at tier 5, 258 at
+ tier 7, 20 at tier 8, and 1 at the very top. Ranks 1–10 all live inside the last fifteen
+ skill levels — at 108 across the board you are still rank 3 — after a whole game in
+ which the number is an unreadable ten-digit blob.
+So there is a second way to hold a rank: take it off whoever has it.
+ The Hall of the First Gate, off the Old Path trailhead past the Western Woods gate.
+ Ten named challengers, rank 10 down to rank 1. Beating one takes that rank
+ Strictly in order. Rank N opens only when rank N+1 has been taken, and each rung also
+ wants a character level (60, 65, 70 ... 100, 110), so the ten fights span the endgame
+ instead of being one evening
+ The rank you hold floors the rank you are shown, and never caps it — hold rank 6 with
+ rank-900 stats and it shows 6; pass it on your own and it shows the better number. A held
+ rank is painted gold
+ Implemented by wrapping you.rank and keeping the earned rank in global.flags, like
+ mod_realm. Nothing is written into a stat, so there is no delta to track and nothing
+ compounds on load — the trap you.res and skl.p both fell into.
+ Losing costs nothing but the walk back. A rank you have not taken cannot be taken off
+ you, and a rank you have taken is never lost
+The duels get longer, not deadlier. Each is a protected one-creature area, so the enemy
+ scaling already treats it as an arena boss and anchors it to your power as you walk in.
+ Each rung then steps up ~7% into FIGHT LENGTH — the rank 1 duel runs 1.77x the rank 10
+ one — and the kill<die guarantee is re-derived from the targets afterwards rather than
+ hoped for
+ Scaling both targets in opposite directions does not survive ten rungs: margin 1.5 over
+ step^1.5 is already under 1.2 by a step of 1.16.
+Wiki. A new "The rank ladder" page, after Progression: the formula, what a maxed
+ character actually measures at, and the ten rungs with their gates and payouts
+Found, not fixed: the player's real max HP is not the one the balance model uses.
+ Section 9's hpTrack assigns you.hpmax at the end of the allbuff wrapper, but allbuff
+ opens with stat_r(), which recomputes hpmax from the base — so the raised value only
+ survives until the next stat_r, and the tick calls that constantly. Sampled 43 times over
+ six seconds of real play on a cap-60 character, you.hpmax was 10,050 every time; the
+ hpTrack value of 184,754 never appeared. Every balance script reads it in the one instant
+ it is standing, so enemy damage is sized for ~18x the health you really have. Harmless
+ early, worst in the endgame. Written up in MOD-NOTES and CLAUDE.md; the fix is its own
+ job, because it moves a number all 2,790 matchups are measured against.
+Tests. tests/ranks.mjs — 31 checks. The wrapper must floor and never cap; the held rank
+ must survive a save/load; the hall must offer exactly one rung at a time and every line
+ must fit a 22px row; and all 100 matchups (10 rungs x 10 story tiers) must keep kill<die
+ with zero whiffs, measured through the game's own dmg_calc. allareas is now 2,790
+ matchups across 31 areas
+
+----MOD 3.1:
+[2026/09/09]
+Fixed: a hundred Fire Masteries and a hundred Companions Disciplines.
+ Two skills shared id 2010 — the Companions parent (2000 + section 10,
+ section 11) and Fire Mastery (2010 + 0, section 29). Ids are the save's key for a
+ skill and the loader matches them in a loop with no break, so one shared id pushed
+ BOTH skills onto the sheet for BOTH saved entries. The next save wrote all of them.
+ It doubled every save/load: 1, 2, 4, 8, 16, 32... A week of play is a hundred of each.
+ Only Fire Mastery moved (2010 → 2016), so water through dark keep the ids they were
+ saved under and nobody loses those levels
+ The Companions parent gets id 2010 back, and with it the level cap that the clash had
+ been handing to Fire Mastery instead
+Existing saves are repaired. The duplicates are IN the save, so fixing the id only stopped
+ it getting worse. The skill list is now de-duplicated on load and the panel redrawn —
+ a 138-row sheet comes back as 12, with every level intact
+Also fixed: The Sunken Hollow and the Pill Tower both had location id 976 (the mod's own,
+ from MOD 2.9). global.lst_loc is matched the same way, so both would have drawn.
+ The tower is 980 now
+Tests. tests/ids.mjs — 32 checks. Every namespace (skl, ttl, act, abl, item, wpn, eqp,
+ sld, acc, chss, area, creature, quest, rcp) must have distinct ids; five save/load cycles
+ must not grow the sheet; and a deliberately damaged sheet must come back clean.
+ Nothing checked this before, which is why it shipped.
+
+You can get into the Marketplace again.
+ The base game's chain: finish at the dojo, then a Paper Boy turns up at the village
+ center at 40% a visit with a "Pamphlet", then you read it for three hours. Exactly one
+ item in the game sets the flag, and the Paper Boy is retired the moment the Pamphlet is
+ GIVEN rather than read. MOD 1.x added selling and never marked it a key item — so you
+ could sell it at the food stand, before ever reading it, and lose the marketplace, its
+ three shops, the guard-duty quest and the realm 2-5 breakthrough pills permanently.
+ Key items can no longer be sold. Derived, not listed: the game's own source is scanned
+ for flags it ever sets back to false, and an item that sets a flag nothing clears is
+ the only copy of something. Six items protected. A Smoke Bomb sets a flag the game does
+ clear, so it is state rather than a key, and stays sellable
+ The Paper Boy comes back if you no longer have a Pamphlet — which is what gets a save
+ that is already stuck out of the hole, without a console. A fresh character still meets
+ him exactly once, through the author's own code
+ And the village center says WHY the door is not there instead of silently omitting the
+ line: "finish at the dojo first", "read your Pamphlet", or "you do not know the way",
+ with the full explanation in the tooltip. The Herbalist inside gets the same treatment
+ Added lines sit above the location's "<= Return" and are kept under 50 characters:
+ .chs is height:22px with no overflow rule, so a choice that wraps spills into the next row.
+Tests. tests/marketplace.mjs — 30 checks, including that every added line fits on one row.
+
+----MOD 3.0:
+[2026/09/09]
+A wiki. A wiki button on the bottom bar and a "Game wiki" row in settings, both
+ opening a reference in a tab of its own. Eleven pages: Start here, Progression, Skills,
+ Cultivation, Elemental mastery, Titles, Areas & enemies, Items, Actions, The dojo, Mechanics
+ It is GENERATED from the live game data every time it is opened, not written out.
+ A hand-written wiki for 100 skills, 595 titles, 371 items and 19 areas would be wrong
+ within a week, and a reference nobody can trust is worse than none. Reading skl, item,
+ area, ttl, act and the mod's own tables means it cannot disagree with the game —
+ including changes made from the console mid-session.
+ It reads the save too, so it doubles as a progress sheet: your level in every skill,
+ which perks you have taken, which titles you hold, your realm and what the next one costs
+ Everything you have not reached is still listed — it is a wiki, not a fog-of-war map
+ Groups collapse. With everything listed, three pages ran past 40,000 pixels open;
+ collapsed, the tallest is 5,800 and Skills opens as ten lines you can take in at once
+ Search opens the groups that still have matches, since a match hidden inside a
+ collapsed group reads as no match at all
+ No fetches and no dependencies — it works from file:// as readily as from a server
+Two things found by generating it rather than writing it:
+ Spawn chances were not what pop[i].c says. z_bake normalises those weights into
+ area.popc, the [lo,hi] bands mon_gen actually rolls against, and the weights need not
+ sum to 1. The Southern forest's .35/.45/.25 are really 33/43/24%. The wiki reads the bands
+ Nothing can ever spawn in the Damp cellar (base game). Its pop entries carry no
+ weight at all, so z_bake produces NaN bands and no comparison in the spawn loop can be
+ true. Nothing in the game travels there either. Left as it is and reported in the wiki
+ rather than quietly fixed — it is the author's unfinished content, like chss.pltwr1 was
+Not listed as places: "Somewhere" (where current_z parks whenever you are not fighting)
+and "Test" (reached only from chss.tst, which has id -1 and is in no sector).
+Tests. tests/wiki.mjs — 57 checks. Coverage is counted from the game rather than
+ listed, so adding a skill without adding it to the wiki fails the suite automatically
+
+----MOD 2.9:
+[2026/09/08]
+The Catacombs are reachable. 26 finished locations — The Mourning Hall, Eleven Wisemen,
+ Thawing Candles, Tunnel of the Dead — written by the game's author and linked to by nothing
+ An entrance on the Village Center, which is where their own exit already led
+ Not a word of it is the mod's; the entrance is the whole change
+ Opens once you have been to the deep forest, so the level cap ladder keeps its order:
+ forest 20 → deep forest 30 → catacombs 40 → golem arena 50
+ This also makes the cap-40 rung live for the first time — nothing could reach it before
+The Pill Tower. The author wrote this choice on the Village Center and commented it out;
+ the location behind it was never built. Now it is
+ An alchemists' tower that sells the pill grades a village herbalist cannot source,
+ realms 6–10 and the Sublime and Transcendent Spirit Pills
+ A spirit vein you can sit in once a day for Qi Circulation exp, scaling with your realm
+ They do not let mortals past the door — you need a realm to get in
+
+
+----MOD 2.8:
+[2026/09/08]
+FIXED: the breakthrough pills had no source. All ten existed as items and nothing gave,
+ dropped or sold them, so the entire realm ladder was unreachable in normal play
+Three sources now, matching how far along each realm is:
+ Realm 1 — the instructor hands it over when the dojo finishes teaching you
+ Realms 2–5 — the Herbalist, who already sells spirit pills (900 to 38,000 copper)
+ Realms 6–10 — the dojo's Level Advancement rungs at 45, 60, 75, 90 and 105
+ The first one is deliberately NOT the Herbalist: the marketplace is gated behind the
+ Paper Boy, a 40% chance per visit, and realm 1 opens long before that
+The Herbalist also stocks the Superior and Refined Spirit Pills now
+
+
+----MOD 2.7:
+[2026/09/08]
+Circulate Qi is unlocked by the dojo now, not by a skill level
+ Clear the Easiest, Easy and Normal dummies and it is yours — it used to hang off
+ Temperance 5, a skill you train by throwing your possessions away, which is an obscure
+ gate for the thing the entire cultivation system is levelled through
+ Qi Circulation appears on the skill sheet at the same moment
+ Temperance keeps a perk at level 5 (INT +1, Energy Effectiveness +2%) so existing saves
+ do not have their milestone flags shift under them
+FIXED: the bottom bar was 8px taller than it should be, covering part of the panel above it
+ The mod's three buttons were display:inline-block where the game's own are plain inline;
+ only an inline-block forces the line box to grow. Bar is back to its original 24px
+
+
+----MOD 2.6:
+[2026/09/08]
+Cultivation realms. The game had Qi Circulation, spirit pills "made from condensed Ki",
+ meridians in its skill text — and no realm to advance through. Now there are seven:
+ Ten realms above Mortal, on exactly the levels that earn each title rank — so realm 3
+ is the Qi Circulation level that earns a rank 3 title, and its own title comes out rank 3
+ Qi Refining · Foundation Establishment · Core Formation · Nascent Soul · Spirit Severing ·
+ Soul Transformation · Void Refinement · Body Integration · Great Ascension · Tribulation Transcendence
+ Reaching the Qi Circulation level does NOT advance you — it puts you at a bottleneck
+ A breakthrough costs a pill, and can fail; training past the requirement improves the odds
+ Each realm multiplies every stat and widens what the body holds, up to x6 at the top
+ Ten breakthrough pills, one per realm, and a title for each realm reached
+Elemental mastery. The six Absorption skills were purely defensive — trained by being hit,
+ and no way to use an element at all. Six Mastery skills to match them:
+ Fire, Water, Air, Earth, Light and Dark, each with a technique that fires in combat
+ "Lantern Splitting Palm", "Still Water Draw", "Hollow Gale", "Settling Weight",
+ "Noon Without Shade", "Lamp Going Out"
+ They scale with INT and their element rather than with your weapon, and are capped at
+ 45% of strikes so a weapon still matters
+ Nothing fires below Qi Refining — your channels are not open yet
+modRealm() and modMastery() print the ladders
+
+
+----MOD 2.5:
+[2026/09/08]
+The mod's changelog moved out of the game's own file and into this one
+ It had been prepended to changelog/changelog.html above the author's entries, which meant
+ the mod was editing a file that was not its own. That file is now untouched — the only
+ change the mod makes to anything of the author's is the single script tag in index.html
+The bottom-bar "changelog" button opens this file; the version number opens the game's, as it always did
+ (the version number's link was absolute, "/changelog/changelog.html", so it 404'd from a
+ file:// URL or any subdirectory — it is resolved relative to the page now)
+
+
+----MOD 2.4:
+[2026/09/08]
+The dojo's Level Advancement ladder now runs to character level 110
+ It stopped at 30, though the instructor promises a reward "after every 5 levels you reach"
+ Sixteen more rungs, 35 to 110 in fives, sequential like the original six
+Four new spirit pill grades, sized against what a level actually costs
+ Superior 100,000 · Refined 600,000 · Sublime 3,500,000 · Transcendent 20,000,000 EXP
+ (a level at 110 costs 5,336,100 — the game's best pill was worth 0.3% of one)
+A third grade of skillbook, +30% weapon mastery exp, offered as a CHOICE at the 50/75/100 rungs
+ Sword Saint, Nightblade, Headsman, Dragoon, Earthbreaker and Iron Body Manuals
+ the same shape as the choice the instructor gives you the first time you clear the dummies
+modDojo() prints the ladder and what is claimed
+Repository: a README added, describing the mod, how to disable it, and how to run the tests
+ (not a game change — logged here because every change gets an entry)
+
+
+----MOD 2.3:
+[2026/09/08]
+Title ranks now run 1–10 and are derived from the level that earns them, not assigned by feel
+ rank 1 at lv 1, rank 3 at 18, rank 5 at 45, rank 7 at 70, rank 9 at 90, rank 10 at 110
+ (the base game gave out eight rank-3 titles at skill level 8–15 — Runner at 10, Rookie at 15)
+Every skill now grants five titles, at levels 25 / 50 / 75 / 90 / 110 — ranks 3 / 5 / 7 / 9 / 10
+ 465 new titles; the pool goes from 120 to 585, and all ten ranks are populated
+Titles do something now: each raises its own skill's exp rate, 5% at rank 3 up to 30% at rank 10
+ Only the title you are WEARING applies — which finally gives the selection screen a purpose
+ At renown level N, every title of rank N or lower applies permanently whether worn or not,
+ so renown 10 makes the whole collection passive
+Renown thresholds rescaled to the larger pool: level 10 now wants 550 titles rather than 100
+modTitles() prints held/total by rank and which ranks have gone passive; modTitles(n) lists one rank
+The title screen groups by skill — one row showing the best you hold, with a caret to open the rest
+ Base-game titles group with the generated ones, so Rookie and Fighter sit under Fighting too
+ Story titles have no skill and stay as plain rows below the groups
+Titles are coloured by rank, in the picker and in the tooltip, and the tooltip states the rank
+ grey 1, cyan 2, lime 3, yellow 4, orange 5, purple 6, pink 7, red 8, gold 9, white 10
+ FIXED: the game's own rank 7 tooltip threw an error (it set `this.dl`, which is undefined
+ in that branch). It had never run because the base game had no title above rank 5 —
+ there are now 93 at rank 7. Ranks 1, 8, 9 and 10 had no colour at all
+
+
+----MOD 2.2:
+[2026/09/08]
+Perks now run the whole ladder — every skill has milestones at 10/25/50/60/75/90/110
+ (447 added; 81 of 94 skills previously gave nothing at all between level 51 and 110)
+Perk rewards match the skill's type: resistances give resistance, absorptions give elemental defence,
+ combat gives STR and crit damage, mental gives INT and EXP, gathering gives energy
+Critical damage from combat perks kept deliberately small — it is shared by 13 skills and compounds,
+ and at full value a crit reached 23x a normal swing, which made combat a coin flip on crits
+The four added actions are earned now, not handed over on load
+ Toughness 4 → Endurance Drill, Harvesting 4 → Forage,
+ Temperance 5 → Circulate Qi, Literacy 8 → Practice Calligraphy
+New settings checkbox "Unrestricted actions": run several actions at once, and start them anywhere
+ (off by default — both are limits the base game means to impose)
+Three save slots, with a "saves" panel and a "new save" button in the bottom bar
+ Switching saves the slot you are leaving first; "delete the save" now deletes only the slot you are in
+Changelog reachable from a "changelog" button in the bottom bar, not just the version number
+
+
+----MOD 2.1:
+[2026/09/08]
+Enemy scaling rewritten — it had been scaling the wrong fields
+ An enemy's STR is subtracted from your damage, so scaling it armoured them instead of
+ lengthening the fight. The tutorial needed ~63,000 swings and killed you in one;
+ everything from the southern forest onward was kill-in-1 / die-in-1
+Difficulty is now solved per spawn against your actual power: ~8 swings to kill, ~20 to die
+Enemies in an area's higher level band are genuinely harder, and each creature keeps its own character
+Arena golems are proper boss fights rather than the softest thing in the game
+
+
+----MOD 2.0:
+[2026/09/07]
+The ~100 "discovered by playing" skills consolidated to 10, with the same total effect
+Skill EXP curve replaced so level 110 is reachable in about six months at 1x speed
+ (the base game's curve made level 109→110 alone cost 1.16e14 xp)
+The three added areas now stay hidden until golem arena IV is cleared, then open in order
+Saves from before this update still load; skills that were merged away simply do not return
+
+
+
diff --git a/index.html b/index.html
index 0756d2b..297747b 100644
--- a/index.html
+++ b/index.html
@@ -14764,5 +14764,8 @@
}
+
+
+