From a5dd0e48b49e2b4bbc507a2de54d28e6bdbfa3e0 Mon Sep 17 00:00:00 2001 From: Xeus24 <201404920+Xeus24@users.noreply.github.com> Date: Sat, 5 Sep 2026 19:15:26 +0000 Subject: [PATCH 01/44] Add proto23 mod: mod.js, regression tests, design notes The mod is a single mod.js loaded by one appended script tag in index.html, so removing that line disables it and `git checkout index.html` reverts it. Sections: speed and skill-xp control, extended milestones, 113 added skills with a story-tied level cap, enemy and area scaling plus three new areas, selling, enemy coin drops, titles and Renown, skill-panel sections and parent skills, and settings-menu multiplier boxes. Verified: 752/752 perks pass the audit, save is 19.3 KB fully maxed and reloads intact, balance sits at kill 2-10 / die 5-42 across every tier. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015hvauCeK8WKVAPwnyWsNHp --- .gitignore | 1 + CLAUDE.md | 103 ++ MOD-NOTES.md | 2208 +++++++++++++++++++++++ index.html | 3 + mod.js | 3651 ++++++++++++++++++++++++++++++++++++++ package.json | 16 + tests/README.md | 64 + tests/audit.mjs | 122 ++ tests/audit2.mjs | 89 + tests/econ.mjs | 48 + tests/fullbal.mjs | 165 ++ tests/realbal.mjs | 87 + tests/run.sh | 57 + tests/settings-boxes.mjs | 119 ++ tests/sweep5.mjs | 60 + 15 files changed, 6793 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 MOD-NOTES.md create mode 100644 mod.js create mode 100644 package.json create mode 100644 tests/README.md create mode 100644 tests/audit.mjs create mode 100644 tests/audit2.mjs create mode 100644 tests/econ.mjs create mode 100644 tests/fullbal.mjs create mode 100644 tests/realbal.mjs create mode 100755 tests/run.sh create mode 100644 tests/settings-boxes.mjs create mode 100644 tests/sweep5.mjs 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/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c8bac1f --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,103 @@ +# 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. + +## 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 + +Four dials on `MOD_ENEMY`, live in the browser console: + +```js +base: 4, rate: 0.02, hpPow: 1.1, tierRate: 0.008 +``` + +The constraint that matters: the game's exp curve is the real limiter, not the +level cap. `expnext = 50 + (lvl+1)^log(9*lvl+1)`, so level 20 costs 13.6M exp +and a passive skill reaches only ~level 14 after 1000 hours. **Tune against +levels 10–22, not against the cap.** Fitting enemies to capped skills made the +late game unwinnable; that mistake is written up in MOD-NOTES.md under +"Balance, fifth pass". + +Run `npm test -- realbal` after any balance change. Healthy output is +`kill 2–10 / die 5–42`, tightening monotonically toward the endgame. If "die" +drops to 1–2 anywhere, that area is a coin flip. + +## Testing + +```sh +npm install && npx playwright install chromium # once +npm test # everything +./tests/run.sh audit realbal # a subset +``` + +`tests/README.md` explains what each script covers. Note that several of them +**write to the save** — export one first, or run against a copy. + +Always finish a change with `node --check mod.js` (`npm run check`) plus the +relevant test script. The mod is ~3,650 lines of wrappers around a codebase +with no types and no module boundaries; the tests are the only safety net. diff --git a/MOD-NOTES.md b/MOD-NOTES.md new file mode 100644 index 0000000..0647236 --- /dev/null +++ b/MOD-NOTES.md @@ -0,0 +1,2208 @@ +# Proto23 local mod + +Set up 2026-09-04. All mod code lives in `mod.js`. The only change to the +game itself is one ` + + + diff --git a/mod.js b/mod.js new file mode 100644 index 0000000..a9322ab --- /dev/null +++ b/mod.js @@ -0,0 +1,3651 @@ +/* =========================================================================== + Proto23 — local mod + --------------------------------------------------------------------------- + Loaded as a plain +``` + +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. + +## 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. + +## What it changes + +Twenty-seven 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). + +**Progression** + +- Skill level caps tied to story progress, 10 through 110, rather than one flat + ceiling. +- A geometric skill exp curve, replacing the base game's. Its own was + super-exponential — level 109→110 alone cost 1.16e14 xp, making the top of + the ladder unreachable by a factor of about 10¹³. Level 110 now takes roughly + six months of steady play at 1× speed. +- Perks at 10/25/50/60/75/90/110 on every skill. 81 of 94 skills previously + gave nothing at all between level 51 and 110. +- 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** + +- Enemy scaling rewritten. It is solved per spawn against your actual 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. + +**Skills, titles and items** + +- Four new skills with paired actions, each earned from a milestone on the + base-game skill it grows out of. +- Title ranks 1–10, derived from the level that earns the title rather than + assigned by feel; five titles per skill; and titles that do something, worn + or made passive by Renown. +- Selling, enemy coin drops, and effects for the skills and affinities the base + game left inert. + +**Quality of life** + +- Three save slots, with a "start new save" button. +- Game speed, skill xp and coin-drop multipliers as settings boxes. +- 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. + +Changes are also logged in the game's own changelog, reachable from the +`changelog` button in the bottom bar. + +## 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 19 scripts +./tests/run.sh audit combat # a subset +``` + +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, three skill builds — +2,490 matchups, all of which must be winnable. + +**Several tests write to the save.** Export one first, or run them against a +copy of the folder. + +## Layout + +| | | +|---|---| +| `mod.js` | the entire mod, ~5,800 lines | +| `MOD-NOTES.md` | the design record, section by section, with the reasoning | +| `CLAUDE.md` | the constraints that are easy to violate by accident | +| `tests/` | 19 Playwright scripts | +| `index.html` | the author's game, plus one script tag | + +## Credit + +The game is truezhangwei's. Everything in `mod.js`, `tests/`, `MOD-NOTES.md` and +this file is a modification of it, and none of it is endorsed by the author. From e8bbab46cc338d5cdf3c332beeb03d6404a39360 Mon Sep 17 00:00:00 2001 From: Xeus24 <201404920+Xeus24@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:11:56 -0700 Subject: [PATCH 12/44] Log the README in the changelog, and make the rule literal I had said every change gets a changelog entry, then left the README out on the reasoning that the in-game changelog is for things a player would notice and a repo README is not one of them. That reasoning is defensible but it was applied silently, which is the part that is wrong -- a rule with an unstated exception is not a rule. So: the README gets an entry, marked as not a game change, and CLAUDE.md now says the rule is literal on purpose, so there is never a judgement call about what counts. Still purely additive against the author's file (92 insertions, 0 deletions); their 19 version blocks are untouched below the divider. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 8 +++++--- changelog/changelog.html | 2 ++ 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ead6ec5..cb1b57b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -202,9 +202,11 @@ skill tops out below the highest story cap. `you.res` continuously for those skills and reconciles it through `global.flags.mod_aff`. -**`changelog/changelog.html` gets an entry for every change.** The mod's block -sits at the top under a gold header, newest first, above the base game's own -entries. It is reachable in game from the `changelog` button in the bottom bar. +**`changelog/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. The mod's block sits at the top +under a gold header, newest first, above the base game's own entries. It is reachable in game from the `changelog` button in the bottom bar. This and the one script tag in `index.html` are the only files outside the mod's own that it touches. diff --git a/changelog/changelog.html b/changelog/changelog.html index ef2a2cf..55271bf 100644 --- a/changelog/changelog.html +++ b/changelog/changelog.html @@ -34,6 +34,8 @@   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:
From 151e298a4aa2d07e2879cd0f97fcf73628377f08 Mon Sep 17 00:00:00 2001 From: Xeus24 <201404920+Xeus24@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:15:39 -0700 Subject: [PATCH 13/44] Split the mod's changelog into its own file The mod's entries had been prepended to changelog/changelog.html, above the author's, under a gold header. It read well and it was wrong: the mod was editing a file that was not its own. They now live in changelog/mod-changelog.html, which restores the property the whole design rests on -- the single script tag in index.html is the only change the mod makes to anything of the author's. git diff origin/main -- changelog/changelog.html comes back empty, and CLAUDE.md now says to check that before committing. The bottom-bar button opens the mod's file; the version number opens the game's, which is what it always did. It is still rebound, because the game's own handler is window.open('/changelog/changelog.html') -- an absolute path from the server root, so it 404s from a file:// URL or any subdirectory. It resolves against document.baseURI now and goes to the same place. The author's file keeps its dark-mode problem: it sets no colours and its palette assumes a white page, so half of it is unreadable in a dark browser. The mod's file pins a white background for itself; his is his to fix. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 23 +++++--- MOD-NOTES.md | 22 ++++--- README.md | 8 ++- changelog/changelog.html | 92 ----------------------------- changelog/mod-changelog.html | 109 +++++++++++++++++++++++++++++++++++ mod.js | 71 ++++++++++++----------- 6 files changed, 181 insertions(+), 144 deletions(-) create mode 100644 changelog/mod-changelog.html diff --git a/CLAUDE.md b/CLAUDE.md index cb1b57b..92a2400 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -202,13 +202,22 @@ skill tops out below the highest story cap. `you.res` continuously for those skills and reconciles it through `global.flags.mod_aff`. -**`changelog/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. The mod's block sits at the top -under a gold header, newest first, above the base game's own entries. It is reachable in game from the `changelog` button in the bottom bar. -This and the one script tag in `index.html` are the only files outside the mod's -own that it touches. +**`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 `origin/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 origin/main -- changelog/changelog.html # must be empty +``` + +In game the `changelog` button opens the mod's file; the version number opens +his. ## Actions diff --git a/MOD-NOTES.md b/MOD-NOTES.md index 9560a1b..0ee6325 100644 --- a/MOD-NOTES.md +++ b/MOD-NOTES.md @@ -1967,14 +1967,20 @@ resolved against `document.baseURI` instead. The version number still works — its handler is rebound by replacing the node, the game having attached it anonymously. -The mod's entries live at the top of that same file under a gold header, above -the author's, with the base game's changelog untouched below a divider. That -makes `changelog/changelog.html` a second file the mod edits, alongside the one -script tag in `index.html`; `git checkout changelog/changelog.html` puts it -back. The file also gained an explicit white background, because it sets no -colours of its own and the author's palette — blue dates, black-backed headers -— assumes a white page, so a browser in dark mode rendered half of it -unreadable. +The mod's entries live in **`changelog/mod-changelog.html`**, its own file. + +They did not start there. The first version prepended them to +`changelog/changelog.html`, above the author's, under a gold header — which +worked, and read well, and meant the mod was editing a file that was not its +own. Splitting them out restores the property the whole design rests on: **the +single script tag in `index.html` is the only change the mod makes to anything +of the author's.** `git diff origin/main -- changelog/changelog.html` comes back +empty, and CLAUDE.md says to check that before committing. + +The mod's file pins a white background, because the author's sets no colours and +his palette — blue dates, black-backed headers — assumes a white page, so a +browser in dark mode renders half of it unreadable. His file is left with that +problem rather than edited to fix it; it is his to fix. **Every change from here on gets an entry there.** That is the point of it. diff --git a/README.md b/README.md index d2f54eb..35d1123 100644 --- a/README.md +++ b/README.md @@ -72,8 +72,9 @@ for every one of them, including the reasoning and what was measured, is in - Collapsible skill sections, live effect descriptions, and a title picker grouped by skill. -Changes are also logged in the game's own changelog, reachable from the -`changelog` button in the bottom bar. +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 @@ -114,7 +115,8 @@ copy of the folder. | `MOD-NOTES.md` | the design record, section by section, with the reasoning | | `CLAUDE.md` | the constraints that are easy to violate by accident | | `tests/` | 19 Playwright scripts | -| `index.html` | the author's game, plus one script tag | +| `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 | ## Credit diff --git a/changelog/changelog.html b/changelog/changelog.html index 55271bf..06fb6ed 100644 --- a/changelog/changelog.html +++ b/changelog/changelog.html @@ -9,98 +9,6 @@ CHANGELOG
:::game is still in very early alpha and is severely underfunished:::


- - - PROTO23 MOD — changes made by the local mod.js 
-The base game's own changelog continues below this block. Nothing here is the -original author's. Remove the <script src="mod.js"> line from index.html and none of it applies.
-
- -----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
-
- -——— base game changelog below ———
-
----461ー466:
[2021/03/21]
Storage containers are generalized and can be placed anywhere(bags, chests, altars, etc)、 also trigger putting events
diff --git a/changelog/mod-changelog.html b/changelog/mod-changelog.html new file mode 100644 index 0000000..1328e8a --- /dev/null +++ b/changelog/mod-changelog.html @@ -0,0 +1,109 @@ + + + +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 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/mod.js b/mod.js index a213944..d8ae851 100644 --- a/mod.js +++ b/mod.js @@ -4882,42 +4882,47 @@ function modPerks(key) { /* =========================================================================== 23. CHANGELOG ACCESS --------------------------------------------------------------------------- - The game already has a changelog and already links to it — the version - number in the bottom bar is clickable. Two problems with that. - - It is not discoverable: nothing about "v470" says "click me". And the link - is `window.open('/changelog/changelog.html')`, an absolute path from the - SERVER ROOT, so it only resolves when the game is served from the root of a - host. Opened from a file:// URL or from any subdirectory it 404s, which is - the more common way to run a local copy. - - So: a labelled button next to the version, and a path resolved relative to - the page rather than to the root. The version number keeps working too — - its handler is rebound rather than removed, by replacing the node, since the - game attached it anonymously. - - The mod's own entries live at the top of that same file, above the author's, - under a gold header. That does mean changelog/changelog.html is a second - file the mod edits, alongside the one script tag in index.html; - `git checkout changelog/changelog.html` puts it back. + Two changelogs, two ways in. + + version number -> changelog/changelog.html the game's, the author's + "changelog" -> changelog/mod-changelog.html this mod's + + The mod's entries used to be prepended to the author's file, above his own. + They are in their own file now: the only thing the mod changes in anything + of his is the single script tag in index.html, which was always the point. + + The version number is rebound rather than left alone because the game's own + handler is `window.open('/changelog/changelog.html')` — an absolute path from + the SERVER ROOT, so it only resolves when the game is served from the root of + a host. From a file:// URL or any subdirectory it 404s, which is how a local + copy usually gets opened. It now resolves against document.baseURI and goes + to the same place it always did. Replacing the node is the only way to drop + the game's anonymous listener. + + The labelled button exists because nothing about "v470" says "click me". =========================================================================== */ -MOD.changelog = 'changelog/changelog.html'; +MOD.changelog = 'changelog/mod-changelog.html'; +MOD.changelog_game = 'changelog/changelog.html'; -/* Relative to the document, so it works from file://, from a subdirectory, and +/* Relative to the document, so it works from file://, from a subdirectory and from a server root alike. */ -function MOD_changelogUrl() { - try { - return new URL(MOD.changelog + '#mod', document.baseURI).href; - } catch (e) { - return '/' + MOD.changelog + '#mod'; - } +function MOD_url(rel) { + try { return new URL(rel, document.baseURI).href; } + catch (e) { return '/' + rel; } } function modChangelog() { - var url = MOD_changelogUrl(); + var url = MOD_url(MOD.changelog); + try { window.open(url, '_blank'); } catch (e) {} + console.log('[mod] mod changelog: ' + url); + return url; +} + +function modGameChangelog() { + var url = MOD_url(MOD.changelog_game); try { window.open(url, '_blank'); } catch (e) {} - console.log('[mod] changelog: ' + url); + console.log('[mod] game changelog: ' + url); return url; } @@ -4926,20 +4931,18 @@ function modChangelog() { var btn = addElement(dom.sl, 'span', null, 'sl'); btn.innerHTML = 'changelog'; btn.style.cssText = 'display:inline-block;width:auto;padding:3px 7px;cursor:pointer;'; - btn.title = 'What changed, mod entries first'; + btn.title = "What this mod changed (the version number opens the game's own)"; btn.addEventListener('click', modChangelog); dom.sl.insertBefore(btn, dom.sl_extra); - // Rebind the version number to the same relative URL. Replacing the node is - // the only way to drop the game's anonymous listener. if (dom.vrs && dom.vrs.parentNode) { var v = dom.vrs.cloneNode(true); dom.vrs.parentNode.replaceChild(v, dom.vrs); dom.vrs = v; - v.title = 'Changelog'; - v.addEventListener('click', modChangelog); + v.title = "The game's changelog"; + v.addEventListener('click', modGameChangelog); } - console.log('[mod] changelog button added — ' + MOD_changelogUrl()); + console.log('[mod] changelog button -> ' + MOD_url(MOD.changelog)); } catch (e) { console.warn('[mod] changelog button failed: ' + e.message); } From 1df8c7162140b1422130aebc0570f253bc91b02c Mon Sep 17 00:00:00 2001 From: Xeus24 <201404920+Xeus24@users.noreply.github.com> Date: Tue, 8 Sep 2026 20:28:58 -0700 Subject: [PATCH 14/44] Add cultivation realms and elemental mastery proto23 is already a cultivation game in its furniture -- Qi Circulation, spirit pills made from "condensed Ki", a dojo, meridians in the skill text -- without the thing the genre is about: a realm you advance through, a wall that stops you, and a breakthrough that costs something. Seven realms, Mortal through Ascendant. The ladder, the dantian, bottlenecks and tribulations are genre conventions shared across hundreds of works; nothing here is taken from any particular novel -- no names, no settings, no text -- which is both the legal answer and the right one, since borrowed specifics would sit badly against proto23's own world. The bottleneck is the point. Reaching the Qi Circulation level does NOT advance the realm, it puts you at a wall, and breaking through costs a pill you had to find. The attempt can fail and spends the pill either way; odds start at 55% and improve 5% per level trained past the requirement, capping at 95% -- the genre's advice to consolidate before pushing, made mechanical. Failure drops you to 1 HP but never kills, because the game has a real death system and a breakthrough should not touch it silently. Each realm multiplies every stat, up to x6. Safe to make that large because section 8's enemy model measures the player rather than assuming a curve. The bonus is applied in allbuff and never written into the stat fields, so it cannot compound across loads -- the test runs twenty consecutive allbuff calls and checks the number does not move. Then six elemental masteries, because the six Absorption skills were purely defensive: trained by being hit, with no way to use an element at all. Combat is automatic -- fght calls battle_ai, there is no ability picker -- so a technique is a proc on the swing, capped at 45% so a weapon still matters. Each is an Ability with stt 2, routing through the INT branch of dmg_calc, so it scales with INT and its element rather than with STR and a weapon. Nothing fires at Mortal; the channels are not open. Two things worth keeping: the wrapped battle_ai swallows any error from the proc and falls through to the ordinary attack, so a bug there can never cost a swing (the test forces the picker to throw and checks all 20 swings still land). And the six skills had to be registered with MOD_KEY_BY_ID and MOD_PARENT_OF -- maps sections 10 and 11 build before these skills exist. A skill created late is invisible to them and nothing complains. rnwn needed the same fix in section 17. New test cultivation.mjs, 19 checks. Full suite green, 20 scripts, 974 perks. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 19 ++ MOD-NOTES.md | 67 ++++++ README.md | 9 + changelog/mod-changelog.html | 21 ++ mod.js | 398 ++++++++++++++++++++++++++++++++++- tests/README.md | 1 + tests/cultivation.mjs | 167 +++++++++++++++ tests/run.sh | 2 +- 8 files changed, 682 insertions(+), 2 deletions(-) create mode 100644 tests/cultivation.mjs diff --git a/CLAUDE.md b/CLAUDE.md index 92a2400..4e46788 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -154,6 +154,25 @@ What matters: that location. Only the *first* line of an `sl()` may pass `true`; every later line, including greyed-out hints, must pass `false`. +## Cultivation + +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. + +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 — diff --git a/MOD-NOTES.md b/MOD-NOTES.md index 0ee6325..f226fb2 100644 --- a/MOD-NOTES.md +++ b/MOD-NOTES.md @@ -1770,6 +1770,73 @@ thresholds were rescaled to the larger pool — level 10 wants 550 titles rather than 100 — so it is a real collection project rather than something finished early, and it is now load-bearing rather than decorative. +## Cultivation, and elements you can actually use + +proto23 is already a cultivation game in its furniture — Qi Circulation, spirit +pills "made from condensed Ki", a dojo, meridians in the skill text — without +the thing the genre is actually about: a realm you advance through, a wall that +stops you, and a breakthrough that costs something. + +### The ladder is a genre convention, not a borrowing + +Qi Refining → Foundation Establishment → Core Formation → Nascent Soul → Soul +Transformation → Ascendant. That ladder, the dantian, meridians, bottlenecks and +tribulations are shared across hundreds of works and belong to no one; they are +what makes a thing read as xianxia. Nothing here is lifted from any particular +novel — no names, no settings, no text — which is both the legal answer and the +right one, since borrowed specifics would sit badly against proto23's own world. + +### The bottleneck is the whole point + +Reaching the Qi Circulation level does **not** advance the realm. It puts you at +a wall, and breaking through costs a pill you had to find or buy. A realm you +get for free is just a second name for a level. + +The attempt can fail, and failure spends the pill anyway. Odds start at 55% at +the threshold and improve 5% per level trained past it, capping at 95% — the +genre's "consolidate before you push" advice, made mechanical. A failure drops +you to 1 HP but never kills: the game has a real death system +(`global.stat.deadt`, the `ndthextr` title) and a breakthrough should not +interact with it silently. + +Each realm multiplies every stat and widens the body, up to ×6 at Ascendant. +The numbers are deliberately large, and safe to make large because section 8's +enemy model measures the player rather than assuming a curve — a jump is +absorbed as tougher enemies rather than as a broken game. + +The bonus is applied **in `allbuff`**, never written into `you.stra` and +friends, so it cannot compound across loads. `tests/cultivation.mjs` runs twenty +consecutive `allbuff` calls and checks the number does not move. + +### Elements you can use + +The six Absorption skills were purely defensive — trained by being hit by an +element, reducing what it does to you, with no way to use one. Six Mastery +skills pair with them. + +Combat is automatic (`fght` calls `battle_ai`; there is no per-turn ability +picker to hang a spell button on), so a technique is a **proc**: `you.battle_ai` +is wrapped and each swing has a chance to come out as a technique instead of a +weapon blow, at `0.006 × mastery level + 0.02 × realm`, capped at 45% so a +weapon never becomes decoration. + +The technique is an `Ability` with `stt: 2` and `aff` set to its element, which +routes it through the **INT branch** of `dmg_calc` — scaling with INT, with +`you.aff[element]`, and against the target's elemental defence rather than with +STR and a weapon. A genuinely different attack, not a reskinned one. + +Nothing fires at Mortal. You cannot throw fire before your channels are open, +which is the genre's rule and also a reason for the realm to matter in combat +rather than only on the character sheet. + +Two things worth keeping: the wrapped `battle_ai` swallows any error from the +proc and falls through to the ordinary attack, so a bug there can never cost you +a swing (the test forces `MOD_pickTechnique` to throw and checks all 20 swings +still land). And the six skills had to be **registered with the maps sections 10 +and 11 built before they existed** — `MOD_KEY_BY_ID` for the cap system, +`MOD_PARENT_OF` for the panel grouping. `rnwn` needed the same in section 17. +A skill created late is invisible to those maps, and nothing complains. + ## The dojo's Level Advancement, carried to 110 The dojo already has a reward ladder and the instructor already promises it diff --git a/README.md b/README.md index 35d1123..397b995 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,15 @@ for every one of them, including the reasoning and what was measured, is in - 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. +**Cultivation** + +- Seven realms — Mortal through Ascendant — advanced by breaking through a + bottleneck, which costs a pill and can fail. Each is worth a compounding + multiplier on every stat. +- Six elemental mastery skills whose techniques fire during combat, scaling + with INT and their element rather than with your weapon. Nothing fires until + your channels are open. + **Skills, titles and items** - Four new skills with paired actions, each earned from a milestone on the diff --git a/changelog/mod-changelog.html b/changelog/mod-changelog.html index 1328e8a..05ac084 100644 --- a/changelog/mod-changelog.html +++ b/changelog/mod-changelog.html @@ -18,6 +18,27 @@ Remove the <script src="mod.js"> line from index.html and none of it applies.

+----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:
+  Mortal → Qi Refining → Foundation Establishment → Core Formation →
+  Nascent Soul → Soul Transformation → Ascendant
+  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 Ascendant
+  Six 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
diff --git a/mod.js b/mod.js index d8ae851..e3e8bb1 100644 --- a/mod.js +++ b/mod.js @@ -31,7 +31,7 @@ console.log('[mod] loading'); var MOD = { - version: '2.4', // v2.0: the ~100 "discovered by playing" skills consolidated + version: '2.6', // v2.0: the ~100 "discovered by playing" skills consolidated // to 10; per-stat effect budget unchanged. Survivors keep // their v1 id, so v1 saves still LOAD — merged-away skills // just don't restore. See "Balance, sixth pass" in @@ -49,6 +49,10 @@ var MOD = { // v2.4: the dojo's Level Advancement ladder carried to // character level 110, with spirit pills and skillbooks // scaled to match. + // v2.5: the mod's changelog split into its own file, so + // the script tag is the only edit to the author's files. + // v2.6: cultivation realms with breakthroughs, and six + // elemental mastery skills whose techniques fire in combat. // Changes are listed in changelog/changelog.html. speed_key: 'p23_mod_speed', skill_xp_mult: 2, // change with setSkillXp(n) @@ -5834,3 +5838,395 @@ console.log('[mod] dojo advancement extended to level ' + MOD_DOJO_RUNGS[MOD_DOJO_RUNGS.length - 1].lv + ' (' + MOD_DOJO_RUNGS.length + ' rungs), ' + MOD_PILLS.length + ' spirit pill grades, ' + MOD_MANUAL_ITEMS.length + ' master manuals. modDojo() for the ladder.'); + + +/* =========================================================================== + 28. CULTIVATION REALMS + --------------------------------------------------------------------------- + proto23 is already a cultivation game in its furniture — Qi Circulation, + spirit pills "made from condensed Ki", a dojo, meridians in the skill text — + without ever having the thing that genre is actually about: a realm you + advance through, a bottleneck that stops you, and a breakthrough that costs + something. + + The ladder is the standard one. It is a genre convention rather than any + particular author's invention, which is why every xianxia uses it: + + 0 Mortal + 1 Qi Refining Qi Circulation 10 + 2 Foundation Establishment 25 + 3 Core Formation 45 + 4 Nascent Soul 65 + 5 Soul Transformation 85 + 6 Ascendant 110 + + --- the bottleneck is the point ------------------------------------------ + + Reaching the level does NOT advance the realm. It puts you AT a bottleneck, + and breaking through costs a pill you have to have found or bought. That is + the whole shape of the genre: the wall, the resource, the attempt. Without + it a realm is just a second name for a level. + + A failed attempt costs the pill and drops you to 1 HP but never kills — the + game has a death system with real consequences (`global.stat.deadt`, the + `ndthextr` title) and a breakthrough should not silently interact with it. + + --- what a realm is worth ------------------------------------------------- + + Each realm multiplies every stat, compounding, and widens what your body can + hold: max HP and max energy scale with it too. The numbers are deliberately + large — this is the axis the genre cares about, and section 8's enemy model + measures the player rather than assuming a curve, so a big jump is absorbed + as tougher enemies rather than as a broken game. + + Stored in `global.flags.mod_realm`, which is saved. The bonus is applied + through allbuff like every other multiplier, never written into `you.stra` + and friends, so it cannot compound across loads. + =========================================================================== */ + +var MOD_REALMS = [ + { n: 0, name: 'Mortal', qic: 0, mult: 1.00, hp: 1.00, + desc: 'You breathe. That is all it is, for now.' }, + { n: 1, name: 'Qi Refining', qic: 10, mult: 1.20, hp: 1.30, + desc: 'Qi moves where you tell it to, mostly.' }, + { n: 2, name: 'Foundation Establishment', qic: 25, mult: 1.55, hp: 1.80, + desc: 'The channels are yours now, and they hold what you put in them.' }, + { n: 3, name: 'Core Formation', qic: 45, mult: 2.10, hp: 2.60, + desc: 'Something in your dantian has condensed and turned, and it does not stop turning.' }, + { n: 4, name: 'Nascent Soul', qic: 65, mult: 3.00, hp: 3.80, + desc: 'There is a second you in there, small and awake.' }, + { n: 5, name: 'Soul Transformation', qic: 85, mult: 4.30, hp: 5.50, + desc: 'The body has stopped being the part of you that matters.' }, + { n: 6, name: 'Ascendant', qic: 110, mult: 6.00, hp: 8.00, + desc: 'You are still standing in the same village. Nothing else is the same.' } +]; + +MOD.realm_flag = 'mod_realm'; + +function MOD_realm() { + var n = Number(global.flags[MOD.realm_flag]) || 0; + return MOD_REALMS[Math.min(Math.max(n, 0), MOD_REALMS.length - 1)]; +} + +/* The realm you could reach, if you had the pill and the nerve. */ +function MOD_realmEligible() { + var lv = skl.qic ? skl.qic.lvl : 0, best = 0; + for (var i = 0; i < MOD_REALMS.length; i++) if (lv >= MOD_REALMS[i].qic) best = i; + return MOD_REALMS[best]; +} + +function MOD_atBottleneck() { + return MOD_realmEligible().n > MOD_realm().n; +} + +/* --- the breakthrough pills ---------------------------------------------- + One per realm above Mortal. Deliberately not craftable and not cheap: the + resource is the obstacle, and a breakthrough you can buy in bulk is not a + bottleneck. + ------------------------------------------------------------------------- */ + +var MOD_BREAK_PILLS = [ + [1, 9120, 'Qi Gathering Pill', 'Coarse, bitter, and enough to force the first channels open.'], + [2, 9121, 'Foundation Pill', 'Sets what you have built, so that it stops moving when you do.'], + [3, 9122, 'Core Condensing Pill', 'Meant to be swallowed whole. It is not meant to be pleasant.'], + [4, 9123, 'Soul Nascence Pill', 'Alchemists argue about whether making this one is permitted.'], + [5, 9124, 'Soul Transformation Pill','The recipe is older than the village and shorter than a sentence.'], + [6, 9125, 'Ascension Pill', 'There is no record of who made the first one, or of what happened to them.'] +]; + +MOD_BREAK_PILLS.forEach(function (b) { + var realm = b[0], id = b[1], name = b[2], flavour = b[3]; + var it = new Item(); it.id = id; + it.name = name; + it.rar = Math.min(realm + 2, 8); + it.desc = flavour + dom.dseparator + + 'Breaks through to ' + MOD_REALMS[realm].name + '
' + + 'Needs Qi Circulation ' + MOD_REALMS[realm].qic + + ' and the realm below it'; + it.stype = 4; + it.v = 200 * Math.pow(4, realm); + it.use = function () { MOD_breakthrough(realm, this); }; + item['mod_bp' + realm] = it; +}); + +/* --- the attempt ---------------------------------------------------------- */ + +function MOD_breakthrough(realm, pill) { + var here = MOD_realm(), want = MOD_REALMS[realm]; + if (!want) return; + + if (realm !== here.n + 1) { + msg(realm <= here.n ? 'You are already past that' : 'There is a realm between you and that one', 'red'); + return; + } + if (skl.qic.lvl < want.qic) { + msg('Your qi is too thin for it — Qi Circulation ' + want.qic + + ' (you are ' + skl.qic.lvl + ')', 'red'); + return; + } + if (global.flags.btl) { msg('Not in the middle of a fight', 'red'); return; } + + if (pill) pill.amount--; + + /* The attempt itself. Failure is possible and costs the pill, because a + breakthrough you cannot fail is an inventory transaction. The odds improve + with how far past the requirement you are — the genre's "consolidate first" + advice, made mechanical. */ + var over = skl.qic.lvl - want.qic; + var odds = Math.min(0.55 + over * 0.05, 0.95); + if (random() > odds) { + msg('The qi turns back on you. ' + want.name + ' does not open.', 'crimson'); + msg('You lose the pill, and most of what you had left.', 'grey'); + you.hp = 1; + you.stat_r(); + return false; + } + + global.flags[MOD.realm_flag] = realm; + you.stat_r(); + try { allbuff(you); } catch (e) {} + + msg('— ' + want.name + ' —', 'gold'); + msg(want.desc, 'plum'); + try { if (ttl['mod_realm' + realm]) giveTitle(ttl['mod_realm' + realm]); } catch (e) {} + try { MOD_updateRenown(); } catch (e) {} + return true; +} + +/* A title per realm, so the ladder shows in the places titles already show. + Ranked by the Qi Circulation level each needs, the way section 24 ranks + everything else. */ +MOD_REALMS.forEach(function (r) { + if (!r.n) return; + var t = new Title(3900 + r.n); + t.name = r.name; + t.desc = r.desc; + t.rar = MOD_rankForLevel(r.qic); + t._modRanked = true; + t._modAtLevel = r.qic; + ttl['mod_realm' + r.n] = t; +}); + +/* --- the bonus ------------------------------------------------------------ + Applied in allbuff, never written into the stat fields, so it cannot + accumulate across loads the way a milestone bonus would. + ------------------------------------------------------------------------- */ + +var MOD_allbuff_before_realm = allbuff; + +allbuff = function (who) { + MOD_allbuff_before_realm(who); + try { + if (!who || typeof you === 'undefined' || who.id !== you.id) return; + var r = MOD_realm(); + if (r.n === 0) return; + you.str *= r.mult; you.int *= r.mult; you.agl *= r.mult; you.spd *= r.mult; + you.hpmax = Math.round(you.hpmax * r.hp); + you.satmax = Math.round(you.satmax * r.hp); + if (you.hp > you.hpmax) you.hp = you.hpmax; + } catch (e) { /* never break a stat refresh */ } +}; + +function modRealm() { + var here = MOD_realm(), next = MOD_REALMS[here.n + 1]; + var lines = ['Realm: ' + here.name + (here.n ? ' (all stats x' + here.mult + + ', body x' + here.hp + ')' : ''), '']; + MOD_REALMS.forEach(function (r) { + var mark = r.n === here.n ? ' <- you' : + (r.n === here.n + 1 && skl.qic.lvl >= r.qic ? ' <- open, needs ' + item['mod_bp' + r.n].name : ''); + lines.push(' ' + (r.n <= here.n ? '[x] ' : '[ ] ') + String(r.n) + ' ' + + r.name.padEnd(26) + ' Qi Circulation ' + String(r.qic).padStart(3) + mark); + }); + lines.push('', 'Qi Circulation is at ' + (skl.qic ? skl.qic.lvl : 0) + '.'); + if (next && skl.qic.lvl >= next.qic) { + var over = skl.qic.lvl - next.qic; + lines.push('You are at a bottleneck. Breaking through succeeds ' + + Math.round(Math.min(0.55 + over * 0.05, 0.95) * 100) + '% of the time at this level;' + + ' train Qi Circulation past ' + next.qic + ' to improve it.'); + } + var out = lines.join('\n'); + console.log(out); + return out; +} + +console.log('[mod] cultivation realms: ' + (MOD_REALMS.length - 1) + ' above mortal, ' + + 'currently ' + MOD_realm().name + '. modRealm() for the ladder.'); + + +/* =========================================================================== + 29. ELEMENTAL MASTERY + --------------------------------------------------------------------------- + The game has six Absorption skills — Fire, Water, Air, Earth, Light, Dark — + and they are entirely defensive: they train when you are hit by that element + and reduce what it does to you. There is no way to USE an element. For a + game with meridians in its skill descriptions that is a conspicuous hole. + + Six Mastery skills fill it, one per element, each paired with the Absorption + it shares a channel with. + + --- how a technique lands ------------------------------------------------ + + Combat is automatic — `fght` calls `battle_ai`, there is no per-turn ability + picker to hang a spell button on. So a technique is a PROC: `you.battle_ai` + is wrapped, and each swing has a chance to come out as a technique instead + of a weapon blow. + + That chance is the mastery level and the realm together, and it is capped + well under 1 so a weapon never becomes decoration. + + The technique itself is an `Ability` with `stt: 2` and `aff` set to its + element, which routes it through the INT branch of dmg_calc — so it scales + with INT, with `you.aff[element]`, and with the elemental defence of what + you are hitting, rather than with STR and your weapon. It is a genuinely + different attack, not a reskinned one. + + --- gated on the realm --------------------------------------------------- + + Nothing procs at Mortal. You cannot throw fire before your channels are + open, which is both the genre's rule and a reason for section 28 to matter + in combat rather than only on the character sheet. + =========================================================================== */ + +/* element index as dmg_calc's own switch uses it: + 1 air, 2 earth, 3 fire, 4 water, 5 light, 6 dark */ +var MOD_ELEMENTS = [ + ['fire', 'Fire Mastery', 3, 'abf', 2001, 'Lantern Splitting Palm', + 'Heat with somewhere to be', 'orangered'], + ['water', 'Water Mastery', 4, 'abw', 2002, 'Still Water Draw', + 'Water does not hurry and does not stop', 'deepskyblue'], + ['air', 'Air Mastery', 1, 'aba', 2003, 'Hollow Gale', + 'The space a blow travels through, turned against it', 'lightcyan'], + ['earth', 'Earth Mastery', 2, 'abe', 2004, 'Settling Weight', + 'Everything is heavier than it wants to be', 'sandybrown'], + ['light', 'Light Mastery', 5, 'abl', 2005, 'Noon Without Shade', + 'Light with nothing merciful in it', 'lightyellow'], + ['dark', 'Dark Mastery', 6, 'abd', 2006, 'Lamp Going Out', + 'What is left when the light is taken away', 'mediumpurple'] +]; + +var MOD_MASTERY = []; // {key, skill, elem, techKey} + +MOD_ELEMENTS.forEach(function (e, i) { + var key = e[0], label = e[1], elem = e[2], absorb = e[3], + id = e[4], techName = e[5], flavour = e[6], colour = e[7]; + + var sk = new Skill(); + sk.id = 2010 + i; // clear of the mod's other ranges + sk.type = 7; // the absorptions' own type + sk.name = label; + sk.desc = flavour + MOD_SEP + + 'Your strikes sometimes become "' + techName + '"'; + sk.mlstn = []; // section 22 fills the ladder in below + skl['mod_' + key] = sk; + + var tech = new Ability(id); + tech.name = techName; + tech.stt = 2; // INT branch of dmg_calc + tech.aff = elem; + tech.atrg = ' ' + techName + ' -> '; + abl['mod_' + key] = tech; + + MOD_MASTERY.push({ key: 'mod_' + key, skill: sk, elem: elem, tech: tech, + absorb: absorb, colour: colour, name: techName }); +}); + +/* Give the six the same perk ladder every other skill got in section 22, so + they are not the only skills in the game with nothing to reach for. */ +MOD_MASTERY.forEach(function (m) { + var add = []; + for (var i = 0; i < MOD_LADDER.length; i++) { + var perk = MOD_ladderPerk(m.key, m.skill, i); + if (perk) add.push(perk); + } + if (add.length) MOD_addMilestones(m.skill, add); +}); + +/* Register the six with the machinery sections 10 and 11 built before they + existed — the id->key map the cap system reads, and the section/parent + grouping the skill panel draws from. `rnwn` needed exactly this in section 17 + for the same reason: a skill created after those maps are built is invisible + to them. */ +MOD_MASTERY.forEach(function (m) { + try { + MOD_KEY_BY_ID[m.skill.id] = m.key; + MOD_PARENT_OF[m.key] = MOD_PARENT_KEY[MOD_sectionOf(m.skill)]; + } catch (e) { console.warn('[mod] could not register ' + m.key + ': ' + e.message); } +}); + +/* --- the proc ------------------------------------------------------------- + Chance rises with the mastery and with the realm, and is capped so a weapon + always matters. Ties go to the highest-levelled mastery rather than to + whichever happens to be first in the list. + ------------------------------------------------------------------------- */ + +var MOD_TECH = { + perLevel: 0.006, // per mastery level + perRealm: 0.02, // per realm above Mortal + cap: 0.45, // no single element ever exceeds this + power: 1.15 // techniques hit slightly above a plain swing +}; + +function MOD_techChance(m) { + var realm = MOD_realm().n; + if (realm < 1) return 0; // channels are not open + if (!m.skill.lvl) return 0; + return Math.min(m.skill.lvl * MOD_TECH.perLevel + realm * MOD_TECH.perRealm, MOD_TECH.cap); +} + +/* Pick a technique for this swing, or null for an ordinary attack. */ +function MOD_pickTechnique() { + var best = null, bestChance = 0; + for (var i = 0; i < MOD_MASTERY.length; i++) { + var c = MOD_techChance(MOD_MASTERY[i]); + if (c <= 0) continue; + if (random() < c && c > bestChance) { best = MOD_MASTERY[i]; bestChance = c; } + } + return best; +} + +var MOD_battle_ai_original = you.battle_ai; + +you.battle_ai = function (x, y, z) { + try { + var m = MOD_pickTechnique(); + if (m) { + giveSkExp(m.skill, 1.2); + if (skl[m.absorb]) giveSkExp(skl[m.absorb], 0.3); // the paired channel + if (skl.qic) giveSkExp(skl.qic, 0.4); + return attack(x, y, m.tech, MOD_TECH.power); + } + } catch (e) { /* a failed proc must never cost you the swing */ } + return MOD_battle_ai_original.call(this, x, y, z); +}; + +/* Live effect lines, the way section 5 does for everything else. */ +MOD_MASTERY.forEach(function (m) { + MOD_EFFECTS[m.key] = function (s) { + var c = MOD_techChance(m); + if (MOD_realm().n < 1) { + return 'Your channels are closed — no technique will fire ' + + 'until Qi Refining'; + } + return '"' + m.name + '" fires on ' + Math.round(c * 100) + '% of your strikes' + + ' (' + Math.round(MOD_TECH.cap * 100) + '% cap)' + + '
Scales with INT and ' + + (skl[m.absorb] ? skl[m.absorb].name : 'its element') + ', not with your weapon'; + }; +}); + +function modMastery() { + var realm = MOD_realm(); + var lines = ['Elemental mastery (realm: ' + realm.name + '):']; + if (realm.n < 1) lines.push(' Nothing fires below Qi Refining.'); + MOD_MASTERY.forEach(function (m) { + lines.push(' ' + m.skill.name.padEnd(16) + ' lv ' + String(m.skill.lvl).padStart(3) + + ' "' + m.name + '"'.padEnd(24) + + ' ' + Math.round(MOD_techChance(m) * 100) + '% of strikes'); + }); + var out = lines.join('\n'); + console.log(out); + return out; +} + +console.log('[mod] elemental mastery: ' + MOD_MASTERY.length + + ' skills with techniques that fire in combat. modMastery() for the rates.'); diff --git a/tests/README.md b/tests/README.md index db44e78..345869f 100644 --- a/tests/README.md +++ b/tests/README.md @@ -44,6 +44,7 @@ a specific binary instead of Playwright's own. | `fightsmoke.mjs` | End-to-end: real battles through the game's own `attack()`, nine per area, so the wrapper is exercised with the miss roll, the dodge check, the per-swing `allbuff` and death handling rather than by calling `dmg_calc` directly. Fails if a fight is lost, never resolves, or turns into a slog. Note that it re-asserts `global.flags.btl` every swing — the page's own game loop will otherwise drop you out of battle state and the duel stalls silently. | | `fullbal.mjs` | Wider version: combat under neutral *and* favourable conditions, where player power comes from (vanilla skills vs. added ones vs. Renown vs. situational buffs), exp pacing, per-kill economy, and a dominance check for whether any single skill carries everything. | | `titles.mjs` | Title ranks and what wearing one does: every skill grants at least four, all ten ranks populated, every rank 10 earned at level 110, nothing rank 3+ earned below level 18 (the base game had eight — Runner at 10, Rookie at 15), renown 10 near-completion, a title inert until worn or until renown covers its rank, and — the one that matters — the exp bonus writes to `skl..p` which IS saved, so it is checked against stacking over thirty ticks and across a reload. | +| `cultivation.mjs` | Realms and elemental mastery: a new character is Mortal, reaching the Qi Circulation level puts you at a bottleneck rather than advancing you, realms cannot be skipped or entered under-levelled, the pill is spent on failure as well as success, consolidating past the requirement improves the odds, a realm is worth a real multiplier and does not compound over repeated `allbuff` calls, no technique fires below Qi Refining, and techniques reach the real attack path at the advertised rate through the INT branch of `dmg_calc`. Also checks a throwing proc never costs you the swing. | | `dojo.mjs` | The Level Advancement ladder past level 30: pills sized against what a level actually costs (`4*lvl^3 + lvl^2`, so 5.3M at level 110 — the game's best pill is 0.3% of one), the continuation hidden until the base six rungs are claimed and off every one-time screen, the base lobby left intact, one rung at a time in order and gated on level, claiming pays coin and pills, the 50/75/100 rungs offer a manual choice and are not claimed until one is picked, and the ladder stops at 110. | | `titlepicker.mjs` | The picker groups by skill: many titles collapse to few rows, the header is the best held rather than the first earned, base-game titles group with the generated ones, the caret opens and closes without selecting, other groups stay shut, selecting from inside a group wears it and closes the window, and story titles stay loose. | | `perkcoverage.mjs` | Every skill has perks all the way up the ladder it can now climb: none without perks, none topping out below the highest story cap, levels ascending everywhere (the save keys "granted" flags by array index), no perk text blank, and no gap over 40 levels. Before section 22, 81 of 94 skills gave nothing between level 51 and 110. | diff --git a/tests/cultivation.mjs b/tests/cultivation.mjs new file mode 100644 index 0000000..839f1e1 --- /dev/null +++ b/tests/cultivation.mjs @@ -0,0 +1,167 @@ +import { chromium } from 'playwright'; + +// Cultivation realms and elemental mastery. +// +// The realm ladder is the genre's standard one (Qi Refining -> Foundation +// Establishment -> Core Formation -> ...), and the point of it is the +// BOTTLENECK: reaching the level does not advance the realm, it puts you at a +// wall that costs a pill to break. A realm you get for free is just a second +// name for a level. +// +// The masteries are the other half: six skills whose techniques actually fire +// in combat. Combat is automatic here -- fght calls battle_ai, there is no +// ability picker -- so a technique is a proc on the swing, and this checks it +// lands at the advertised rate through the real attack path. +// +// PORT=8080 node tests/cultivation.mjs + +const HOST = `http://127.0.0.1:${process.env.PORT || 8080}`; +const b = await chromium.launch(process.env.CHROMIUM ? { executablePath: process.env.CHROMIUM } : {}); +const p = await b.newPage(); +const errs = []; p.on('pageerror', e => errs.push(String(e).slice(0, 200))); +await p.goto(`${HOST}/index.html`, { waitUntil: 'load' }); +await p.waitForTimeout(4500); + +const fail = []; +const check = (c, what) => { if (!c) fail.push(what); console.log(` ${c ? 'ok ' : 'FAIL'} ${what}`); }; + +console.log('--- the ladder'); +const ladder = await p.evaluate(() => ({ + realms: MOD_REALMS.map(r => `${r.n} ${r.name} (Qi ${r.qic}, x${r.mult})`), + start: MOD_realm().name, + pills: MOD_BREAK_PILLS.map(b => item['mod_bp' + b[0]].name), + masteries: MOD_MASTERY.map(m => m.skill.name), + perks: MOD_MASTERY.map(m => m.skill.mlstn.length), + wired: MOD_MASTERY.every(m => MOD_KEY_BY_ID[m.skill.id] === m.key && !!MOD_PARENT_OF[m.key]) +})); +ladder.realms.forEach(r => console.log(' ' + r)); +check(ladder.start === 'Mortal', 'a new character starts Mortal'); +check(ladder.pills.length === 6, `one breakthrough pill per realm (${ladder.pills.length})`); +check(ladder.masteries.length === 6, `six mastery skills (${ladder.masteries.join(', ')})`); +check(ladder.perks.every(n => n === 7), 'each has the same perk ladder every other skill got'); +check(ladder.wired, 'and all six are registered with the cap and skill-panel maps'); + +console.log('\n--- reaching the level does NOT advance the realm'); +const wall = await p.evaluate(() => { + global.flags.mod_realm = 0; skl.qic.lvl = 30; + return { realm: MOD_realm().name, eligible: MOD_realmEligible().name, atWall: MOD_atBottleneck() }; +}); +check(wall.realm === 'Mortal' && wall.eligible !== 'Mortal', + `Qi Circulation 30 makes ${wall.eligible} reachable but you are still ${wall.realm}`); +check(wall.atWall, 'the bottleneck is reported'); + +console.log('\n--- you cannot skip a realm, or break through under-levelled'); +const rules = await p.evaluate(() => { + global.flags.mod_realm = 0; skl.qic.lvl = 30; + const skip = MOD_breakthrough(3, { amount: 1 }); // two realms ahead + global.flags.mod_realm = 0; skl.qic.lvl = 5; + const under = MOD_breakthrough(1, { amount: 1 }); // needs Qi 10 + return { skip, under, realm: MOD_realm().name }; +}); +check(rules.skip === undefined && rules.under === undefined, 'both attempts refused'); +check(rules.realm === 'Mortal', 'and the realm did not move'); + +console.log('\n--- breaking through costs the pill whether or not it works'); +const attempt = await p.evaluate(() => { + global.flags.mod_realm = 0; skl.qic.lvl = 10; + let consumed = 0, wins = 0, losses = 0; + for (let i = 0; i < 200; i++) { + global.flags.mod_realm = 0; + const pill = { amount: 1 }; + const r = MOD_breakthrough(1, pill); + if (pill.amount === 0) consumed++; + r ? wins++ : losses++; + } + return { consumed, wins, losses, odds: Math.min(0.55 + 0 * 0.05, 0.95) }; +}); +check(attempt.consumed === 200, `the pill is spent every time (${attempt.consumed}/200)`); +check(attempt.losses > 0, `failure is possible (${attempt.losses} of 200 failed)`); +check(Math.abs(attempt.wins / 200 - attempt.odds) < 0.12, + `success ~${Math.round(attempt.odds * 100)}% at the threshold (got ${Math.round(attempt.wins / 200 * 100)}%)`); + +console.log('\n--- consolidating past the requirement improves the odds'); +const odds = await p.evaluate(() => [0, 4, 8].map(over => { + skl.qic.lvl = 10 + over; + let wins = 0; + for (let i = 0; i < 300; i++) { global.flags.mod_realm = 0; if (MOD_breakthrough(1, { amount: 1 })) wins++; } + return { over, pct: Math.round(wins / 300 * 100) }; +})); +odds.forEach(o => console.log(` Qi Circulation ${10 + o.over} (+${o.over} past): ${o.pct}%`)); +check(odds[2].pct > odds[0].pct, 'training past the wall makes it likelier'); + +console.log('\n--- a realm is worth something, and does not compound over reloads'); +const worth = await p.evaluate(() => { + global.flags.mod_realm = 0; skl.qic.lvl = 10; + you.stat_r(); allbuff(you); + const mortal = { str: you.str, hp: you.hpmax }; + global.flags.mod_realm = 3; + you.stat_r(); allbuff(you); + const core = { str: you.str, hp: you.hpmax }; + for (let i = 0; i < 20; i++) allbuff(you); // many refreshes + const after = { str: you.str, hp: you.hpmax }; + return { strX: +(core.str / mortal.str).toFixed(2), hpX: +(core.hp / mortal.hp).toFixed(2), + stable: Math.abs(after.str - core.str) < 1e-6 && after.hp === core.hp }; +}); +check(worth.strX > 1.9, `Core Formation is worth x${worth.strX} STR and x${worth.hpX} HP`); +check(worth.stable, 'twenty allbuff refreshes do not stack it'); + +console.log('\n--- no technique fires below Qi Refining'); +const closed = await p.evaluate(() => { + global.flags.mod_realm = 0; + MOD_MASTERY.forEach(m => { m.skill.lvl = 60; }); + return { chances: MOD_MASTERY.map(m => MOD_techChance(m)), desc: MOD_EFFECTS.mod_fire(skl.mod_fire) }; +}); +check(closed.chances.every(c => c === 0), 'every technique is inert at Mortal'); +check(/channels are closed/.test(closed.desc), 'and the skill says why'); + +console.log('\n--- techniques fire in real combat at the advertised rate'); +const combat = await p.evaluate(() => { + global.flags.mod_realm = 2; + MOD_MASTERY.forEach(m => { m.skill.lvl = 0; }); + skl.mod_fire.lvl = 40; + const expected = MOD_techChance(MOD_MASTERY[0]); + global.current_z = area.frstn1a2; + const e = area.frstn1a2.pop[0]; + const mon = mon_gen(e.crt); lvlup(mon, 5); + global.current_m = mon; global.target = you.eqp[2]; + global.flags.m_blh = true; + let procs = 0, swings = 0; + const orig = attack; + attack = function (x, y, atk, pow) { + swings++; + if (atk && atk.id >= 2001 && atk.id <= 2006) procs++; + return orig(x, y, atk, pow); + }; + for (let i = 0; i < 600; i++) { + mon.hp = mon.hpmax; you.hp = you.hpmax; global.flags.btl = true; + you.battle_ai(you, mon); + } + attack = orig; global.flags.btl = false; global.flags.m_blh = false; + return { swings, procs, rate: procs / swings, expected, + statType: abl.mod_fire.stt, element: abl.mod_fire.aff }; +}); +console.log(` ${combat.procs} of ${combat.swings} swings became a technique ` + + `(${Math.round(combat.rate * 100)}%, advertised ${Math.round(combat.expected * 100)}%)`); +check(combat.procs > 0, 'techniques actually reach the real attack path'); +check(Math.abs(combat.rate - combat.expected) < 0.07, 'at close to the advertised rate'); +check(combat.statType === 2, 'and route through the INT branch of dmg_calc, not the weapon one'); + +console.log('\n--- a swing is never lost to a broken proc'); +const safe = await p.evaluate(() => { + const orig = MOD_pickTechnique; + MOD_pickTechnique = function () { throw new Error('deliberate'); }; + const mon = mon_gen(area.frstn1a2.pop[0].crt); lvlup(mon, 5); + global.current_m = mon; global.target = you.eqp[2]; + global.flags.btl = true; global.flags.m_blh = true; + let landed = 0; + for (let i = 0; i < 20; i++) { mon.hp = mon.hpmax; if (you.battle_ai(you, mon) !== undefined) landed++; } + MOD_pickTechnique = orig; global.flags.btl = false; global.flags.m_blh = false; + return landed; +}); +check(safe === 20, `all 20 swings still landed with the proc throwing (${safe})`); + +console.log('\nerrors:', errs.length ? errs : 'none'); +if (errs.length) fail.push('page errors: ' + JSON.stringify(errs)); +await b.close(); +if (fail.length) { console.error('\nFAIL:\n - ' + fail.join('\n - ')); process.exit(1); } +console.log('\nPASS — realms gate on a bottleneck that costs something, and techniques land in combat.'); diff --git a/tests/run.sh b/tests/run.sh index 88f5c8c..e231c55 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -19,7 +19,7 @@ fi if [ "$#" -gt 0 ]; then TESTS=("$@") else - TESTS=(audit audit2 earlybal combat allareas fightsmoke fullbal econ settings-boxes savecompat saveslots actionlock freeactions perkcoverage titles titlepicker dojo capreach areagate) + TESTS=(audit audit2 earlybal combat allareas fightsmoke fullbal econ settings-boxes savecompat saveslots actionlock freeactions perkcoverage titles titlepicker dojo cultivation capreach areagate) fi echo "serving $ROOT on port $PORT" From c10491716ab6843984177fb453304ec788d695cf Mon Sep 17 00:00:00 2001 From: Xeus24 <201404920+Xeus24@users.noreply.github.com> Date: Tue, 8 Sep 2026 21:29:02 -0700 Subject: [PATCH 15/44] Scale the realms off the title ranks Ten realms instead of six, sitting on MOD_RANK_AT -- the same thresholds section 24 derives title ranks from -- and DERIVED from it rather than listed again. Realm 3 is exactly the Qi Circulation level that earns a rank 3 title, and each realm's own title falls out at its own rank without being told to. Retune the ranks and the realms follow; there is no second copy to forget. 1 Qi Refining Qi 1 6 Soul Transformation Qi 58 2 Foundation Establishment Qi 8 7 Void Refinement Qi 70 3 Core Formation Qi 18 8 Body Integration Qi 82 4 Nascent Soul Qi 30 9 Great Ascension Qi 90 5 Spirit Severing Qi 45 10 Tribulation Transc. Qi 110 Ten breakthrough pills to match, one per realm. The multipliers were respread over ten steps rather than six, so each is a smaller jump while the top stays x6 on stats and x8.5 on the body. The technique proc's per-realm term dropped 0.02 -> 0.012, since it was tuned when there were six realms and would otherwise have eaten most of the 45% cap on realm alone. The still-extended ladder is the genre's own -- Spirit Severing, Void Refinement, Body Integration, Great Ascension, Tribulation Transcendence are as widely shared as the first five, and none of it comes from any particular work. cultivation.mjs now asserts the alignment directly (r.qic === MOD_RANK_AT[r.n-1], MOD_rankForLevel(r.qic) === r.n, and the realm title's own rar), which is what will stop the two ladders drifting next time either is touched. Four of its assertions had hardcoded "realm 1 needs Qi 10" and broke the moment the ladder was rescaled -- the same staleness in miniature -- so they read the thresholds out of MOD_REALMS now. Full suite green, 20 scripts. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 5 ++ MOD-NOTES.md | 23 +++++++- changelog/mod-changelog.html | 10 ++-- mod.js | 106 +++++++++++++++++++++++------------ tests/cultivation.mjs | 52 ++++++++++++----- 5 files changed, 140 insertions(+), 56 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 4e46788..d847449 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -156,6 +156,11 @@ What matters: ## 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. + 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 diff --git a/MOD-NOTES.md b/MOD-NOTES.md index f226fb2..b6aea14 100644 --- a/MOD-NOTES.md +++ b/MOD-NOTES.md @@ -1779,13 +1779,30 @@ stops you, and a breakthrough that costs something. ### The ladder is a genre convention, not a borrowing -Qi Refining → Foundation Establishment → Core Formation → Nascent Soul → Soul -Transformation → Ascendant. That ladder, the dantian, meridians, bottlenecks and -tribulations are shared across hundreds of works and belong to no one; they are +Qi Refining → Foundation Establishment → Core Formation → Nascent Soul → Spirit +Severing → Soul Transformation → Void Refinement → Body Integration → Great +Ascension → Tribulation Transcendence. That ladder, the dantian, meridians, +bottlenecks and tribulations are shared across hundreds of works and belong to +no one; they are what makes a thing read as xianxia. Nothing here is lifted from any particular novel — no names, no settings, no text — which is both the legal answer and the right one, since borrowed specifics would sit badly against proto23's own world. +### Scaled off the title ranks, not restated + +The ten realms sit on `MOD_RANK_AT` — the same thresholds section 24 derives +title ranks from — and are **derived from it rather than listed again**. Realm 3 +is exactly the Qi Circulation level that earns a rank 3 title, and each realm's +own title falls out at its own rank without being told to. Retune the rank +thresholds and the realms follow; there is no second copy to forget. + +`tests/cultivation.mjs` asserts that alignment directly (`r.qic === +MOD_RANK_AT[r.n-1]`, `MOD_rankForLevel(r.qic) === r.n`, and the realm title's +own `rar`), which is what stops the two ladders drifting the next time either is +touched. The test's other assertions read the thresholds out of `MOD_REALMS` +too — the first version hardcoded "realm 1 needs Qi 10" and broke the moment the +ladder was rescaled, which is the same staleness in miniature. + ### The bottleneck is the whole point Reaching the Qi Circulation level does **not** advance the realm. It puts you at diff --git a/changelog/mod-changelog.html b/changelog/mod-changelog.html index 05ac084..30773e1 100644 --- a/changelog/mod-changelog.html +++ b/changelog/mod-changelog.html @@ -22,12 +22,14 @@ [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:
-  Mortal → Qi Refining → Foundation Establishment → Core Formation →
-  Nascent Soul → Soul Transformation → Ascendant
+  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 Ascendant
-  Six breakthrough pills, one per realm, and a title for each realm reached
+  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
diff --git a/mod.js b/mod.js index e3e8bb1..f380d57 100644 --- a/mod.js +++ b/mod.js @@ -5849,16 +5849,27 @@ console.log('[mod] dojo advancement extended to level ' + advance through, a bottleneck that stops you, and a breakthrough that costs something. - The ladder is the standard one. It is a genre convention rather than any - particular author's invention, which is why every xianxia uses it: - - 0 Mortal - 1 Qi Refining Qi Circulation 10 - 2 Foundation Establishment 25 - 3 Core Formation 45 - 4 Nascent Soul 65 - 5 Soul Transformation 85 - 6 Ascendant 110 + The ladder is the genre's extended one — a convention rather than any + particular author's invention, which is why every xianxia uses some version + of it. Ten stages, and the thresholds are NOT written out here: they are + MOD_RANK_AT, the same levels section 24 derives title ranks from. Realm 3 is + exactly the Qi Circulation level that earns a rank 3 title, and each realm's + own title lands at its own rank without being told to. + + 0 Mortal — + 1 Qi Refining Qi Circulation 1 + 2 Foundation Establishment 8 + 3 Core Formation 18 + 4 Nascent Soul 30 + 5 Spirit Severing 45 + 6 Soul Transformation 58 + 7 Void Refinement 70 + 8 Body Integration 82 + 9 Great Ascension 90 + 10 Tribulation Transcendence 110 + + Deriving rather than restating means the two ladders cannot drift: retune + MOD_RANK_AT and the realms follow. --- the bottleneck is the point ------------------------------------------ @@ -5884,23 +5895,40 @@ console.log('[mod] dojo advancement extended to level ' + and friends, so it cannot compound across loads. =========================================================================== */ -var MOD_REALMS = [ - { n: 0, name: 'Mortal', qic: 0, mult: 1.00, hp: 1.00, - desc: 'You breathe. That is all it is, for now.' }, - { n: 1, name: 'Qi Refining', qic: 10, mult: 1.20, hp: 1.30, - desc: 'Qi moves where you tell it to, mostly.' }, - { n: 2, name: 'Foundation Establishment', qic: 25, mult: 1.55, hp: 1.80, - desc: 'The channels are yours now, and they hold what you put in them.' }, - { n: 3, name: 'Core Formation', qic: 45, mult: 2.10, hp: 2.60, - desc: 'Something in your dantian has condensed and turned, and it does not stop turning.' }, - { n: 4, name: 'Nascent Soul', qic: 65, mult: 3.00, hp: 3.80, - desc: 'There is a second you in there, small and awake.' }, - { n: 5, name: 'Soul Transformation', qic: 85, mult: 4.30, hp: 5.50, - desc: 'The body has stopped being the part of you that matters.' }, - { n: 6, name: 'Ascendant', qic: 110, mult: 6.00, hp: 8.00, - desc: 'You are still standing in the same village. Nothing else is the same.' } +/* One realm per title rank, at the same thresholds — DERIVED from MOD_RANK_AT + rather than restated, so the two ladders cannot drift apart. Realm 3 is + exactly the Qi Circulation level that earns a rank 3 title, and each realm's + own title comes out at its own rank without being told to. + + The ten-stage ladder is the genre's extended one, the same way the six-stage + version was: shared across hundreds of works, invented by none of them. */ +var MOD_REALM_DATA = [ + ['Qi Refining', 1.10, 1.20, 'Qi moves where you tell it to, mostly.'], + ['Foundation Establishment', 1.30, 1.50, 'The channels are yours now, and they hold what you put in them.'], + ['Core Formation', 1.55, 1.90, 'Something in your dantian has condensed and turned, and it does not stop turning.'], + ['Nascent Soul', 1.85, 2.40, 'There is a second you in there, small and awake.'], + ['Spirit Severing', 2.20, 3.00, 'You have cut something loose that most people carry their whole lives.'], + ['Soul Transformation', 2.65, 3.80, 'The body has stopped being the part of you that matters.'], + ['Void Refinement', 3.15, 4.70, 'You have started refining the space the qi moves through, rather than the qi.'], + ['Body Integration', 3.80, 5.80, 'Flesh and spirit stop being two things you have to reconcile.'], + ['Great Ascension', 4.80, 7.00, 'There is very little left above you, and it knows your name now.'], + ['Tribulation Transcendence',6.00, 8.50, 'You are still standing in the same village. Nothing else is the same.'] ]; +var MOD_REALMS = [{ n: 0, name: 'Mortal', qic: 0, mult: 1, hp: 1, + desc: 'You breathe. That is all it is, for now.' }]; + +MOD_REALM_DATA.forEach(function (r, i) { + MOD_REALMS.push({ + n: i + 1, + name: r[0], + qic: MOD_RANK_AT[i], // the level that earns a rank i+1 title + mult: r[1], + hp: r[2], + desc: r[3] + }); +}); + MOD.realm_flag = 'mod_realm'; function MOD_realm() { @@ -5925,26 +5953,34 @@ function MOD_atBottleneck() { bottleneck. ------------------------------------------------------------------------- */ -var MOD_BREAK_PILLS = [ - [1, 9120, 'Qi Gathering Pill', 'Coarse, bitter, and enough to force the first channels open.'], - [2, 9121, 'Foundation Pill', 'Sets what you have built, so that it stops moving when you do.'], - [3, 9122, 'Core Condensing Pill', 'Meant to be swallowed whole. It is not meant to be pleasant.'], - [4, 9123, 'Soul Nascence Pill', 'Alchemists argue about whether making this one is permitted.'], - [5, 9124, 'Soul Transformation Pill','The recipe is older than the village and shorter than a sentence.'], - [6, 9125, 'Ascension Pill', 'There is no record of who made the first one, or of what happened to them.'] +var MOD_PILL_NAMES = [ + ['Qi Gathering Pill', 'Coarse, bitter, and enough to force the first channels open.'], + ['Foundation Pill', 'Sets what you have built, so it stops moving when you do.'], + ['Core Condensing Pill', 'Meant to be swallowed whole. It is not meant to be pleasant.'], + ['Soul Nascence Pill', 'Alchemists argue about whether making this one is permitted.'], + ['Severing Pill', 'Whatever it cuts away does not grow back.'], + ['Transformation Pill', 'The recipe is older than the village and shorter than a sentence.'], + ['Void Refining Pill', 'It is not clear that this one is made of anything.'], + ['Integration Pill', 'Taken by people who have already decided.'], + ['Great Ascension Pill', 'Nobody sells these. They are given, or they are taken.'], + ['Tribulation Pill', 'There is no record of who made the first one, or of what became of them.'] ]; +var MOD_BREAK_PILLS = MOD_PILL_NAMES.map(function (p, i) { + return [i + 1, 9120 + i, p[0], p[1]]; +}); + MOD_BREAK_PILLS.forEach(function (b) { var realm = b[0], id = b[1], name = b[2], flavour = b[3]; var it = new Item(); it.id = id; it.name = name; - it.rar = Math.min(realm + 2, 8); + it.rar = Math.min(realm, 10); it.desc = flavour + dom.dseparator + 'Breaks through to ' + MOD_REALMS[realm].name + '
' + 'Needs Qi Circulation ' + MOD_REALMS[realm].qic + ' and the realm below it'; it.stype = 4; - it.v = 200 * Math.pow(4, realm); + it.v = Math.round(150 * Math.pow(2.6, realm)); it.use = function () { MOD_breakthrough(realm, this); }; item['mod_bp' + realm] = it; }); @@ -6161,7 +6197,7 @@ MOD_MASTERY.forEach(function (m) { var MOD_TECH = { perLevel: 0.006, // per mastery level - perRealm: 0.02, // per realm above Mortal + perRealm: 0.012, // per realm above Mortal — 10 realms now, not 6 cap: 0.45, // no single element ever exceeds this power: 1.15 // techniques hit slightly above a plain swing }; diff --git a/tests/cultivation.mjs b/tests/cultivation.mjs index 839f1e1..6935673 100644 --- a/tests/cultivation.mjs +++ b/tests/cultivation.mjs @@ -27,7 +27,17 @@ const check = (c, what) => { if (!c) fail.push(what); console.log(` ${c ? 'ok console.log('--- the ladder'); const ladder = await p.evaluate(() => ({ + count: MOD_REALMS.length - 1, realms: MOD_REALMS.map(r => `${r.n} ${r.name} (Qi ${r.qic}, x${r.mult})`), + // the realms are DERIVED from MOD_RANK_AT, so realm N must be the level that + // earns a rank N title, and each realm's own title must land at rank N. + // Asserting it here is what stops the two ladders drifting apart later. + aligned: MOD_REALMS.slice(1).every(r => + r.qic === MOD_RANK_AT[r.n - 1] && + MOD_rankForLevel(r.qic) === r.n && + ttl['mod_realm' + r.n] && ttl['mod_realm' + r.n].rar === r.n && + !!item['mod_bp' + r.n]), + ranks: MOD_RANK_AT, start: MOD_realm().name, pills: MOD_BREAK_PILLS.map(b => item['mod_bp' + b[0]].name), masteries: MOD_MASTERY.map(m => m.skill.name), @@ -35,8 +45,14 @@ const ladder = await p.evaluate(() => ({ wired: MOD_MASTERY.every(m => MOD_KEY_BY_ID[m.skill.id] === m.key && !!MOD_PARENT_OF[m.key]) })); ladder.realms.forEach(r => console.log(' ' + r)); +const MOD_REALM_COUNT = ladder.count; +const MOD_REALMS_NAME3 = ladder.realms[3]; +check(ladder.count === ladder.ranks.length, + `one realm per title rank (${ladder.count} realms, ${ladder.ranks.length} ranks)`); +check(ladder.aligned, + 'realm N sits at the level that earns a rank N title, and its own title is rank N'); check(ladder.start === 'Mortal', 'a new character starts Mortal'); -check(ladder.pills.length === 6, `one breakthrough pill per realm (${ladder.pills.length})`); +check(ladder.pills.length === MOD_REALM_COUNT, `one breakthrough pill per realm (${ladder.pills.length})`); check(ladder.masteries.length === 6, `six mastery skills (${ladder.masteries.join(', ')})`); check(ladder.perks.every(n => n === 7), 'each has the same perk ladder every other skill got'); check(ladder.wired, 'and all six are registered with the cap and skill-panel maps'); @@ -52,18 +68,22 @@ check(wall.atWall, 'the bottleneck is reported'); console.log('\n--- you cannot skip a realm, or break through under-levelled'); const rules = await p.evaluate(() => { - global.flags.mod_realm = 0; skl.qic.lvl = 30; - const skip = MOD_breakthrough(3, { amount: 1 }); // two realms ahead - global.flags.mod_realm = 0; skl.qic.lvl = 5; - const under = MOD_breakthrough(1, { amount: 1 }); // needs Qi 10 - return { skip, under, realm: MOD_realm().name }; + // read the requirements rather than hardcoding them: the thresholds come from + // MOD_RANK_AT and change whenever the title ranks are retuned + const two = MOD_REALMS[2]; + global.flags.mod_realm = 0; skl.qic.lvl = 999; + const skip = MOD_breakthrough(3, { amount: 1 }); // realm 3 from realm 0 + global.flags.mod_realm = 1; skl.qic.lvl = two.qic - 1; // one short of realm 2 + const under = MOD_breakthrough(2, { amount: 1 }); + return { skip, under, realm: MOD_realm().name, needed: two.qic }; }); -check(rules.skip === undefined && rules.under === undefined, 'both attempts refused'); -check(rules.realm === 'Mortal', 'and the realm did not move'); +check(rules.skip === undefined, 'skipping a realm is refused'); +check(rules.under === undefined, `breaking through one level short of Qi ${rules.needed} is refused`); +check(rules.realm !== MOD_REALMS_NAME3, 'and the realm did not move'); console.log('\n--- breaking through costs the pill whether or not it works'); const attempt = await p.evaluate(() => { - global.flags.mod_realm = 0; skl.qic.lvl = 10; + global.flags.mod_realm = 0; skl.qic.lvl = MOD_REALMS[1].qic; // exactly at the wall let consumed = 0, wins = 0, losses = 0; for (let i = 0; i < 200; i++) { global.flags.mod_realm = 0; @@ -81,17 +101,17 @@ check(Math.abs(attempt.wins / 200 - attempt.odds) < 0.12, console.log('\n--- consolidating past the requirement improves the odds'); const odds = await p.evaluate(() => [0, 4, 8].map(over => { - skl.qic.lvl = 10 + over; + skl.qic.lvl = MOD_REALMS[1].qic + over; let wins = 0; for (let i = 0; i < 300; i++) { global.flags.mod_realm = 0; if (MOD_breakthrough(1, { amount: 1 })) wins++; } return { over, pct: Math.round(wins / 300 * 100) }; })); -odds.forEach(o => console.log(` Qi Circulation ${10 + o.over} (+${o.over} past): ${o.pct}%`)); +odds.forEach(o => console.log(` +${o.over} past the requirement: ${o.pct}%`)); check(odds[2].pct > odds[0].pct, 'training past the wall makes it likelier'); console.log('\n--- a realm is worth something, and does not compound over reloads'); const worth = await p.evaluate(() => { - global.flags.mod_realm = 0; skl.qic.lvl = 10; + global.flags.mod_realm = 0; skl.qic.lvl = 1; you.stat_r(); allbuff(you); const mortal = { str: you.str, hp: you.hpmax }; global.flags.mod_realm = 3; @@ -99,10 +119,14 @@ const worth = await p.evaluate(() => { const core = { str: you.str, hp: you.hpmax }; for (let i = 0; i < 20; i++) allbuff(you); // many refreshes const after = { str: you.str, hp: you.hpmax }; - return { strX: +(core.str / mortal.str).toFixed(2), hpX: +(core.hp / mortal.hp).toFixed(2), + return { name: MOD_REALMS[3].name, want: MOD_REALMS[3].mult, + strX: +(core.str / mortal.str).toFixed(2), hpX: +(core.hp / mortal.hp).toFixed(2), + top: MOD_REALMS[MOD_REALMS.length - 1].mult, stable: Math.abs(after.str - core.str) < 1e-6 && after.hp === core.hp }; }); -check(worth.strX > 1.9, `Core Formation is worth x${worth.strX} STR and x${worth.hpX} HP`); +check(Math.abs(worth.strX - worth.want) < 0.06, + `${worth.name} is worth the x${worth.want} it advertises (measured x${worth.strX} STR, x${worth.hpX} HP)`); +check(worth.top >= 5, `and the top realm is worth x${worth.top}`); check(worth.stable, 'twenty allbuff refreshes do not stack it'); console.log('\n--- no technique fires below Qi Refining'); From a5414604142cabddad95cddc9877deac62f2913b Mon Sep 17 00:00:00 2001 From: Xeus24 <201404920+Xeus24@users.noreply.github.com> Date: Tue, 8 Sep 2026 22:59:36 -0700 Subject: [PATCH 16/44] Unlock Circulate Qi from the dojo, and fix the bottom bar's height Two things. Circulate Qi used to hang off Temperance 5, like the other three added actions. That was fine until section 28 made it the way into the cultivation system -- realms are levelled through Qi Circulation -- at which point gating it behind a skill you train by throwing your possessions away is an obscure lock on the most important thing in the mod. It now comes from clearing the dojo's three tutorial fights: the difficulty select's "Easiest", "Easy" and "Normal", which set tr1_win, tr2_win and tr3_win. That is the moment the dojo has finished teaching you to fight, which is the right moment to hand over what the rest hangs off. A tick check rather than a milestone, because the condition is story flags rather than a skill level -- the same way the base game grants act.scout from a story beat. The skill is revealed on the sheet at the same moment, since the game otherwise only shows a skill once it first levels and you would hold an action whose skill is nowhere. Temperance keeps a perk at level 5, now a plain one. Removing the milestone would have shifted every later index in skl.rccln.mlstn, and those indices are what the save keys its granted flags to. Second, the bottom bar was 8px taller than it should be and covering the bottom of the panel above it. The mod's three buttons were display:inline-block where the game's own are plain inline; both render 22px tall, but only an inline-block contributes its full box to the line box. #sl is position:fixed at bottom:0, so those 8px came straight off the game screen. Measured rather than guessed -- hiding just those three buttons took the bar from 32px back to 24px -- and it is 24px either way now. actionlock.mjs gains seven checks for the new unlock, and its modUnlockAll assertion no longer depends on test order. Full suite green, 20 scripts. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 23 ++++++++--- MOD-NOTES.md | 38 ++++++++++++++++++ changelog/mod-changelog.html | 14 +++++++ mod.js | 78 ++++++++++++++++++++++++++++++++---- tests/actionlock.mjs | 56 +++++++++++++++++++++++--- 5 files changed, 190 insertions(+), 19 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d847449..d805f33 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,6 +58,13 @@ append-only. Getting this wrong corrupts existing saves. 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 @@ -245,11 +252,17 @@ his. ## Actions -The mod adds four actions. They are **earned**, from milestones on base-game -skills, the same way `skl.walk` lv 1 grants the base game's "Run": -Toughness 4 → Endurance Drill, Harvesting 4 → Forage, Temperance 5 → Circulate -Qi, Literacy 8 → Practice Calligraphy. Do not go back to granting them on a -timer. +The mod adds four actions. They are **earned**, not granted on a timer. Three +come from milestones on the base-game skill they grow out of, the same way +`skl.walk` lv 1 grants "Run": Toughness 4 → Endurance Drill, Harvesting 4 → +Forage, Literacy 8 → Practice Calligraphy. + +**Circulate Qi is the exception**: it comes from clearing the dojo's three +tutorial fights (`tr1_win`/`tr2_win`/`tr3_win` — Easiest, Easy, Normal), checked +on the tick, because the condition is story flags rather than a skill level. It +is the way into the whole cultivation system, so gating it behind Temperance — +a skill trained by discarding possessions — was obscure. Temperance keeps a +plain perk at level 5 so the save's index-keyed milestone flags do not shift. - Milestone flags are stored **by array index**, so a gating milestone must be appended and levels must stay ascending. Section 3 already appended level-50 diff --git a/MOD-NOTES.md b/MOD-NOTES.md index b6aea14..b67142e 100644 --- a/MOD-NOTES.md +++ b/MOD-NOTES.md @@ -2068,6 +2068,44 @@ problem rather than edited to fix it; it is his to fix. **Every change from here on gets an entry there.** That is the point of it. +## Circulate Qi comes from the dojo + +Three of the four added actions hang off a milestone on a base-game skill. +Circulate Qi did too — Temperance 5 — and that was wrong once section 28 made it +the way into the cultivation system: realms are levelled through Qi Circulation, +and Temperance is trained by throwing your possessions away. An obscure gate on +the most important thing in the mod. + +It is granted by clearing the dojo's three tutorial fights instead — the +difficulty select's "Easiest", "Easy" and "Normal", which set `tr1_win`, +`tr2_win` and `tr3_win`. That is the moment the dojo has finished teaching you +to fight, which is the right moment to hand over what the rest hangs off. + +A **tick check rather than a milestone**, because the condition is three story +flags rather than a skill level — the same way the base game grants `act.scout` +from a story beat rather than a perk. The flags only go false→true, so it is +one-way, and `giveAction` is a no-op once you hold it. + +The skill itself is revealed at the same time. The game only shows a skill once +it first levels, which would otherwise leave you holding an action whose skill +is nowhere on the sheet. + +**Temperance keeps a perk at level 5**, now a plain one. Removing the milestone +would have shifted every later index in `skl.rccln.mlstn`, and those indices are +what the save keys its "granted" flags to. + +## The bottom bar was covering the panel + +The mod's three bar buttons were `display:inline-block`; the game's own are +plain `inline`. Both render 22px tall, but only an inline-block contributes its +full box to the line box — so `#sl` went from 24px to 32px, and since it is +`position:fixed` at `bottom:0`, those 8px came straight off the game screen and +covered the bottom of the panel above it. + +Measured rather than guessed: hiding just the three buttons took the bar from +32px back to 24px. Matching the game's own styling fixes it; the check is that +the bar's height is identical with the buttons shown and hidden. + ## The added actions are earned now The four actions this mod adds — Circulate Qi, Forage, Practice Calligraphy, diff --git a/changelog/mod-changelog.html b/changelog/mod-changelog.html index 30773e1..6de7ae3 100644 --- a/changelog/mod-changelog.html +++ b/changelog/mod-changelog.html @@ -18,6 +18,20 @@ Remove the <script src="mod.js"> line from index.html and none of it applies.

+----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",
diff --git a/mod.js b/mod.js index f380d57..71f0597 100644 --- a/mod.js +++ b/mod.js @@ -31,7 +31,7 @@ console.log('[mod] loading'); var MOD = { - version: '2.6', // v2.0: the ~100 "discovered by playing" skills consolidated + version: '2.7', // v2.0: the ~100 "discovered by playing" skills consolidated // to 10; per-stat effect budget unchanged. Survivors keep // their v1 id, so v1 saves still LOAD — merged-away skills // just don't restore. See "Balance, sixth pass" in @@ -53,6 +53,8 @@ var MOD = { // the script tag is the only edit to the author's files. // v2.6: cultivation realms with breakthroughs, and six // elemental mastery skills whose techniques fire in combat. + // v2.7: Circulate Qi unlocked by the dojo's tutorial fights + // rather than by Temperance 5. // Changes are listed in changelog/changelog.html. speed_key: 'p23_mod_speed', skill_xp_mult: 2, // change with setSkillXp(n) @@ -604,9 +606,15 @@ var MOD_ACTION_UNLOCKS = [ { skill: 'hst', lv: 4, act: act.mod_forage, f: function () { you.inta += 1; you.stat_r(); }, p: 'INT +1, unlocks the action "Forage"' }, - { skill: 'rccln', lv: 5, act: act.mod_qi, - f: function () { you.inta += 1; you.stat_r(); }, - p: 'INT +1, unlocks the action "Circulate Qi"' }, + /* Circulate Qi used to hang off Temperance 5 like the others. It is the way + into the whole cultivation system now — realms are levelled through it — + and gating that behind a skill you train by throwing possessions away was + obscure. It is granted by finishing the dojo's three tutorial fights + instead; see MOD_QI_UNLOCK below. Temperance keeps a perk at the same + level so the save's index-keyed milestone flags do not shift. */ + { skill: 'rccln', lv: 5, act: null, + f: function () { you.inta += 1; you.mods.sbonus += 0.02; you.stat_r(); }, + p: 'INT +1, Energy Effectiveness +2%' }, { skill: 'rdg', lv: 8, act: act.mod_calli, f: function () { you.inta += 2; you.stat_r(); }, p: 'INT +2, unlocks the action "Practice Calligraphy"' } @@ -614,22 +622,76 @@ var MOD_ACTION_UNLOCKS = [ MOD_ACTION_UNLOCKS.forEach(function (u) { var sk = skl[u.skill]; - if (!sk || !u.act) { console.warn('[mod] no unlock for ' + u.skill); return; } + if (!sk) { console.warn('[mod] no skill ' + u.skill); return; } + if (!u.act) { // a plain perk, no action + MOD_addMilestones(sk, [{ lv: u.lv, g: false, p: u.p, f: u.f }]); + return; + } MOD_addMilestones(sk, [{ lv: u.lv, g: false, p: u.p, f: function () { u.f(); try { giveAction(u.act); } catch (e) {} } }]); }); +/* --- Circulate Qi, from the dojo's tutorial fights ------------------------ + The difficulty select offers "Easiest", "Easy" and "Normal" — areas trn1, + trn2 and trn3, which set tr1_win, tr2_win and tr3_win. Clearing all three is + the moment the dojo has finished teaching you to fight, which is the right + moment to hand over the thing the rest of the mod's cultivation hangs off. + + A tick check rather than a milestone, because the condition is three story + flags rather than a skill level — the same way the base game grants + act.scout from a story beat rather than a perk. The flags only ever go from + false to true, so this is one-way, and giveAction is a no-op once you have + it. + + The skill itself is revealed at the same time. The game only shows a skill + once it first levels, which would leave you holding an action whose skill is + nowhere on the sheet. */ +var MOD_QI_UNLOCK = ['tr1_win', 'tr2_win', 'tr3_win']; + +function MOD_qiUnlocked() { + for (var i = 0; i < MOD_QI_UNLOCK.length; i++) { + if (global.flags[MOD_QI_UNLOCK[i]] !== true) return false; + } + return true; +} + +function MOD_checkQiUnlock() { + try { + if (!MOD_qiUnlocked()) return; + if (act.mod_qi.have !== true) { + giveAction(act.mod_qi); + msg('The forms settle into something you can feel moving.', 'plum'); + } + if (skl.qic && you.skls && you.skls.indexOf(skl.qic) === -1) { + you.skls.push(skl.qic); + if (!global.flags.sklu) { global.flags.sklu = true; dom.ct_bt2.innerHTML = 'skills'; } + } + } catch (e) { /* never break a tick over an unlock */ } +} + +var MOD_ontick_before_qi = ontick; +ontick = function () { + MOD_ontick_before_qi(); + MOD_checkQiUnlock(); +}; + /* Where the four stand, and what is still needed. */ function modActions() { var lines = ['Added actions:']; MOD_ACTION_UNLOCKS.forEach(function (u) { + if (!u.act) return; var sk = skl[u.skill]; var have = u.act.have === true; lines.push(' ' + (have ? '[unlocked] ' : '[ locked ] ') + u.act.name + (have ? '' : ' — ' + (sk ? sk.name + ' ' + sk.lvl + '/' + u.lv : u.skill + ' ' + u.lv))); }); + var qi = act.mod_qi.have === true; + lines.push(' ' + (qi ? '[unlocked] ' : '[ locked ] ') + act.mod_qi.name + + (qi ? '' : ' — clear the dojo\'s Easiest, Easy and Normal dummies (' + + MOD_QI_UNLOCK.filter(function (f) { return global.flags[f] === true; }).length + + '/3 done)')); lines.push('modUnlockAll() grants them regardless, if you would rather not wait.'); var out = lines.join('\n'); console.log(out); @@ -4359,13 +4421,13 @@ function MOD_toggleSlotPanel(force) { // of the bar is positioned from the right and would be overlapped. var saves = addElement(dom.sl, 'span', null, 'sl'); saves.innerHTML = 'saves'; - saves.style.cssText = 'display:inline-block;width:auto;padding:3px 7px;cursor:pointer;'; + saves.style.cssText = 'width:auto;padding:3px;cursor:pointer;'; saves.title = 'Three save slots'; saves.addEventListener('click', function () { MOD_toggleSlotPanel(); }); var fresh = addElement(dom.sl, 'span', null, 'sl'); fresh.innerHTML = 'new save'; - fresh.style.cssText = 'display:inline-block;width:auto;padding:3px 7px;cursor:pointer;'; + fresh.style.cssText = 'width:auto;padding:3px;cursor:pointer;'; fresh.title = 'Start a new character in a free slot'; fresh.addEventListener('click', function () { var free = MOD_firstEmptySlot(); @@ -4934,7 +4996,7 @@ function modGameChangelog() { try { var btn = addElement(dom.sl, 'span', null, 'sl'); btn.innerHTML = 'changelog'; - btn.style.cssText = 'display:inline-block;width:auto;padding:3px 7px;cursor:pointer;'; + btn.style.cssText = 'width:auto;padding:3px;cursor:pointer;'; btn.title = "What this mod changed (the version number opens the game's own)"; btn.addEventListener('click', modChangelog); dom.sl.insertBefore(btn, dom.sl_extra); diff --git a/tests/actionlock.mjs b/tests/actionlock.mjs index 852f66b..678209a 100644 --- a/tests/actionlock.mjs +++ b/tests/actionlock.mjs @@ -4,8 +4,17 @@ import { chromium } from 'playwright'; // // They used to be granted 2.5s after startup and re-granted on a 5s timer, so a // brand-new character had four unexplained abilities before leaving the -// tutorial. Each now hangs off a milestone on the base-game skill it grows out -// of, the same way skl.walk level 1 grants the base game's "Run". +// tutorial. Three now hang off a milestone on the base-game skill they grow out +// of, the same way skl.walk level 1 grants the base game's "Run": +// +// Toughness 4 -> Endurance Drill Harvesting 4 -> Forage +// Literacy 8 -> Practice Calligraphy +// +// Circulate Qi is the exception, and deliberately so. It is the way into the +// whole cultivation system, so it comes from a story beat rather than a skill +// level: clearing the dojo's Easiest, Easy and Normal dummies (tr1/2/3_win). +// That is a tick check rather than a milestone, the same way the base game +// grants act.scout from a story beat. // // PORT=8080 node tests/actionlock.mjs @@ -20,7 +29,7 @@ const fail = []; const check = (cond, what) => { if (!cond) fail.push(what); console.log(` ${cond ? 'ok ' : 'FAIL'} ${what}`); }; const status = () => p.evaluate(() => ({ - locked: MOD_ACTION_UNLOCKS.map(u => ({ + locked: MOD_ACTION_UNLOCKS.filter(u => u.act).map(u => ({ action: u.act.name, have: u.act.have === true, skill: skl[u.skill] ? skl[u.skill].name : u.skill, key: u.skill, need: u.lv, at: skl[u.skill] ? skl[u.skill].lvl : -1 @@ -83,11 +92,46 @@ await p.waitForTimeout(4500); st = await status(); check(st.locked.every(u => !u.have), 'a fresh save reloads with all four still locked'); +console.log('\n--- Circulate Qi comes from the dojo, not from a skill level'); +const qi = await p.evaluate(() => { + const reset = () => { ['tr1_win','tr2_win','tr3_win'].forEach(f => { global.flags[f] = false; }); + act.mod_qi.have = false; + const i = you.skls.indexOf(skl.qic); if (i >= 0) you.skls.splice(i, 1); + acts = acts.filter(a => a !== act.mod_qi); }; + const st = () => ({ have: act.mod_qi.have === true, onSheet: you.skls.indexOf(skl.qic) !== -1 }); + reset(); MOD_checkQiUnlock(); const none = st(); + global.flags.tr1_win = true; MOD_checkQiUnlock(); const easiest = st(); + global.flags.tr2_win = true; MOD_checkQiUnlock(); const easy = st(); + global.flags.tr3_win = true; MOD_checkQiUnlock(); const normal = st(); + MOD_checkQiUnlock(); MOD_checkQiUnlock(); + return { none, easiest, easy, normal, copies: acts.filter(a => a === act.mod_qi).length, + // Temperance kept a perk at the same level, so the save's index-keyed + // milestone flags do not shift under an existing character + rccln5: skl.rccln.mlstn.filter(m => m.lv === 5).length, + rcclnGrantsAction: /Circulate/.test(skl.rccln.mlstn.filter(m => m.lv === 5)[0].p) }; +}); +check(!qi.none.have, 'locked with none of the three cleared'); +check(!qi.easiest.have, 'still locked after Easiest'); +check(!qi.easy.have, 'still locked after Easy'); +check(qi.normal.have, 'unlocked once Normal is cleared too'); +check(qi.normal.onSheet, 'and Qi Circulation appears on the skill sheet with it'); +check(qi.copies === 1, `granted once, not once per tick (${qi.copies} in the action list)`); +check(qi.rccln5 === 1 && !qi.rcclnGrantsAction, + 'Temperance still has exactly one perk at level 5, now a plain one — no index shift for old saves'); + console.log('\n--- the escape hatch still works'); -const granted = await p.evaluate(() => modUnlockAll()); +// Lock everything first. The Qi section above leaves Circulate Qi unlocked, so +// asserting a raw count here would be measuring test order, not the function. +const granted = await p.evaluate(() => { + MOD_ACTIONS.forEach(a => { a.have = false; }); + acts = acts.filter(a => MOD_ACTIONS.indexOf(a) === -1); + return modUnlockAll(); +}); st = await status(); -check(granted === 4, `modUnlockAll() granted ${granted}`); -check(st.locked.every(u => u.have), 'all four present after modUnlockAll()'); +const allFour = await p.evaluate(() => MOD_ACTIONS.filter(a => a.have === true).length); +check(granted === 4, `modUnlockAll() granted all four from locked (${granted})`); +check(allFour === 4, `and all four are held afterwards (${allFour})`); +check(st.locked.every(u => u.have), 'including the three that are skill-gated'); console.log('\n--- and modActions() reports honestly'); const report = await p.evaluate(() => modActions()); From 9c05eb5424045d9864922057b5ef0109f8f5ad27 Mon Sep 17 00:00:00 2001 From: Xeus24 <201404920+Xeus24@users.noreply.github.com> Date: Tue, 8 Sep 2026 23:11:01 -0700 Subject: [PATCH 17/44] Give the breakthrough pills a source The realm system shipped with no way to obtain a single one. All ten existed as items and nothing gave, dropped or sold them, so the entire ladder -- the thing sections 28 and 29 both hang off -- was reachable only from the console. cultivation.mjs passed throughout, because it called MOD_breakthrough directly with { amount: 1 } as the pill. It tested the mechanism and never asked where the input came from. That is the blind spot of a test that supplies its own inputs, so the fix is a test that asks the world instead: it walks the Herbalist's stock, the dojo's rungs and the Qi-unlock path, and fails if any realm's pill is orphaned. realm 1 the instructor, when the dojo finishes teaching you realms 2-5 the Herbalist, 900 to 38,000 copper realms 6-10 the dojo's Level Advancement rungs at 45, 60, 75, 90, 105 The Herbalist rather than a new alchemist shopfront: it is already the marketplace's plants-and-medicine vendor, already stocks sp1/sp2/sp3, and xianxia alchemy is herbalism with qi. Appending to vendor.pha1.items uses the game's own restock and purchase machinery, so there is no second screen to keep in step. It also picks up the Superior and Refined Spirit Pills, which had the same problem in miniature. Realm 1 is deliberately not from the Herbalist. The marketplace is gated behind reading a flyer the Paper Boy hands you at 40% per Village Center visit, and only after dj1end, while realm 1 opens at Qi Circulation 1 -- sourcing the first pill there would have put the whole ladder behind a random encounter. Save-safe: vendor stock is stored by item id and restored by scanning itemgroup[(id+1)/10000|0], which for the 912x ids is `item`, where these are defined. Verified end to end -- the pill appears in real restocked stock, survives the round trip, and advances the realm when used. Full suite green, 20 scripts. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 8 ++++ MOD-NOTES.md | 38 +++++++++++++++++++ changelog/mod-changelog.html | 13 +++++++ mod.js | 71 +++++++++++++++++++++++++++++++++++- tests/cultivation.mjs | 30 +++++++++++++++ 5 files changed, 158 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index d805f33..5a7eea1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -168,6 +168,14 @@ Realms are **derived from `MOD_RANK_AT`** — the same thresholds title ranks us 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 diff --git a/MOD-NOTES.md b/MOD-NOTES.md index b67142e..679a553 100644 --- a/MOD-NOTES.md +++ b/MOD-NOTES.md @@ -1803,6 +1803,44 @@ touched. The test's other assertions read the thresholds out of `MOD_REALMS` too — the first version hardcoded "realm 1 needs Qi 10" and broke the moment the ladder was rescaled, which is the same staleness in miniature. +### Where the pills come from, and the hole where that used to be + +The realm system shipped with **no way to obtain a single breakthrough pill.** +All ten existed as items; nothing gave, dropped or sold them. The whole ladder — +the thing sections 28 and 29 both hang off — was reachable only from the +console. + +`tests/cultivation.mjs` passed throughout, because it called `MOD_breakthrough` +directly with `{ amount: 1 }` as the pill. It tested the mechanism and never +asked where the input came from. That is the specific blind spot of a test that +supplies its own inputs, and the fix is a test that asks the world instead: it +now walks the Herbalist's stock list, the dojo's rungs and the Qi-unlock path, +and fails if any realm's pill is orphaned. + +Three sources, matching how far along the realm is: + +| realm | source | +|---|---| +| 1 | the instructor, when the dojo finishes teaching you | +| 2-5 | the Herbalist, 900 to 38,000 copper | +| 6-10 | the dojo's Level Advancement rungs at 45, 60, 75, 90, 105 | + +The Herbalist rather than a new alchemist shopfront: it is already the +marketplace's plants-and-medicine vendor, already stocks `sp1`/`sp2`/`sp3`, and +xianxia alchemy is herbalism with qi. Appending to `vendor.pha1.items` uses the +game's own restock and purchase machinery, so there is no second screen to keep +in step. + +Realm 1 is deliberately **not** from the Herbalist. The marketplace is gated +behind reading a flyer the Paper Boy hands you at 40% per Village Center visit, +and only after `dj1end` — while realm 1 opens at Qi Circulation 1. Sourcing the +first pill there would have put the entire ladder behind a random encounter. + +Save-safe by inspection: vendor stock is stored by item id and restored by +scanning `itemgroup[(id+1)/10000|0]`, which for the 912x ids is `item`, where +these are defined. Verified end to end — the pill appears in real restocked +stock, survives the round trip, and advances the realm when used. + ### The bottleneck is the whole point Reaching the Qi Circulation level does **not** advance the realm. It puts you at diff --git a/changelog/mod-changelog.html b/changelog/mod-changelog.html index 6de7ae3..a97a193 100644 --- a/changelog/mod-changelog.html +++ b/changelog/mod-changelog.html @@ -18,6 +18,19 @@ Remove the <script src="mod.js"> line from index.html and none of it applies.

+----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
diff --git a/mod.js b/mod.js index 71f0597..b572124 100644 --- a/mod.js +++ b/mod.js @@ -31,7 +31,7 @@ console.log('[mod] loading'); var MOD = { - version: '2.7', // v2.0: the ~100 "discovered by playing" skills consolidated + version: '2.8', // v2.0: the ~100 "discovered by playing" skills consolidated // to 10; per-stat effect budget unchanged. Survivors keep // their v1 id, so v1 saves still LOAD — merged-away skills // just don't restore. See "Balance, sixth pass" in @@ -55,6 +55,8 @@ var MOD = { // elemental mastery skills whose techniques fire in combat. // v2.7: Circulate Qi unlocked by the dojo's tutorial fights // rather than by Temperance 5. + // v2.8: breakthrough pills given a source — they had none, + // so the realm ladder was unreachable in play. // Changes are listed in changelog/changelog.html. speed_key: 'p23_mod_speed', skill_xp_mult: 2, // change with setSkillXp(n) @@ -663,6 +665,19 @@ function MOD_checkQiUnlock() { if (act.mod_qi.have !== true) { giveAction(act.mod_qi); msg('The forms settle into something you can feel moving.', 'plum'); + /* And the first breakthrough pill. Realm 1 opens at Qi Circulation 1, + long before the marketplace does — the Paper Boy who starts that chain + only appears after the dojo, at 40% a visit — so leaving the first + pill to the Herbalist would gate the entire ladder behind a random + encounter. The instructor hands it over here, which is the right beat + anyway. */ + try { + if (item.mod_bp1 && !global.flags.mod_firstpill) { + giveItem(item.mod_bp1, 1); + global.flags.mod_firstpill = true; + msg('Instructor: Take this. When you can feel it moving, swallow it.', 'gold'); + } + } catch (e) {} } if (skl.qic && you.skls && you.skls.indexOf(skl.qic) === -1) { you.skls.push(skl.qic); @@ -5787,12 +5802,18 @@ function MOD_rungReward(lv) { return { pill: 'sp7', n: lv >= 110 ? 3 : 2, coin: 3000 + (lv - 95) * 100 }; } +/* Realms 6-10 are past what a village herbalist can source, so the dojo + carries them. Spread over the continuation rungs rather than clustered, so + each one is a reason to come back. */ +var MOD_RUNG_REALM = { 45: 6, 60: 7, 75: 8, 90: 9, 105: 10 }; + var MOD_DOJO_RUNGS = []; for (var MOD_rl = 35; MOD_rl <= 110; MOD_rl += 5) { var rw = MOD_rungReward(MOD_rl); MOD_DOJO_RUNGS.push({ lv: MOD_rl, flag: 'mod_djrw' + MOD_rl, pill: rw.pill, n: rw.n, coin: rw.coin, + realmPill: MOD_RUNG_REALM[MOD_rl] || null, manual: (MOD_rl === 50 || MOD_rl === 75 || MOD_rl === 100) }); } @@ -5829,6 +5850,10 @@ function MOD_dojoAdvancement() { chs('"Accept"', false, 'lime').addEventListener('click', function () { giveWealth(next.coin); giveItem(item[next.pill], next.n); + if (next.realmPill && item['mod_bp' + next.realmPill]) { + giveItem(item['mod_bp' + next.realmPill], 1); + msg('Instructor: And this. I am not going to pretend I know where it came from.', 'gold'); + } if (next.manual) { MOD_dojoManualChoice(next); return; } global.flags[next.flag] = true; smove(chss.t3, false); @@ -5889,7 +5914,8 @@ function modDojo() { var done = !!global.flags[r.flag]; lines.push(' ' + (done ? '[done] ' : (you.lvl >= r.lv ? '[open] ' : '[ ] ')) + 'lv ' + String(r.lv).padStart(3) + ' ' + String(r.coin).padStart(5) + ' coin, ' + - item[r.pill].name + ' x' + r.n + (r.manual ? ', a manual of your choice' : '')); + item[r.pill].name + ' x' + r.n + (r.manual ? ', a manual of your choice' : '') + + (r.realmPill ? ', ' + item['mod_bp' + r.realmPill].name : '')); }); var out = lines.join('\n'); console.log(out); @@ -6047,6 +6073,47 @@ MOD_BREAK_PILLS.forEach(function (b) { item['mod_bp' + realm] = it; }); +/* --- where the pills come from ------------------------------------------- + Shipped without this and the whole realm ladder was unreachable in normal + play: the ten pills existed as items and nothing gave, dropped or sold them. + The test called MOD_breakthrough directly and never asked where a pill came + from, which is exactly the gap a test that mocks its inputs leaves open. + + Three sources, matching how far along each realm is: + + realm 1 the instructor, when the dojo finishes teaching you + realms 2-5 the Herbalist, who already sells spirit pills + realms 6-10 the dojo's Level Advancement rungs + + The Herbalist rather than a new alchemist shopfront: it is the marketplace's + plants-and-medicine vendor, it already stocks sp1/sp2/sp3, and xianxia + alchemy is herbalism with qi. Appending to `vendor.pha1.items` uses the + game's own restock and purchase machinery — no new screen to keep in step. + + Save-safe: vendor stock is stored by item id and restored by scanning + `itemgroup[(id+1)/10000|0]`, which for 912x is `item`, where these live. + -------------------------------------------------------------------------- */ + +(function () { + try { + if (!vendor.pha1 || !vendor.pha1.items) return; + // realms 2-5. Rarer and dearer the higher it goes; the top half is not + // something a village herbalist can get hold of. + [[2, 900, 0.55, 1, 2], + [3, 3200, 0.40, 1, 2], + [4, 11000, 0.25, 1, 1], + [5, 38000, 0.12, 1, 1]].forEach(function (r) { + var it = item['mod_bp' + r[0]]; + if (it) vendor.pha1.items.push({ item: it, p: r[1], c: r[2], min: r[3], max: r[4] }); + }); + // and the two spirit pill grades above the ones already on the shelf + [[item.sp4, 2600, 0.5, 1, 3], [item.sp5, 14000, 0.3, 1, 2]].forEach(function (r) { + if (r[0]) vendor.pha1.items.push({ item: r[0], p: r[1], c: r[2], min: r[3], max: r[4] }); + }); + console.log('[mod] Herbalist stocks breakthrough pills for realms 2-5'); + } catch (e) { console.warn('[mod] could not stock the Herbalist: ' + e.message); } +})(); + /* --- the attempt ---------------------------------------------------------- */ function MOD_breakthrough(realm, pill) { diff --git a/tests/cultivation.mjs b/tests/cultivation.mjs index 6935673..c81648b 100644 --- a/tests/cultivation.mjs +++ b/tests/cultivation.mjs @@ -57,6 +57,36 @@ check(ladder.masteries.length === 6, `six mastery skills (${ladder.masteries.joi check(ladder.perks.every(n => n === 7), 'each has the same perk ladder every other skill got'); check(ladder.wired, 'and all six are registered with the cap and skill-panel maps'); +console.log('\n--- every breakthrough pill has a source in the game'); +// The realm system shipped once with none of them obtainable: the ten pills +// existed as items and nothing gave, dropped or sold them, so the whole ladder +// was console-only. The test called MOD_breakthrough directly and never asked +// where a pill came from — the gap a test that mocks its inputs leaves open. +const sources = await p.evaluate(() => { + const src = {}; + for (let n = 1; n <= MOD_REALMS.length - 1; n++) src[n] = []; + // the instructor, on the Qi unlock + if (/mod_bp1/.test(String(MOD_checkQiUnlock))) src[1].push('dojo (Qi unlock)'); + // the Herbalist's stock list + (vendor.pha1.items || []).forEach(e => { + const m = /^mod_bp(\d+)$/.exec(Object.keys(item).find(k => item[k] === e.item) || ''); + if (m) src[+m[1]].push('Herbalist ' + e.p + 'c'); + }); + // the dojo's continuation rungs + MOD_DOJO_RUNGS.forEach(r => { if (r.realmPill) src[r.realmPill].push('dojo lv ' + r.lv); }); + return { src, spirit: (vendor.pha1.items || []) + .filter(e => [item.sp4, item.sp5, item.sp6, item.sp7].indexOf(e.item) !== -1) + .map(e => e.item.name) }; +}); +Object.keys(sources.src).forEach(n => + console.log(` realm ${String(n).padStart(2)} ${sources.src[n].join(', ') || 'NOWHERE'}`)); +const orphans = Object.keys(sources.src).filter(n => !sources.src[n].length); +check(orphans.length === 0, + `every realm's pill is obtainable in play (${orphans.length ? 'orphaned: ' + orphans.join(',') : 'all ' + Object.keys(sources.src).length})`); +check(sources.src[1].some(s => /dojo/.test(s)), + 'realm 1 comes from the dojo, not the marketplace — the marketplace is gated behind a 40% encounter'); +check(sources.spirit.length >= 2, `the Herbalist also stocks the new spirit pills (${sources.spirit.join(', ')})`); + console.log('\n--- reaching the level does NOT advance the realm'); const wall = await p.evaluate(() => { global.flags.mod_realm = 0; skl.qic.lvl = 30; From b9639e582a164a8432b33d98dcdbbadf717b4e04 Mon Sep 17 00:00:00 2001 From: Xeus24 <201404920+Xeus24@users.noreply.github.com> Date: Tue, 8 Sep 2026 23:43:11 -0700 Subject: [PATCH 18/44] Open the catacombs, and build the Pill Tower Two places, both of which the game was already pointing at. The mod had spent a lot of sections adding systems and none adding world. The catacombs were finished and unreachable. 26 locations exist, fully written, with their own ambient text table and a bestiary entry for the ghouls that says they live there. grep catamn returns three hits: its definition, its own handler, and one smove(chss.catamn) from inside cata1 going back. Nothing links in -- and its own exit leads to the Village Center, which is where the entrance was always meant to be. So the entrance is one chs() there. Not a word of the content is the mod's; the entrance is the whole change, and places.mjs asserts it (zero mod_cata* keys). It is gated, and not for flavour. Visiting sets mod_t_cata, the cap-40 rung in MOD_TIERS -- the rung that has been dead all along precisely because nothing could reach it. An open door would hand a fresh character cap 40 straight out of the tutorial, past the forest's 20 and the deep forest's 30. Gated on mod_t_deep, the rung below it, so the ladder keeps its order: forest 20 -> deep 30 -> catacombs 40 -> arena 50. That rule is now in CLAUDE.md for any future area that sets a tier flag. The Pill Tower is the author's own abandoned idea. On the Village Center, in their file: // chs('"=> Visit Pill Tower"',false).addEventListener('click',()=>{ // smove(chss.pltwr1); // }); The choice was written and commented away; the location was never built. An alchemists' tower is exactly what a cultivation game wants and exactly what this mod lacked, so this completes their intention rather than inventing a different shopfront. It is chss.mod_pltwr, not chss.pltwr1 -- the two must not collide if they ever finish theirs, and the test checks chss.pltwr1 is still undefined. It carries its own vendor with the grades a village herbalist cannot source (realms 6-10, plus the Sublime and Transcendent Spirit Pills), and a spirit vein you can sit in once a day for Qi Circulation exp scaling with your realm -- somewhere for cultivation to go, which the ladder otherwise entirely lacked. Gated on having a realm at all: they do not let mortals past the door. New test places.mjs, 20 checks. Full suite green, 21 scripts. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 12 ++- MOD-NOTES.md | 55 ++++++++++++ changelog/mod-changelog.html | 17 ++++ mod.js | 159 ++++++++++++++++++++++++++++++++++- tests/README.md | 1 + tests/places.mjs | 120 ++++++++++++++++++++++++++ tests/run.sh | 2 +- 7 files changed, 361 insertions(+), 5 deletions(-) create mode 100644 tests/places.mjs diff --git a/CLAUDE.md b/CLAUDE.md index 5a7eea1..a0fd813 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -154,9 +154,15 @@ What matters: 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 unreachable in the base game.** All 26 locations exist but - nothing links into `chss.catamn`, so `mod_t_cata` can never fire and the cap-40 - rung is dead (harmlessly — arena I-II grants cap 50 anyway). See MOD-NOTES. +- **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`. diff --git a/MOD-NOTES.md b/MOD-NOTES.md index 679a553..49afb9d 100644 --- a/MOD-NOTES.md +++ b/MOD-NOTES.md @@ -1364,6 +1364,61 @@ the tier by the base game's own design (Forest-far at level 7, the Catacombs at The tier-appropriate endgame is a real fight at 13 hits, and the conditional skills still pay for themselves by cutting it hard. +## Two places, both of which the game was already pointing at + +The mod had spent a lot of sections adding systems and none adding world. These +are the two places the game itself had already gestured at. + +### The catacombs, finished and unreachable + +26 locations exist, fully written, with their own ambient text table and a +bestiary entry for the ghouls that says they live there. `grep catamn` returns +three hits: its definition, its own handler, and one `smove(chss.catamn)` from +*inside* `cata1` going back. Nothing links in. Its own exit leads to the Village +Center, which is where the entrance was always meant to be. + +So the entrance is one `chs()` there — the same trick the Old Path trailhead +uses. **Not a word of the content is the mod's**; the entrance is the whole +change, and `tests/places.mjs` asserts that (`mod_cata*` keys: zero). + +It has to be gated, and not for flavour. Visiting sets `mod_t_cata`, which is +the cap-40 rung in `MOD_TIERS` — the rung that has been dead all along precisely +because nothing could reach it. An open door would hand a fresh character cap 40 +straight out of the tutorial, past the forest's 20 and the deep forest's 30. +Gated on `mod_t_deep`, the rung below it, so the ladder keeps its order: + + forest 20 -> deep forest 30 -> catacombs 40 -> golem arena 50 + +That is the general rule for any new area that sets a tier flag, and it is now +in CLAUDE.md. + +### The Pill Tower, which the author started + +On the Village Center, in the author's own file: + +```js +// chs('"=> Visit Pill Tower"',false).addEventListener('click',()=>{ +// smove(chss.pltwr1); +// }); +``` + +The choice was written and commented away; `chss.pltwr1` was never built. An +alchemists' tower is exactly what a cultivation game wants and exactly what this +mod was missing, so this builds it rather than inventing a different shopfront — +completing their intention instead of imposing mine. + +It is `chss.mod_pltwr`, **not** `chss.pltwr1`: if the author ever finishes +theirs, the two must not collide. The test checks `chss.pltwr1` is still +undefined. + +It carries its own vendor with the grades a village herbalist cannot source +(realms 6-10, plus the Sublime and Transcendent Spirit Pills), and a spirit +vein you can sit in once a day for Qi Circulation exp that scales with your +realm — somewhere for cultivation to *go*, which the ladder otherwise entirely +lacked. Gated on having a realm at all: they do not let mortals past the door, +which is the genre's own snobbery and gives the realm ladder a presence in the +world rather than only on the character sheet. + ## The catacombs are unreachable in the base game `mod_t_cata` gates the cap-40 rung, and it is set by hooking `chss.catamn` / diff --git a/changelog/mod-changelog.html b/changelog/mod-changelog.html index a97a193..0e7bf49 100644 --- a/changelog/mod-changelog.html +++ b/changelog/mod-changelog.html @@ -18,6 +18,23 @@ Remove the <script src="mod.js"> line from index.html and none of it applies.

+----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,
diff --git a/mod.js b/mod.js index b572124..a7b584b 100644 --- a/mod.js +++ b/mod.js @@ -31,7 +31,7 @@ console.log('[mod] loading'); var MOD = { - version: '2.8', // v2.0: the ~100 "discovered by playing" skills consolidated + version: '2.9', // v2.0: the ~100 "discovered by playing" skills consolidated // to 10; per-stat effect budget unchanged. Survivors keep // their v1 id, so v1 saves still LOAD — merged-away skills // just don't restore. See "Balance, sixth pass" in @@ -57,6 +57,8 @@ var MOD = { // rather than by Temperance 5. // v2.8: breakthrough pills given a source — they had none, // so the realm ladder was unreachable in play. + // v2.9: the catacombs wired up (26 of the author's rooms, + // previously unreachable) and the Pill Tower built. // Changes are listed in changelog/changelog.html. speed_key: 'p23_mod_speed', skill_xp_mult: 2, // change with setSkillXp(n) @@ -6395,3 +6397,158 @@ function modMastery() { console.log('[mod] elemental mastery: ' + MOD_MASTERY.length + ' skills with techniques that fire in combat. modMastery() for the rates.'); + + +/* =========================================================================== + 30. TWO PLACES + --------------------------------------------------------------------------- + The mod had added systems and no world. This adds the two places the game + itself was already pointing at. + + --- the catacombs, which were finished and unreachable ------------------- + + 26 locations exist, fully written, with their own ambient text table and a + bestiary entry for the ghouls that says they live there. Nothing links in: + `grep catamn` returns its definition, its own handler, and one + `smove(chss.catamn)` from INSIDE cata1 going back. Its own exit leads to the + Village Center, which is where the entrance was always meant to be. + + So the entrance is one `chs()` on the Village Center — the same trick the + Old Path trailhead uses. Nothing here is written by the mod; it opens the + author's own rooms. + + It has to be GATED, though, and not for flavour. Visiting sets `mod_t_cata`, + which is the cap-40 rung in MOD_TIERS — the rung that has been dead all + along because nothing could reach it. Open the door to a fresh character and + they take cap 40 straight out of the tutorial, past the forest's 20 and the + deep forest's 30. Gated on `mod_t_deep` instead, so the ladder keeps its + order: forest 20 -> deep forest 30 -> catacombs 40 -> arena 50. + + --- the Pill Tower, which the author started and commented out ----------- + + On the Village Center, in the author's own file: + + // chs('"=> Visit Pill Tower"',false).addEventListener('click',()=>{ + // smove(chss.pltwr1); + // }); + + The choice was written and commented away; `chss.pltwr1` was never built. + An alchemists' tower is exactly what a cultivation game wants and exactly + what this mod was missing, so this builds it rather than inventing a + different shopfront. + + The location is `chss.mod_pltwr`, not `chss.pltwr1` — if the author ever + finishes theirs, the two must not collide. + + Gated on having a realm: they do not let mortals past the door. That gives + the realm ladder somewhere to be visible in the world rather than only on + the character sheet, and it is the genre's own snobbery. + =========================================================================== */ + +/* --- A. the catacombs entrance -------------------------------------------- */ + +MOD.cata_gate = 'mod_t_deep'; // cap 30 — see the header for why + +var MOD_lsmain1_original_sl = chss.lsmain1.sl; + +chss.lsmain1.sl = function () { + MOD_lsmain1_original_sl.apply(this, arguments); + try { + /* Every chs() here passes false. chs(txt, true) calls clr_chs() and would + wipe the Village Center that was just drawn. */ + if (global.flags[MOD.cata_gate] === true) { + chs('"=> Enter the Catacombs"', false, 'darkgrey').addEventListener('click', function () { + smove(chss.catamn); + }); + } + if (MOD_realm().n >= 1) { + chs('"=> Visit the Pill Tower"', false, 'plum').addEventListener('click', function () { + smove(chss.mod_pltwr); + }); + } + } catch (e) { + console.warn('[mod] village center additions failed: ' + e.message); + } +}; + +/* --- B. the Pill Tower ---------------------------------------------------- */ + +/* Its own vendor: the grades a village herbalist cannot source. Same shape as + the game's own vendors, so restock and the purchase screen work unchanged. */ +vendor.mod_pltwr = new Vendor(); +vendor.mod_pltwr.name = 'Pill Tower'; +vendor.mod_pltwr.infl = 1; +vendor.mod_pltwr.dfl = 0.25; +vendor.mod_pltwr.timeorig = 3; // restocks every three days +vendor.mod_pltwr.data = { time: 3, rep: 0 }; +vendor.mod_pltwr.items = [ + { item: item.mod_bp6, p: 120000, c: 0.45, min: 1, max: 1 }, + { item: item.mod_bp7, p: 340000, c: 0.30, min: 1, max: 1 }, + { item: item.mod_bp8, p: 900000, c: 0.20, min: 1, max: 1 }, + { item: item.mod_bp9, p: 2400000, c: 0.12, min: 1, max: 1 }, + { item: item.mod_bp10, p: 7000000, c: 0.06, min: 1, max: 1 }, + { item: item.sp6, p: 90000, c: 0.5, min: 1, max: 2 }, + { item: item.sp7, p: 480000, c: 0.3, min: 1, max: 1 } +].filter(function (e) { return !!e.item; }); +restock(vendor.mod_pltwr); + +/* The spirit vein. Somewhere for cultivation to GO — the realm ladder was + otherwise entirely a character-sheet affair. Once a day, because a place you + can sit in forever is just a faster version of the action you already have. + The day is read off `time.day`, which the game already advances. */ +MOD.vein_flag = 'mod_vein_day'; + +function MOD_veinReady() { + return Number(global.flags[MOD.vein_flag] || -1) !== Number(time.day); +} + +function MOD_veinExp() { + // scales with the realm, so it stays worth the walk + return Math.round(60 * Math.pow(1.9, MOD_realm().n)); +} + +chss.mod_pltwr = new Chs(); chss.mod_pltwr.id = 976; + +chss.mod_pltwr.sl = function () { + global.flags.inside = true; + d_loc('Pill Tower'); + global.lst_loc = 976; + + var realm = MOD_realm(); + chs('The air inside is thick enough to lean on. Somewhere above, something is being ' + + 'refined that the attendants will not name.', true); + chs('Attendant: ' + realm.name + '. You may go up as far as the ' + + 'second floor.', false, 'grey'); + + chs('"Purchase"', false, 'orange').addEventListener('click', function () { + chs_spec(4, vendor.mod_pltwr); + chs('"<= Return"', false, '', '', null, null, null, true).addEventListener('click', function () { + smove(chss.mod_pltwr, false); + }); + }); + + if (MOD_veinReady()) { + chs('"Sit in the spirit vein"', false, 'plum').addEventListener('click', function () { + var gain = MOD_veinExp(); + global.flags[MOD.vein_flag] = time.day; + try { giveSkExp(skl.qic, gain); } catch (e) {} + chs('You sit where the floor is warmest and stop doing anything else.', true, 'plum'); + chs('Qi Circulation +' + gain.toLocaleString() + + ' exp. The vein will not give again today.', false); + chs('"<= Stand up"', false).addEventListener('click', function () { + smove(chss.mod_pltwr, false); + }); + }); + } else { + chs('The vein is spent for today', false, 'grey'); + } + + chs('"<= Leave"', false).addEventListener('click', function () { + smove(chss.lsmain1); + }); +}; + +try { addtosector(sector.vmain1, chss.mod_pltwr); } catch (e) { /* sector optional */ } + +console.log('[mod] catacombs entrance (gated on ' + MOD.cata_gate + + ') and the Pill Tower added to the Village Center'); diff --git a/tests/README.md b/tests/README.md index 345869f..6e5b0e8 100644 --- a/tests/README.md +++ b/tests/README.md @@ -44,6 +44,7 @@ a specific binary instead of Playwright's own. | `fightsmoke.mjs` | End-to-end: real battles through the game's own `attack()`, nine per area, so the wrapper is exercised with the miss roll, the dodge check, the per-swing `allbuff` and death handling rather than by calling `dmg_calc` directly. Fails if a fight is lost, never resolves, or turns into a slog. Note that it re-asserts `global.flags.btl` every swing — the page's own game loop will otherwise drop you out of battle state and the duel stalls silently. | | `fullbal.mjs` | Wider version: combat under neutral *and* favourable conditions, where player power comes from (vanilla skills vs. added ones vs. Renown vs. situational buffs), exp pacing, per-kill economy, and a dominance check for whether any single skill carries everything. | | `titles.mjs` | Title ranks and what wearing one does: every skill grants at least four, all ten ranks populated, every rank 10 earned at level 110, nothing rank 3+ earned below level 18 (the base game had eight — Runner at 10, Rookie at 15), renown 10 near-completion, a title inert until worn or until renown covers its rank, and — the one that matters — the exp bonus writes to `skl..p` which IS saved, so it is checked against stacking over thirty ticks and across a reload. | +| `places.mjs` | The catacombs entrance and the Pill Tower: both gated and the Village Center's own choices untouched, the catacombs gate sitting on the rung below their cap so the ladder keeps its order (forest 20 → deep 30 → catacombs 40), all 26 of the author's rooms reachable with none written by the mod, `mod_t_cata` finally firing, the tower drawing and stocking without colliding with the author's own `chss.pltwr1`, and the spirit vein giving once a day and scaling with the realm. | | `cultivation.mjs` | Realms and elemental mastery: a new character is Mortal, reaching the Qi Circulation level puts you at a bottleneck rather than advancing you, realms cannot be skipped or entered under-levelled, the pill is spent on failure as well as success, consolidating past the requirement improves the odds, a realm is worth a real multiplier and does not compound over repeated `allbuff` calls, no technique fires below Qi Refining, and techniques reach the real attack path at the advertised rate through the INT branch of `dmg_calc`. Also checks a throwing proc never costs you the swing. | | `dojo.mjs` | The Level Advancement ladder past level 30: pills sized against what a level actually costs (`4*lvl^3 + lvl^2`, so 5.3M at level 110 — the game's best pill is 0.3% of one), the continuation hidden until the base six rungs are claimed and off every one-time screen, the base lobby left intact, one rung at a time in order and gated on level, claiming pays coin and pills, the 50/75/100 rungs offer a manual choice and are not claimed until one is picked, and the ladder stops at 110. | | `titlepicker.mjs` | The picker groups by skill: many titles collapse to few rows, the header is the best held rather than the first earned, base-game titles group with the generated ones, the caret opens and closes without selecting, other groups stay shut, selecting from inside a group wears it and closes the window, and story titles stay loose. | diff --git a/tests/places.mjs b/tests/places.mjs new file mode 100644 index 0000000..c2f13f5 --- /dev/null +++ b/tests/places.mjs @@ -0,0 +1,120 @@ +import { chromium } from 'playwright'; + +// The two places the game was already pointing at. +// +// The catacombs: 26 locations, fully written by the author, with their own +// ambient text and a bestiary entry naming them -- and nothing linking in. The +// entrance is one chs() on the Village Center, which is where its own exit +// already leads. +// +// The Pill Tower: the author wrote the Village Center choice and commented it +// out; chss.pltwr1 was never built. This builds it as chss.mod_pltwr, so the +// two cannot collide if they ever finish theirs. +// +// PORT=8080 node tests/places.mjs + +const HOST = `http://127.0.0.1:${process.env.PORT || 8080}`; +const b = await chromium.launch(process.env.CHROMIUM ? { executablePath: process.env.CHROMIUM } : {}); +const p = await b.newPage(); +const errs = []; p.on('pageerror', e => errs.push(String(e).slice(0, 200))); +await p.goto(`${HOST}/index.html`, { waitUntil: 'load' }); +await p.waitForTimeout(4500); + +const fail = []; +const check = (c, what) => { if (!c) fail.push(what); console.log(` ${c ? 'ok ' : 'FAIL'} ${what}`); }; + +const village = (flags) => p.evaluate(f => { + Object.assign(global.flags, f); + try { chss.lsmain1.sl(); } catch (e) { return ['THREW: ' + e.message]; } + return [...dom.ctr_2.querySelectorAll('.chs')].map(n => n.textContent.trim()); +}, flags); + +console.log('--- both are gated, and the Village Center is left intact'); +let v = await village({ mod_t_deep: false, mod_realm: 0 }); +check(!v.some(t => /Catacombs/.test(t)), 'no catacombs before the deep forest'); +check(!v.some(t => /Pill Tower/.test(t)), 'no Pill Tower while still Mortal'); +check(v.some(t => /Enter Dojo/.test(t)) && v.some(t => /Message Board/.test(t)), + "the game's own choices are all still there"); + +v = await village({ mod_t_deep: true }); +check(v.some(t => /Enter the Catacombs/.test(t)), 'the catacombs open once the deep forest is done'); +check(!v.some(t => /Pill Tower/.test(t)), 'the tower still does not'); + +v = await village({ mod_realm: 2 }); +check(v.some(t => /Pill Tower/.test(t)), 'and the tower opens once you have a realm'); + +console.log('\n--- the gate exists to keep the cap ladder in order'); +const caps = await p.evaluate(() => { + const at = (f) => { Object.assign(global.flags, { mod_t_forest: false, mod_t_deep: false, + frstn1a3u: false, mod_t_cata: false, trne1e1: false, trne3e1: false, tr3_win: false }, + f); MOD_CAP.current = -1; return MOD_levelCap(); }; + return { fresh: at({}), forest: at({ mod_t_forest: true }), + deep: at({ mod_t_forest: true, mod_t_deep: true }), + cata: at({ mod_t_forest: true, mod_t_deep: true, mod_t_cata: true }), + gate: MOD.cata_gate }; +}); +console.log(` fresh ${caps.fresh} -> forest ${caps.forest} -> deep ${caps.deep} -> catacombs ${caps.cata}`); +check(caps.gate === 'mod_t_deep', `gated on ${caps.gate}, the rung below it`); +check(caps.cata > caps.deep && caps.deep > caps.forest, + 'so the catacombs rung comes after the ones below it, not straight out of the tutorial'); + +console.log('\n--- the catacombs themselves are the author\'s, and they work'); +const cata = await p.evaluate(() => { + global.flags.mod_t_cata = false; + try { chss.catamn.sl(); } catch (e) { return { threw: e.message }; } + const rows = [...dom.ctr_2.querySelectorAll('.chs')].map(n => n.textContent.trim()); + // count how many catacomb rooms exist and how many the mod wrote (none) + const rooms = Object.keys(chss).filter(k => /^cata/.test(k)).length; + return { rows, rooms, tierSet: global.flags.mod_t_cata === true, + modWrote: Object.keys(chss).filter(k => /^mod_cata/.test(k)).length }; +}); +check(!cata.threw, 'the entryway draws'); +check(cata.rows.some(t => /Move North/.test(t)) && cata.rows.some(t => /Exit/.test(t)), + 'with its own exits'); +check(cata.rooms >= 26, `${cata.rooms} catacomb rooms now reachable`); +check(cata.modWrote === 0, 'none of them written by the mod — the entrance is the whole change'); +check(cata.tierSet, 'and visiting sets mod_t_cata, so the cap-40 rung is finally live'); + +console.log('\n--- the Pill Tower'); +const tower = await p.evaluate(() => { + global.flags.mod_realm = 2; + try { chss.mod_pltwr.sl(); } catch (e) { return { threw: e.message }; } + const rows = [...dom.ctr_2.querySelectorAll('.chs')].map(n => n.textContent.trim()); + let tries = 0, everything = new Set(); + while (tries++ < 400) { restock(vendor.mod_pltwr); + vendor.mod_pltwr.stock.forEach(s => everything.add(s[0].name)); } + return { rows, stocks: [...everything], clash: typeof chss.pltwr1, + id: chss.mod_pltwr.id }; +}); +check(!tower.threw, 'the tower draws'); +check(tower.rows.some(t => /Purchase/.test(t)), 'it sells'); +check(tower.rows.some(t => /spirit vein/.test(t)), 'it has the spirit vein'); +check(tower.rows.some(t => /Leave/.test(t)), 'and a way out'); +check(tower.clash === 'undefined', "it does not collide with the author's own chss.pltwr1"); +console.log(' stocks: ' + tower.stocks.join(', ')); +check(tower.stocks.length >= 5, `${tower.stocks.length} distinct wares over 400 restocks`); + +console.log('\n--- the vein gives once a day'); +const vein = await p.evaluate(() => { + global.flags.mod_vein_day = -1; global.flags.mod_realm = 2; + const before = skl.qic.exp + skl.qic.lvl * 1e6; + chss.mod_pltwr.sl(); + [...dom.ctr_2.querySelectorAll('.chs')].find(n => /spirit vein/.test(n.textContent)).click(); + const after = skl.qic.exp + skl.qic.lvl * 1e6; + chss.mod_pltwr.sl(); + const second = [...dom.ctr_2.querySelectorAll('.chs')].map(n => n.textContent.trim()); + // and it scales with the realm, so it stays worth the walk + global.flags.mod_realm = 1; const low = MOD_veinExp(); + global.flags.mod_realm = 9; const high = MOD_veinExp(); + return { gained: after > before, spent: second.some(t => /spent for today/.test(t)), + offeredAgain: second.some(t => /Sit in the spirit vein/.test(t)), low, high }; +}); +check(vein.gained, 'sitting in it trains Qi Circulation'); +check(vein.spent && !vein.offeredAgain, 'and it is spent for the rest of the day'); +check(vein.high > vein.low * 10, `it scales with the realm (${vein.low} at realm 1, ${vein.high.toLocaleString()} at 9)`); + +console.log('\nerrors:', errs.length ? errs : 'none'); +if (errs.length) fail.push('page errors: ' + JSON.stringify(errs)); +await b.close(); +if (fail.length) { console.error('\nFAIL:\n - ' + fail.join('\n - ')); process.exit(1); } +console.log('\nPASS — the catacombs are reachable and the Pill Tower exists.'); diff --git a/tests/run.sh b/tests/run.sh index e231c55..873f265 100755 --- a/tests/run.sh +++ b/tests/run.sh @@ -19,7 +19,7 @@ fi if [ "$#" -gt 0 ]; then TESTS=("$@") else - TESTS=(audit audit2 earlybal combat allareas fightsmoke fullbal econ settings-boxes savecompat saveslots actionlock freeactions perkcoverage titles titlepicker dojo cultivation capreach areagate) + TESTS=(audit audit2 earlybal combat allareas fightsmoke fullbal econ settings-boxes savecompat saveslots actionlock freeactions perkcoverage titles titlepicker dojo cultivation places capreach areagate) fi echo "serving $ROOT on port $PORT" From 66b1eed6527dc0c81c42569ab95b5dad050b2c49 Mon Sep 17 00:00:00 2001 From: Xeus24 <201404920+Xeus24@users.noreply.github.com> Date: Wed, 9 Sep 2026 22:45:20 -0700 Subject: [PATCH 19/44] Add a wiki, generated from the live game data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A `wiki` button on the bottom bar and a "Game wiki" row in settings, both opening an eleven-page reference in a tab of its own: Start here, Progression, Skills, Cultivation, Elemental mastery, Titles, Areas & enemies, Items, Actions, The dojo, Mechanics. It is generated from the running game every time it opens, and that is the whole design. 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. It reads the save too, so it doubles as a progress sheet, and it fetches nothing, so it works from file://. Groups collapse. Listed flat, three pages ran past 40,000px — Items 53,723, Titles 46,890, Skills 40,538. Collapsed the tallest is 5,826 and Skills opens as ten lines. Search opens the groups it matches inside, since a match hidden in a collapsed group reads as no match at all. Anything past 60 entries gets an alphabetical sub-split; the story titles split by rank instead, that being the axis they are already sorted on. Generating it rather than writing it turned up two things: * Spawn chances are not pop[i].c. z_bake normalises those weights into area.popc, the [lo,hi] bands mon_gen rolls against, and they need not sum to 1 — the Southern forest's .35/.45/.25 are really 33/43/24%. * Nothing can ever spawn in the Damp cellar (base game). Its pop entries carry no weight, so z_bake produces NaN bands and no comparison in the spawn loop can be true. Nothing travels to area.clg 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. area.nwh and area.tst are excluded outright: the first is where current_z parks when you are not fighting, the second hangs off a location with id -1 that is in no sector. tests/wiki.mjs, 57 checks. Coverage is counted from the game rather than from a list, so adding a skill without adding it to the wiki fails the suite on its own. The built document is parsed in a real browser, and the last check clicks the actual button and asserts a working tab comes up. Full suite green: 22 scripts, 2,490 matchups, 974 perks. Co-Authored-By: Claude Opus 5 --- CLAUDE.md | 28 + MOD-NOTES.md | 104 ++++ README.md | 12 +- changelog/mod-changelog.html | 31 + mod.js | 1049 +++++++++++++++++++++++++++++++++- tests/README.md | 1 + tests/run.sh | 2 +- tests/wiki.mjs | 368 ++++++++++++ 8 files changed, 1589 insertions(+), 6 deletions(-) create mode 100644 tests/wiki.mjs diff --git a/CLAUDE.md b/CLAUDE.md index a0fd813..cb4ff5d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -264,6 +264,34 @@ git diff origin/main -- changelog/changelog.html # must be empty In game the `changelog` button opens the mod's file; the version number opens his. +## The wiki + +Section 31. The `wiki` button on the bottom bar and the settings row both call +`modWiki()`, which builds a complete HTML document as a string and writes it +into `window.open('', '_blank')`. + +**It is generated from the live game data, and that is the point.** It reads +`skl`, `area`, `item`, `ttl`, `act` and the mod's own tables, so it cannot +disagree with the game. Never hand-write content into a page builder that could +be read off an object instead — `tests/wiki.mjs` counts coverage from the game +rather than from a list, so a skill added without a wiki entry fails the suite +on its own, and that only works while the pages stay derived. + +- `MOD_wikiGroup()` for anything list-shaped. With everything listed, three + pages ran past 40,000px; groups collapse by default and the tallest is now + 5,800. Anything past `MOD_WIKI_ALPHA_AT` gets an alphabetical sub-split. +- **Spawn shares come from `area.popc`, not `pop[i].c`.** `z_bake` normalises + those weights into the `[lo,hi]` bands `mon_gen` rolls against, and they do + not have to sum to 1 — the Southern forest's .35/.45/.25 are really 33/43/24%. +- `area.nwh` and `area.tst` are excluded: the first is where `current_z` parks + whenever you are not fighting, the second hangs off `chss.tst`, which has + `id -1` and is in no sector. `area.clg` **is** listed and flagged — its `pop` + entries carry no weight, so its bands are NaN and nothing can ever spawn + there. That is the author's unfinished content, reported rather than fixed. +- A page builder that throws is caught and says so in place; it must not take + the document down. +- No fetches, no CDN, no dependencies. It has to work from `file://`. + ## Actions The mod adds four actions. They are **earned**, not granted on a timer. Three diff --git a/MOD-NOTES.md b/MOD-NOTES.md index 49afb9d..83cd217 100644 --- a/MOD-NOTES.md +++ b/MOD-NOTES.md @@ -2130,6 +2130,110 @@ restore into the slots they were saved from and that appended perks sit after them, ungranted unless the level already earns them — which is what it checks now, and what will survive the next perk added. +## A wiki, generated rather than written + +Section 31. A `wiki` button on the bottom bar and a "Game wiki" row in +settings, both opening an eleven-page reference in a tab of its own. + +The whole design is one decision: **it is generated from the live game data +every time it is opened.** A hand-written wiki for 100 skills, 595 titles, 371 +items and 19 areas would be wrong within a week, and wrong in the worst way — a +reference nobody can trust is worse than no reference. Reading `skl`, `item`, +`area`, `ttl`, `act` and the mod's own tables means the page cannot disagree +with the game, including changes made from the console mid-session. + +It also reads the **save**, 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. + +Two things it deliberately does not do: + +* **It does not open inside the game.** The panel is a fixed layout with a fixed + bottom bar; a reference wants a sidebar and a wide table. +* **It does not fetch anything.** The document is a string written into the new + window, so it works from `file://`, from a subdirectory and from a server root + alike — the same reason `MOD_url` exists for the changelog. + +`modWiki()` returns the HTML as well as opening it, so the tests can assert on +the document without driving a popup. + +### Collapsing was not a nicety + +The first build listed everything flat. Measured, three of the eleven pages +were over 40,000 pixels tall — Items 53,723, Titles 46,890, Skills 40,538. That +is a hundred screens of scrolling, which is functionally the same as not having +the content. Groups became `
`, collapsed by default: + +| page | flat | collapsed | +|---|---|---| +| Items | 53,723px | 495px | +| Titles | 46,890px | 5,826px | +| Skills | 40,538px | 591px | + +Skills now opens as its ten sections on one screen. `
` rather than a +click handler because it works with JavaScript off, it is keyboard-reachable for +free, and the browser can be asked to reach inside it. + +Two consequences that had to be handled: + +* **Search must open the groups it matches inside.** A match hidden in a + collapsed group reads as no match at all, which is worse than having no + search. +* **A group can still be a wall.** 321 of the game's 371 plain items share one + inventory type, so grouping by type alone left a group of 321. Anything past + `MOD_WIKI_ALPHA_AT` gets an alphabetical sub-split, and the story titles — the + one title group with no skill to divide them — split by rank instead, since + that is the axis they are already sorted on. The test asserts no group holds + more than 60 entries, and it caught the story-titles group at 83 after I had + already fixed Items. + +The item groups use the game's **own** inventory tabs (`isort`: ALL/WPN/EQP/USE/ +OTHER) rather than new categories, so a group here is a tab there. Names are +filed under their first *alphanumeric* character — several items are literally +quoted, the master's manuals among them, and filing `"Sword Saint Manual"` under +a quotation mark helps nobody. + +### Generating it surfaced two things writing it would not have + +**Spawn chances were not what `pop[i].c` says.** The first draft printed those +weights as percentages. They are not percentages: `z_bake` normalises them into +`area.popc`, a list of `[lo,hi]` bands that `mon_gen` rolls a uniform random +against, and the weights do not have to sum to 1. The Southern forest's +`.35/.45/.25` sum to 1.05 and are really 33/43/24%. The wiki reads the bands. + +**Nothing can ever spawn in the Damp cellar.** Its two `pop` entries carry no +`c` at all, so `z_bake` computes `1 - NaN` and both bands come out `[NaN, NaN]`. +Every comparison in the spawn loop is then false, so no creature can be chosen — +and the area has `size: 33`, meaning a quest that needs 33 kills there could +never finish. Nothing in the game travels to `area.clg` either; like +`chss.pltwr1`, it is an unfinished idea of the author's. + +Left as it is and **reported in the wiki** rather than quietly fixed. The mod's +job here is to describe the game, and an area you cannot fight in is a fact +about the game worth stating. The flag is derived from the bands, not hard-coded +against the key, so it stays true if the data changes. + +Two areas are excluded outright, on the test of whether you can get to them: +`area.nwh` ("Somewhere") is where `current_z` parks whenever you are *not* in a +fight — `mon_gen` skips it explicitly with `area.id !== 101` — and `area.tst` +("Test") is reached only from `chss.tst`, which has `id -1` and is in no sector. +Those are machinery. `area.clg` is content, so it is listed with its flag. + +### Tested + +`tests/wiki.mjs`, 57 checks. The coverage assertions **count from the game** +rather than from a written list: every skill in `skl`, every area in `area`, +every item in `item`, `wpn`, `eqp`, `sld` and `acc`, every title in `ttl`, every +action, realm, technique, manual and pill, and all 974 perk lines. Adding a +skill without adding it to the wiki therefore fails the suite on its own, which +is the property that makes generating it worth more than writing it. + +The built document is then parsed in a real browser — a page that throws +halfway renders blank, and a string test would not notice — and the last check +clicks the actual bottom-bar button and asserts a real tab comes up with a +working page in it. + ## A changelog you can actually reach The game has a changelog and already links to it — the version number in the diff --git a/README.md b/README.md index 397b995..1448c3c 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,7 @@ edits appear not to take, which is confusing enough to be worth the flag. ## What it changes -Twenty-seven sections, roughly in the order they were built. The design record +Thirty-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). @@ -80,6 +80,10 @@ for every one of them, including the reasoning and what was measured, is in 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. Eleven pages covering every + skill, area, item, title, action and realm — generated from the live game + data each time it opens, so it cannot fall out of step, and reading your save, + so it doubles as a progress sheet. 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 @@ -103,7 +107,7 @@ by a stat ratio. ```bash npm install && npx playwright install chromium # once -npm test # all 19 scripts +npm test # all 22 scripts ./tests/run.sh audit combat # a subset ``` @@ -120,10 +124,10 @@ copy of the folder. | | | |---|---| -| `mod.js` | the entire mod, ~5,800 lines | +| `mod.js` | the entire mod, ~7,600 lines | | `MOD-NOTES.md` | the design record, section by section, with the reasoning | | `CLAUDE.md` | the constraints that are easy to violate by accident | -| `tests/` | 19 Playwright scripts | +| `tests/` | 22 Playwright scripts | | `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 | diff --git a/changelog/mod-changelog.html b/changelog/mod-changelog.html index 0e7bf49..a9d031d 100644 --- a/changelog/mod-changelog.html +++ b/changelog/mod-changelog.html @@ -18,6 +18,37 @@ Remove the <script src="mod.js"> line from index.html and none of it applies.

+----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,
diff --git a/mod.js b/mod.js index a7b584b..00a3c66 100644 --- a/mod.js +++ b/mod.js @@ -31,7 +31,7 @@ console.log('[mod] loading'); var MOD = { - version: '2.9', // v2.0: the ~100 "discovered by playing" skills consolidated + version: '3.0', // v2.0: the ~100 "discovered by playing" skills consolidated // to 10; per-stat effect budget unchanged. Survivors keep // their v1 id, so v1 saves still LOAD — merged-away skills // just don't restore. See "Balance, sixth pass" in @@ -6552,3 +6552,1050 @@ try { addtosector(sector.vmain1, chss.mod_pltwr); } catch (e) { /* sector option console.log('[mod] catacombs entrance (gated on ' + MOD.cata_gate + ') and the Pill Tower added to the Village Center'); + + +/* =========================================================================== + 31. THE WIKI + --------------------------------------------------------------------------- + A reference for the game, opened from the bottom bar and from the settings + menu, in a tab of its own. + + It is GENERATED FROM THE LIVE GAME DATA, every time it is opened, and that + is the whole design. A hand-written wiki for 200-odd skills, 500-odd titles, + 50 areas and 11 realms would be wrong within a week — and wrong in the worst + way, since a reference nobody can trust is worse than none. Reading `skl`, + `item`, `area`, `ttl`, `act` and the mod's own tables means the page cannot + disagree with the game: if a number changes here, the wiki changes with it, + including changes made from the console mid-session. + + It also reads the SAVE, so it doubles as a progress sheet — your level in + every skill, which perks you have taken, which titles you hold, what your + realm is and what the next one costs. The parts you have not reached are + still listed: this is a wiki, not a fog-of-war map. + + Two things it deliberately does not do: + + * It does not open inside the game. The panel is ~700px of a fixed layout + with a fixed bottom bar; a reference wants a sidebar and a wide table. + * It does not fetch anything. The document is built as a string and written + into the new window, so it works from file://, from a subdirectory and + from a server root alike — the same reason MOD_url exists for the + changelog. + + `modWiki()` returns the HTML rather than only opening it, so tests can assert + on the document without driving a popup. + =========================================================================== */ + +var MOD_WIKI = { + title: 'proto23 — wiki', + /* Nothing in here is user input: every string comes from the game file or + from this mod. The one real hazard of building a document as a string is a + literal inside content closing the block early, so that is the + one thing scrubbed. */ + safe: function (h) { + return String(h === undefined || h === null ? '' : h).replace(/<\/script/gi, '<\\/script'); + } +}; + +/* Descriptions carry real HTML (coloured spans, the game's separator div), so + they are passed through rather than escaped — but the separator becomes a + rule, since a wiki row is not a tooltip. */ +function MOD_wikiDesc(d) { + var s; + try { s = (typeof d === 'function') ? d() : d; } catch (e) { s = ''; } + if (!s) return ''; + s = String(s).split('
 
').join('
'); + return MOD_WIKI.safe(s); +} + +function MOD_wikiNum(n) { + if (!isFinite(n)) return '—'; + return Math.round(n).toLocaleString(); +} + +/* An effect line at a level the player has not reached yet. The effect + functions read the skill they are handed, so the level is borrowed and put + back; anything that throws simply gets no projection. */ +function MOD_wikiEffectAt(key, sk, lvl) { + var eff = MOD_EFFECTS[key]; + if (!eff) return ''; + var was = sk.lvl, out = ''; + try { sk.lvl = lvl; out = eff(sk) || ''; } + catch (e) { out = ''; } + finally { sk.lvl = was; } + return MOD_WIKI.safe(out); +} + +/* --- page builders -------------------------------------------------------- + Each returns the inner HTML of one page. `wk-e` marks a searchable entry and + `wk-g` a group that hides itself when all its entries are filtered out. + ------------------------------------------------------------------------- */ + +var MOD_WIKI_PAGES = []; + +function MOD_wikiPage(id, label, build) { + MOD_WIKI_PAGES.push({ id: id, label: label, build: build }); +} + +/* A collapsible group. + + Not decoration: with every skill, item and title listed, three of the eleven + pages ran past 40,000 pixels — a hundred screens of scrolling, which is the + same as not having the content at all. Collapsed, each of those pages opens + as an index you can read in one screen and expand where you want. + +
rather than a click handler, so it still works with JavaScript off, + the browser's own find can be made to reach inside it, and the summary is + keyboard-reachable for free. `level` is the heading level for the summary, + since a group is sometimes a section of a page and sometimes a subsection. */ +function MOD_wikiGroup(heading, count, inner, level) { + var hn = 'h' + (level || 2); + return '
<' + hn + '>' + heading + + (count !== null && count !== undefined + ? ' (' + count + ')' : '') + + '
' + inner + '
'; +} + +/* --- 1. orientation ------------------------------------------------------- */ +MOD_wikiPage('start', 'Start here', function () { + var cap = MOD_levelCap(); + var realm = MOD_realm(); + var h = '

proto23

' + + '

An idle RPG by truezhangwei, plus a mod. You train skills by ' + + 'doing things, the things you can do open up as the story does, and none of ' + + 'it is in a hurry.

' + + + '

Where you are right now

' + + '' + + '' + + '' + + '' + + '' + + '
Character level' + MOD_wikiNum(you.lvl) + '
Skill level cap' + cap + ' — ' + + MOD_WIKI.safe(MOD_CAP.name) + '
Cultivation realm' + MOD_WIKI.safe(realm.name) + + ' (' + realm.n + ' of ' + (MOD_REALMS.length - 1) + ')
Titles held' + MOD_titleCount() + ' — renown ' + + (skl.rnwn ? skl.rnwn.lvl : 0) + '
Coin' + MOD_wikiNum(you.wealth || 0) + '
' + + + '

The four things worth knowing early

' + + '
' + + '
Your skill levels are capped by the story, not by grinding.
' + + '
The cap starts at 10 and ends at 110, and it moves when you reach a new ' + + 'place — not when you earn enough experience. If a skill has stopped ' + + 'levelling, you are not doing it wrong; you need to go somewhere. See ' + + 'Progression.
' + + + '
Armour is subtraction, so levels matter more than they look.
' + + '
A hit does the attacker\'s STR minus the defender\'s STR. That means a ' + + 'small gap in strength is a large gap in damage, and a big enough one takes ' + + 'your damage to nothing at all rather than merely reducing it. See ' + + 'Mechanics.
' + + + '
Most skills have a parent that pulls them along.
' + + '
Every section has a parent skill that earns a share of everything its ' + + 'children do, and any child lagging behind the parent trains faster — up to ' + + 'twice as fast at ' + MOD_PARENT.maxGap + ' levels behind. A neglected skill is ' + + 'cheap to bring up later, which is the only reason having this many is ' + + 'workable.
' + + + '
Cultivation is a second ladder, and it is not automatic.
' + + '
Circulating qi levels a skill; the skill does not raise your realm. ' + + 'Reaching the level opens a bottleneck, and breaking through it costs a pill ' + + 'and can fail. See Cultivation.
' + + '
' + + + '

What is the mod and what is the game

' + + '

The game is index.html and it is entirely truezhangwei\'s ' + + 'work. Everything in mod.js is a modification of it and is not ' + + 'endorsed by the author. Pages here mark added content with ' + + 'mod where the distinction matters. The full ' + + 'list of changes is in the changelog, on the bottom bar.

'; + return h; +}); + +/* --- 2. progression ------------------------------------------------------- */ +MOD_wikiPage('progress', 'Progression', function () { + var cap = MOD_levelCap(); + var h = '

Progression

' + + '

Skill levels are capped by how far the story has gone. Ten ' + + 'rungs, 10 through 110. The cap applies to every skill at once; Renown is ' + + 'the single exception, since it is counted rather than trained.

' + + '' + + ''; + + MOD_TIERS.forEach(function (t) { + var got = null; + try { got = MOD_tierFlag(t); } catch (e) {} + var here = (t.cap === cap); + var need = t.needText ? t.needText + : (t.flags.length ? t.flags.join(' or ') : 'the start of the game'); + h += '' + + '' + + '' + + '' + + ''; + }); + h += '
CapStageWhat opens it
' + t.cap + '' + MOD_WIKI.safe(t.name) + '' + MOD_WIKI.safe(need) + '' + (here ? 'you are here' + : got ? 'reached' : '') + '
' + + + '

The experience curve

' + + '

The mod replaces the game\'s skill curve, which was super-exponential — ' + + 'level 109 to 110 alone cost 1.16×1014 experience, so the top ' + + 'of the ladder was unreachable by a factor of about ten trillion. It is now ' + + 'geometric:

' + + '
next level = ' + MOD_XP.base + ' × ' + MOD_XP.ratio + 'level
' + + '

tuned so a skill that ticks steadily reaches 110 in roughly six months at ' + + '1× speed. Character level uses the game\'s own curve, untouched.

' + + '' + + ''; + [1, 10, 25, 50, 60, 75, 90, 109].forEach(function (l) { + h += ''; + }); + h += '
LevelExperience for the next
' + l + '' + + MOD_wikiNum(MOD_XP.base * Math.pow(MOD_XP.ratio, l)) + '
'; + return h; +}); + +/* --- 3. skills ------------------------------------------------------------ */ +MOD_wikiPage('skills', 'Skills', function () { + var cap = MOD_levelCap(); + var bySection = {}; + var keys = []; + for (var k in skl) { + var s = skl[k]; + if (!s || typeof s !== 'object' || !s.name) continue; + keys.push(k); + } + keys.forEach(function (k) { + var s = skl[k]; + var sec = MOD_sectionOf(s); + (bySection[sec] = bySection[sec] || []).push(k); + }); + + var h = '

Skills

' + + '

' + keys.length + ' skills in ' + + Object.keys(MOD_SECTIONS).length + ' sections. Every one has perks at ' + + MOD_LADDER.join(', ') + ', and every one that has milestones grants titles at ' + + MOD_TITLE_RUNGS.join(', ') + '. Levels and ticks below are yours.

'; + + Object.keys(MOD_SECTIONS).forEach(function (sec) { + var list = bySection[sec]; + if (!list || !list.length) return; + var parentKey = MOD_PARENT_KEY[sec]; + var g = ''; + if (parentKey && skl[parentKey]) { + g += '

Parent skill: ' + + MOD_WIKI.safe(skl[parentKey].bname || skl[parentKey].name) + + ' — earns ' + Math.round(MOD_PARENT.feed * 100) + + '% of what its children earn, and speeds up any child that lags behind it.

'; + } + list.sort(function (a, b) { return skl[a].name < skl[b].name ? -1 : 1; }); + list.forEach(function (k) { + var s = skl[k]; + var lvl = Number(s.lvl) || 0; + g += '
' + + '

' + MOD_WIKI.safe(s.bname || s.name) + + (s.bname ? ' (' + MOD_WIKI.safe(s.name) + ')' : '') + + 'lv ' + lvl + + (lvl >= cap ? ' — at the cap' : '') + '

' + + '
' + MOD_wikiDesc(s.desc) + '
'; + + var at110 = MOD_wikiEffectAt(k, s, 110); + if (at110) g += '
At level 110: ' + at110 + '
'; + + if (s.mlstn && s.mlstn.length) { + g += ''; + s.mlstn.forEach(function (m) { + g += '' + + '' + + '' + + ''; + }); + g += '
' + m.lv + '' + MOD_WIKI.safe(m.p || '—') + '' + (m.g === true ? '✓' : '') + '
'; + } + g += '
'; + }); + h += MOD_wikiGroup(MOD_WIKI.safe(MOD_SECTIONS[sec]), list.length, g, 2); + }); + return h; +}); + +/* --- 4. cultivation ------------------------------------------------------- */ +MOD_wikiPage('cultivation', 'Cultivation', function () { + var now = MOD_realm(); + var elig = 0; + try { elig = MOD_realmEligible().n; } catch (e) {} + var qic = skl.qic ? skl.qic.lvl : 0; + + var h = '

Cultivation

' + + '

Ten realms above Mortal. Circulating qi levels the Qi ' + + 'Circulation skill; the skill does not advance the realm. Reaching the level ' + + 'opens a bottleneck, and breaking through it costs a pill and can fail — the ' + + 'pill is spent either way.

' + + '

Where you are

' + + '' + + '' + + '' + + '
Realm' + MOD_WIKI.safe(now.name) + '
Qi Circulationlevel ' + qic + '
Bottleneck open' + + (elig > now.n ? 'yes — ' + MOD_WIKI.safe(MOD_REALMS[now.n + 1].name) + + ' is within reach' : 'no') + '
' + + '

Realm thresholds are the same levels that earn a title rank, so realm N ' + + 'is the level at which a rank N title arrives. Every realm multiplies all ' + + 'stats and health, and the multipliers do not stack with each other — the ' + + 'realm you are in is the one that applies.

' + + '' + + ''; + + MOD_REALMS.forEach(function (r) { + var pill = r.n >= 1 ? item['mod_bp' + r.n] : null; + h += '' + + '' + + '' + + '' + + '' + + '' + + ''; + }); + h += '
RealmNameQi Circ.StatsHealthPill
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' + + MOD_WIKI.safe(r.desc) + '
' + r.qic + '×' + r.mult + '×' + r.hp + '' + (pill ? MOD_WIKI.safe(pill.name) : '—') + + '
' + + + '

Where the pills come from

' + + '

Every breakthrough pill has a source in normal play. None of them are ' + + 'craftable, and none are cheap — a bottleneck you can buy in bulk is not a ' + + 'bottleneck.

' + + '' + + '' + + '' + + '' + + '
RealmsSource
1The dojo instructor, when qi ' + + 'circulation is first taught
2–5The Herbalist in the village ' + + '(vendor.pha1), restocked
6–10The dojo\'s Level Advancement ' + + 'ladder, and the Pill Tower
' + + '

Realm 1 is deliberately not sold at the marketplace: ' + + 'the marketplace is behind a 40%-a-visit chance encounter, and realm 1 opens ' + + 'long before that is reliable.

' + + + '

Breaking through

' + + '

The odds improve the further past the threshold you are, and cap short of ' + + 'certain. A failure costs the pill and nothing else.

' + + '' + + ''; + [0, 2, 4, 6, 8].forEach(function (o) { + h += ''; + }); + h += '
Levels past the thresholdChance
+' + o + '' + + Math.round(Math.min(0.55 + o * 0.05, 0.95) * 100) + '%
'; + return h; +}); + +/* --- 5. elemental mastery ------------------------------------------------- */ +MOD_wikiPage('mastery', 'Elemental mastery', function () { + var h = '

Elemental mastery mod

' + + '

Six masteries, each with a technique that fires on its own ' + + 'during a fight. Combat is automatic and there is no ability picker, so a ' + + 'technique is a chance on each of your swings to become something else — one ' + + 'that scales with INT and the element rather than with your weapon.

' + + '

Nothing fires while you are still Mortal: the channels have to be open ' + + 'first. Chance is ' + (MOD_TECH.perLevel * 100).toFixed(1) + '% per mastery ' + + 'level plus ' + (MOD_TECH.perRealm * 100).toFixed(1) + '% per realm, capped at ' + + Math.round(MOD_TECH.cap * 100) + '%; a technique lands for ×' + + MOD_TECH.power + ' of a normal strike.

' + + '' + + ''; + + var realm = 0; + try { realm = MOD_realm().n; } catch (e) {} + MOD_MASTERY.forEach(function (m) { + var lvl = Number(m.skill.lvl) || 0; + var ch = realm < 1 ? 0 + : Math.min(lvl * MOD_TECH.perLevel + realm * MOD_TECH.perRealm, MOD_TECH.cap); + h += '' + + '' + + '' + + ''; + }); + h += '
MasteryTechniqueLevelChance now
' + + MOD_WIKI.safe(m.skill.name) + '' + MOD_WIKI.safe(m.name) + '' + lvl + '' + (realm < 1 ? 'Mortal' + : (ch * 100).toFixed(1) + '%') + '
'; + return h; +}); + +/* --- 6. titles ------------------------------------------------------------ */ +MOD_wikiPage('titles', 'Titles', function () { + var perm = MOD_permanentRank(); + var held = MOD_titleCount(); + var total = 0; for (var kk in ttl) if (ttl[kk] && ttl[kk].name) total++; + + var h = '

Titles

' + + '

A title\'s rank runs 1 to 10 and is derived from the level ' + + 'that earns it, so a rank is a statement about where in the game you are. ' + + 'Only the title you are wearing applies — until renown makes the lower ranks ' + + 'permanent.

' + + '

Where you are

' + + '' + + '' + + '' + + '
Titles held' + held + ' of ' + total + '
Renown level' + (skl.rnwn ? skl.rnwn.lvl : 0) + + ' of ' + MOD_RENOWN_MAX + '
Ranks that apply without wearing' + + (perm >= 1 ? '1–' + perm : 'none yet') + '
' + + + '

Ranks

' + + ''; + MOD_RANK_AT.forEach(function (lv, i) { + var st = MOD_rankStyle(i + 1); + h += '' + + ''; + }); + h += '
RankEarned from levelColour
' + (i + 1) + '' + lv + '' + '●●●' + '
' + + + '

Renown

' + + '

Renown is not trained. It counts the titles you hold, and each level ' + + 'multiplies every stat by ' + (1 + MOD_RENOWN_RATE) + ' — compounding, so ' + + 'level ' + MOD_RENOWN_MAX + ' is ×' + + (Math.round(MOD_renownMult(MOD_RENOWN_MAX) * 1000) / 1000) + '. At renown N, ' + + 'every title of rank N or below applies permanently.

' + + '' + + ''; + MOD_RENOWN_STEPS.forEach(function (n, i) { + h += '' + + ''; + }); + h += '
RenownTitles neededAll statsPassive ranks
' + (i + 1) + + '' + n + '×' + + (Math.round(MOD_renownMult(i + 1) * 1000) / 1000) + '1–' + (i + 1) + '
' + + + '

Every title

' + + '

Grouped by the skill that grants it. Titles with no skill of ' + + 'their own come from the story.

'; + + var groups = {}, loose = []; + for (var k in ttl) { + var t = ttl[k]; + if (!t || !t.name) continue; + var sk = MOD_TITLE_SKILL[k]; + if (sk && skl[sk]) (groups[sk] = groups[sk] || []).push(k); else loose.push(k); + } + var order = Object.keys(groups).sort(function (a, b) { + return skl[a].name < skl[b].name ? -1 : 1; + }); + + var row = function (k) { + var t = ttl[k]; + var st = MOD_rankStyle(t.rar); + var gen = null; + for (var i = 0; i < MOD_SKILL_TITLES.length; i++) { + if (MOD_SKILL_TITLES[i].key === k) { gen = MOD_SKILL_TITLES[i]; break; } + } + return '
' + + '' + + MOD_WIKI.safe(t.name) + '' + + 'rank ' + (t.rar || 1) + '' + + (gen ? 'lv ' + gen.rung + ' · +' + + Math.round(gen.xp * 100) + '% ' + MOD_WIKI.safe(skl[gen.skill].name) + + ' exp' : '') + + (t.have === true ? 'held' : '') + + '
' + MOD_wikiDesc(t.desc) + '
'; + }; + + var byRank = function (a, b) { return (ttl[a].rar || 1) - (ttl[b].rar || 1); }; + order.forEach(function (sk) { + groups[sk].sort(byRank); + h += MOD_wikiGroup(MOD_WIKI.safe(skl[sk].bname || skl[sk].name), + groups[sk].length, groups[sk].map(row).join(''), 3); + }); + if (loose.length) { + loose.sort(byRank); + /* The story titles are the one group with no skill to divide them, and + there are enough of them to need dividing. Rank rather than the alphabet: + it is the axis they are already sorted on, and the one a reader cares + about — a rank is a statement about where in the game a title comes from. */ + var inner; + if (loose.length <= MOD_WIKI_ALPHA_AT) { + inner = loose.map(row).join(''); + } else { + var byRankNo = {}; + loose.forEach(function (k) { + var r = Math.min(Math.max(Number(ttl[k].rar) || 1, 1), 10); + (byRankNo[r] = byRankNo[r] || []).push(k); + }); + inner = ''; + Object.keys(byRankNo).sort(function (a, b) { return a - b; }).forEach(function (r) { + var st = MOD_rankStyle(r); + inner += MOD_wikiGroup('Rank ' + r + '', byRankNo[r].length, + byRankNo[r].map(row).join(''), 4); + }); + } + h += MOD_wikiGroup('From the story', loose.length, inner, 3); + } + return h; +}); + +/* --- 7. areas ------------------------------------------------------------- */ +MOD_wikiPage('areas', 'Areas & enemies', function () { + var h = '

Areas & enemies

' + + '

Every area, what lives in it and at what levels, and what it ' + + 'drops. Enemy strength is not read off these levels — see the note below the ' + + 'table.

'; + + /* Two of the game's 21 areas are not places and listing them misleads: + nwh "Somewhere" — where global.current_z parks whenever you are NOT in + a fight, populated by creature.default ("Nothing"). + tst "Test" — reached only from chss.tst, which has id -1 and is in no + sector, so nothing in the game can travel there. + The test for "is this a place" is whether you can get to it, and neither + can be. Everything else is listed whether or not you have been. */ + var MOD_WIKI_NOT_PLACES = { nwh: 1, tst: 1 }; + + var keys = []; + for (var k in area) { + if (area[k] && area[k].pop && !MOD_WIKI_NOT_PLACES[k]) keys.push(k); + } + /* Ordered by the middle of the level band, which is the closest thing the + game has to a difficulty order. */ + keys.sort(function (a, b) { + var mid = function (z) { + if (!z.pop.length) return 0; + var s = 0; z.pop.forEach(function (e) { s += (e.lvlmin + e.lvlmax) / 2; }); + return s / z.pop.length; + }; + return mid(area[a]) - mid(area[b]); + }); + + keys.forEach(function (k) { + var z = area[k]; + /* An area whose baked bands are not finite can never spawn anything — see + the note rendered under its table. Derived rather than listed by name, so + it stays true if the game changes. */ + var broken = !z.popc || z.popc.length !== z.pop.length || + z.popc.some(function (band) { return !band || !isFinite(band[1] - band[0]); }); + h += '

' + MOD_WIKI.safe(z.name) + + ' ' + k + '' + + (broken ? 'nothing spawns' : '') + + '' + (z.size === -1 ? 'endless' : z.size + ' to clear') + + '

'; + if (z.pop.length) { + /* The spawn share is NOT pop[i].c. z_bake normalises those weights into + area.popc — a list of [lo,hi] bands that mon_gen rolls a uniform + random against — and the weights do not have to sum to 1, so they are + off wherever they don't. The Southern forest's .35/.45/.25 are really + 33/43/24%. Read the bands the game actually rolls against. */ + var bands = z.popc || []; + h += ''; + z.pop.forEach(function (e, i) { + var c = e.crt || {}; + var band = bands[i]; + var share = band ? band[1] - band[0] : NaN; + h += '' + + '' + + ''; + }); + h += '
' + MOD_WIKI.safe(c.name || '?') + 'lv ' + e.lvlmin + + (e.lvlmax !== e.lvlmin ? '–' + e.lvlmax : '') + '' + (isFinite(share) + ? Math.round(share * 100) + '% of spawns' + : '—') + '
'; + if (broken) { + h += '
Nothing spawns here. The area\'s entries carry ' + + 'no spawn weight, so the table the game rolls against comes out as ' + + 'NaN and no creature can ever match it. A bug in the base game, left ' + + 'as it is — nothing in the game travels to this area either.
'; + } + } + if (z.drop && z.drop.length) { + var d = z.drop.map(function (x) { + return MOD_WIKI.safe((x.item && x.item.name) || '?') + + ' ' + ((x.c || 0) * 100).toFixed(2) + '%'; + }).join(', '); + h += '
Drops: ' + d + '
'; + } + h += '
'; + }); + + h += '

How hard an enemy is

' + + '

Enemies are not fitted to a level. Each spawn is solved against your ' + + 'actual power at the moment it appears, aiming at roughly ' + MOD_ENEMY.kill + + ' swings to kill it and ' + MOD_ENEMY.die + ' before it kills you, with a ' + + 'guaranteed margin between the two so a fight is never a coin flip. Variety ' + + 'comes from the area\'s level band and each creature\'s own stat weighting, ' + + 'not from difficulty scaling.

' + + '

The reason for that: within the first stage alone you go from 1 STR to ' + + 'around 265, so anything anchored to a level is either trivial at the end of ' + + 'a stage or impossible at the start of one.

'; + return h; +}); + +/* --- 8. items ------------------------------------------------------------- + The one place where "list everything" needs help: 321 of the game's 371 + plain items are the same inventory type, so grouping by type alone leaves a + single group of 321. Anything past MOD_WIKI_ALPHA_AT gets an alphabetical + sub-split as well — the oldest index pattern there is, and derived rather + than invented, which a hand-made taxonomy would not be. + ------------------------------------------------------------------------- */ + +var MOD_WIKI_ALPHA_AT = 60; + +/* The game's own inventory tabs (isort): ALL / WPN / EQP / USE / OTHER. Using + its categories rather than new ones means a group here is a tab there. */ +var MOD_WIKI_STYPE = { 2: 'Weapons', 3: 'Equipment', 4: 'Usable', 5: 'Other' }; + +function MOD_wikiItemRow(it) { + var val = it.v !== undefined ? it.v : (it.val !== undefined ? it.val : null); + return '
' + + '

' + MOD_WIKI.safe(it.name) + + (it.rar > 1 ? 'rarity ' + it.rar + '' : '') + + (val !== null ? '' + MOD_wikiNum(val) + ' coin' : '') + + (it.amount > 0 ? '×' + it.amount + '' : '') + + '

' + MOD_wikiDesc(it.desc) + '
'; +} + +/* The letter a name files under. Several items are quoted — the master's + manuals are literally named "Sword Saint Manual" — and filing those under a + quotation mark is no use to anyone looking for S. */ +function MOD_wikiLetter(name) { + var m = String(name || '').replace(/^[^0-9A-Za-z]+/, ''); + return (m.charAt(0) || '?').toUpperCase(); +} + +function MOD_wikiByLetter(a, b) { + var x = MOD_wikiLetter(a.name) + String(a.name || '').toLowerCase(); + var y = MOD_wikiLetter(b.name) + String(b.name || '').toLowerCase(); + return x < y ? -1 : (x > y ? 1 : 0); +} + +/* Flat below the threshold, alphabetically sub-grouped above it. Ranges are cut + from the entries actually present, so they stay even as content is added. */ +function MOD_wikiAlpha(entries, render, level) { + if (entries.length <= MOD_WIKI_ALPHA_AT) return entries.map(render).join(''); + var per = Math.ceil(entries.length / Math.ceil(entries.length / 40)); + var out = ''; + for (var i = 0; i < entries.length; i += per) { + var chunk = entries.slice(i, i + per); + var first = MOD_wikiLetter(chunk[0].name); + var last = MOD_wikiLetter(chunk[chunk.length - 1].name); + out += MOD_wikiGroup(first === last ? first : first + '–' + last, + chunk.length, chunk.map(render).join(''), level || 4); + } + return out; +} + +MOD_wikiPage('items', 'Items', function () { + var h = '

Items

' + + '

Everything the game defines, whether or not you have seen ' + + 'it. Grouped the way your inventory groups it. Values are the base sell ' + + 'price where the game gives one.

'; + + /* item[] first, split by the inventory tab it appears under. */ + var buckets = {}; + for (var k in item) { + var it = item[k]; + if (!it || !it.name || it.name === 'dummy') continue; + var lab = MOD_WIKI_STYPE[it.stype] || 'Uncategorised'; + (buckets[lab] = buckets[lab] || []).push(it); + } + ['Usable', 'Other', 'Weapons', 'Equipment', 'Uncategorised'].forEach(function (lab) { + var list = buckets[lab]; + if (!list || !list.length) return; + list.sort(MOD_wikiByLetter); + h += MOD_wikiGroup(lab, list.length, + MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2); + }); + + /* and the equipment objects, which live in their own globals */ + [['Weapons', wpn], ['Armour', eqp], ['Shields', sld], ['Accessories', acc]] + .forEach(function (g) { + var list = []; + for (var kk in g[1]) { + if (g[1][kk] && g[1][kk].name && g[1][kk].name !== 'dummy') list.push(g[1][kk]); + } + if (!list.length) return; + list.sort(MOD_wikiByLetter); + h += MOD_wikiGroup(g[0], list.length, + MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2); + }); + return h; +}); + +/* --- 9. actions ----------------------------------------------------------- */ +MOD_wikiPage('actions', 'Actions', function () { + var unlock = {}; + MOD_ACTION_UNLOCKS.forEach(function (u) { + if (u.act) unlock[u.act.id] = (skl[u.skill] ? skl[u.skill].name : u.skill) + + ' level ' + u.lv; + }); + if (typeof act.mod_qi !== 'undefined') { + unlock[act.mod_qi.id] = 'clearing the dojo\'s Easiest, Easy and Normal fights'; + } + + var h = '

Actions

' + + '

What you can be doing. One at a time, and only where the ' + + 'action makes sense — unless "Unrestricted actions" is ticked in settings, ' + + 'which lets several run at once anywhere. Both limits are deliberate; the ' + + 'switch is there because they are not to everyone\'s taste.

'; + + var keys = []; + for (var k in act) if (act[k] && act[k].name && act[k].name !== 'dummy') keys.push(k); + keys.sort(function (a, b) { return act[a].name < act[b].name ? -1 : 1; }); + + keys.forEach(function (k) { + var a = act[k]; + var how = unlock[a.id]; + h += '
' + + '

' + MOD_WIKI.safe(a.name) + + (a.have === true ? 'unlocked' + : 'locked') + + (how ? 'from ' + MOD_WIKI.safe(how) + '' : '') + + '

' + MOD_wikiDesc(a.desc) + '
'; + }); + return h; +}); + +/* --- 10. the dojo --------------------------------------------------------- */ +MOD_wikiPage('dojo', 'The dojo', function () { + var h = '

The dojo

' + + '

The instructor promises a reward every five levels. The base ' + + 'game stops delivering at 30; the mod carries the same ladder to 110, on the ' + + 'same terms.

' + + '

Only the lowest rung you have not claimed is offered, which is how the ' + + 'first six behave. Rewards are sized against the character experience curve ' + + '(4×level3 + level2) — a level at 110 costs 5.3 ' + + 'million, which is why the base game\'s best pill is worth 0.3% of one there.

' + + '' + + ''; + + MOD_DOJO_RUNGS.forEach(function (r) { + var pill = item[r.pill]; + var extra = []; + if (r.realmPill) extra.push('breakthrough pill for realm ' + r.realmPill); + if (r.manual) extra.push('a choice of master\'s manual'); + h += '' + + '' + + '' + + '' + + '' + + ''; + }); + h += '
LevelRewardCoinAlso
' + r.lv + '' + (pill ? MOD_WIKI.safe(pill.name) + ' ×' + r.n + : '—') + '' + MOD_wikiNum(r.coin) + '' + (extra.join(', ') || '—') + '' + (global.flags[r.flag] ? 'claimed' : '') + + '
' + + + '

Spirit pills

' + + '

Character experience in a bottle. The four the mod adds continue the ' + + 'game\'s own series up to the new curve.

' + + '' + + ''; + MOD_PILLS.forEach(function (p) { + h += ''; + }); + h += '
PillExperience
' + MOD_WIKI.safe(p[2]) + '' + + p[3].toLocaleString() + '
' + + + '

Master\'s manuals

' + + '

A third grade of skillbook, offered at levels 50, 75 and 100 as a choice ' + + 'the same way the first one was. Each is worth +' + + Math.round(MOD_MANUAL_RATE * 100) + '% experience in its skill, permanently.

' + + '' + + ''; + MOD_MANUALS.forEach(function (m) { + h += ''; + }); + h += '
ManualSkill
' + MOD_WIKI.safe(m[2]) + '' + + MOD_WIKI.safe(skl[m[0]] ? (skl[m[0]].bname || skl[m[0]].name) : m[0]) + + '
'; + return h; +}); + +/* --- 11. mechanics -------------------------------------------------------- */ +MOD_wikiPage('mechanics', 'Mechanics', function () { + return '

Mechanics

' + + + '

Damage is subtractive, in both directions

' + + '

A hit does the attacker\'s strength minus the defender\'s strength, plus ' + + 'the weapon, plus one, before criticals. A defender\'s STR is their ' + + 'armour — there is no separate armour stat doing that job.

' + + '

Two consequences worth carrying around:

' + + '
    ' + + '
  • Damage does not taper, it stops. Once a defender\'s STR passes ' + + 'yours, your hits do not get small; they land for nothing. Progress against ' + + 'something too strong is not slow, it is zero.
  • ' + + '
  • A stat ratio tells you nothing. Comparing your HP to an enemy\'s ' + + 'strength ignores the subtraction entirely and reads several times off in ' + + 'either direction.
  • ' + + '
' + + '

This is also the reason enemies are never scaled by raising ' + + 'their strength: it would raise their offence and their armour together, and ' + + 'the second effect arrives first.

' + + + '

Criticals carry the late game

' + + '

Critical rate reaches about a third at the top of the ladder and a ' + + 'critical is roughly eight times a normal swing, so around three quarters of ' + + 'all damage done at that point is critical damage. Anything that raises ' + + 'critical rate or critical damage is worth more than it looks; Sharp Eye and ' + + 'War are the two that do.

' + + + '

Hit chance

' + + '

Landing a hit is a contest of agility. It is a ratio rather than a ' + + 'subtraction, so agility behaves more gently than strength does — falling ' + + 'behind on it costs you accuracy, not everything.

' + + + '

Skill sections and parents

' + + '

Each of the ' + Object.keys(MOD_SECTIONS).length + ' sections has a parent ' + + 'skill. A parent earns ' + Math.round(MOD_PARENT.feed * 100) + '% of everything ' + + 'its children earn and grants no stats of its own. In return, a child below ' + + 'the parent\'s level trains ' + Math.round(MOD_PARENT.perLevel * 100) + + '% faster per level of lag, up to ' + MOD_PARENT.maxGap + ' levels behind. ' + + 'Bringing one section up makes every skill in it cheaper to catch up.

' + + + '

Saving

' + + '

Three slots, switched from the bottom bar. The game builds its world once ' + + 'at startup and cannot unload a save, so switching slots or starting a new ' + + 'game reloads the page. Exporting a save before running anything unusual is ' + + 'always the safe move.

'; +}); + +/* --- the document --------------------------------------------------------- + One string, no fetches, no dependencies: this has to work from file:// as + readily as from a server. + ------------------------------------------------------------------------- */ + +function MOD_wikiCss() { + return [ + ':root{color-scheme:dark}', + '*{box-sizing:border-box}', + 'body{margin:0;background:#060a1c;color:#dfe6f5;font:14px/1.6 "MS Gothic",' + + '"MS Mincho",monospace;display:flex;min-height:100vh}', + 'a{color:#7cb0ff}', + 'nav{width:210px;flex:0 0 210px;background:#081040;border-right:1px solid #46a;' + + 'position:sticky;top:0;height:100vh;overflow:auto;padding:14px 0}', + 'nav h2{font-size:13px;color:#9fb4d8;margin:0 0 10px 14px;letter-spacing:1px}', + 'nav a{display:block;padding:6px 14px;text-decoration:none;color:#cfe0ff;' + + 'border-left:3px solid transparent}', + 'nav a:hover{background:#154080}', + 'nav a.on{background:#123070;border-left-color:#7cb0ff;color:#fff}', + 'nav .srch{margin:0 10px 12px;width:calc(100% - 20px);background:#050a20;' + + 'color:#dfe6f5;border:1px solid #46a;padding:5px;font:inherit}', + 'nav .xall{display:block;margin:12px 10px 0;color:#8296b8;font-size:12px;' + + 'cursor:pointer;user-select:none}', + 'nav .xall input{vertical-align:-1px;margin-right:4px}', + 'details.wk-g{border:1px solid #17224a;background:#0a1130;margin:8px 0}', + 'details.wk-g>summary{cursor:pointer;padding:7px 12px;list-style:none;' + + 'background:#0d1740}', + 'details.wk-g>summary:hover{background:#154080}', + 'details.wk-g>summary::-webkit-details-marker{display:none}', + // a caret drawn rather than borrowed, so it is the same in every browser + 'details.wk-g>summary::before{content:"\\25B8";color:#7cb0ff;' + + 'display:inline-block;width:1em;transition:transform .12s}', + 'details.wk-g[open]>summary::before{transform:rotate(90deg)}', + 'details.wk-g>summary h2,details.wk-g>summary h3{display:inline;margin:0;' + + 'font-size:15px;color:#fff}', + '.wk-gi{padding:2px 12px 10px}', + 'details.wk-g details.wk-g>summary h4{display:inline;margin:0;font-size:13px;' + + 'color:#cfe0ff;font-weight:normal}', + 'details.wk-g details.wk-g>summary{padding:5px 10px;background:#0b1435}', + 'main{flex:1;min-width:0;padding:22px 30px 80px;max-width:1000px}', + 'h1{font-size:26px;margin:0 0 6px;color:#fff;border-bottom:1px solid #46a;' + + 'padding-bottom:8px}', + 'h2{font-size:19px;color:#bcd4ff;margin:30px 0 8px}', + 'h3{font-size:15px;color:#fff;margin:0 0 4px}', + 'p.lede{color:#a8bcdc;margin:0 0 18px}', + '.dim{color:#8296b8;font-weight:normal}', + '.flav{font-size:12px;font-style:italic}', + '.note{background:#0b1638;border-left:3px solid #46a;padding:8px 12px;' + + 'color:#a8bcdc;margin:14px 0}', + 'code,pre{background:#0b1638;border:1px solid #24356e;padding:1px 5px;' + + 'color:#9fd0ff}', + 'pre{padding:10px;overflow-x:auto}', + 'dl dt{color:#fff;margin-top:14px}', + 'dl dd{margin:2px 0 0 0;color:#a8bcdc}', + 'ul{color:#a8bcdc}', + 'table.wk-tbl{border-collapse:collapse;width:100%;margin:10px 0;' + + 'display:block;overflow-x:auto}', + 'table.wk-tbl th{text-align:left;color:#9fb4d8;font-weight:normal;' + + 'border-bottom:1px solid #46a;padding:5px 9px;white-space:nowrap}', + 'table.wk-tbl td{border-bottom:1px solid #17224a;padding:5px 9px;' + + 'vertical-align:top}', + 'table.wk-tbl tr.done td{color:#cfe0ff}', + 'table.wk-tbl tr.here{background:#123070}', + '.num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}', + '.wk-e{border:1px solid #17224a;background:#081040;padding:9px 12px;' + + 'margin:7px 0;border-radius:2px}', + 'tr.wk-e{border:0;background:none}', + '.wk-e.done{border-color:#2a4a8c}', + '.wk-e h3{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}', + '.lv{margin-left:auto;font-size:12px;color:#8296b8;font-weight:normal}', + '.lv.capped{color:#ffd24a}', + '.rank{font-size:12px;color:#8296b8;font-weight:normal}', + '.desc{color:#a8bcdc;font-size:13px}', + '.proj{color:#ffd24a;font-size:12px;margin-top:3px}', + '.tag{font-size:11px;border:1px solid #46a;color:#9fb4d8;padding:0 5px;' + + 'border-radius:2px;font-weight:normal}', + '.tag.mod{border-color:#7a4ea8;color:#c07bff}', + '.tag.done{border-color:#2e7d4f;color:#6fdc9a}', + '.tag.here{border-color:#ffd24a;color:#ffd24a}', + '.tag.warn{border-color:#a8562e;color:#ff9a5c}', + 'table.wk-perks{border-collapse:collapse;margin:6px 0 0;width:100%}', + 'table.wk-perks td{padding:2px 8px 2px 0;font-size:12px;color:#8296b8;' + + 'border-bottom:1px solid #101a3c}', + 'table.wk-perks tr.done td{color:#cfe0ff}', + 'table.wk-perks td.tick{color:#6fdc9a;width:1em;text-align:right}', + // fixed columns so the level bands line up between one area and the next + 'table.wk-pop{table-layout:fixed}', + 'table.wk-pop td:first-child{width:60%}', + 'table.wk-pop td:nth-child(2){width:20%}', + '.wk-title .tname{font-weight:bold}', + '.wk-now{background:#0b1638;border:1px solid #24356e;padding:10px 14px;' + + 'margin:0 0 18px;display:inline-block;min-width:320px}', + '.wk-now h3{color:#9fb4d8;font-weight:normal;font-size:13px;' + + 'letter-spacing:1px;margin-bottom:6px}', + '.wk-now td{padding:2px 16px 2px 0}', + '.wk-now td:last-child{color:#fff;text-align:right}', + '.pg{display:none}.pg.on{display:block}', + 'footer{color:#5a6c8c;font-size:12px;border-top:1px solid #17224a;' + + 'margin-top:40px;padding-top:12px}', + '@media(max-width:760px){body{display:block}nav{width:auto;height:auto;' + + 'position:static;flex:none}main{padding:16px}}' + ].join('\n'); +} + +/* Filtering, page switching and the hash. Small enough to inline, and inlining + it is the point — the document has to stand on its own. */ +function MOD_wikiJs() { + return [ + 'var pages=[].slice.call(document.querySelectorAll(".pg"));', + 'var links=[].slice.call(document.querySelectorAll("nav a[data-p]"));', + 'function show(id){', + ' var found=false;', + ' pages.forEach(function(p){var on=p.id==="pg-"+id;if(on)found=true;', + ' p.className="pg"+(on?" on":"")});', + ' if(!found&&pages.length){pages[0].className="pg on";id=pages[0].id.slice(3)}', + ' links.forEach(function(a){a.className=a.dataset.p===id?"on":""});', + ' window.scrollTo(0,0);return id;}', + 'links.forEach(function(a){a.addEventListener("click",function(e){', + ' e.preventDefault();location.hash=a.dataset.p;show(a.dataset.p)})});', + 'window.addEventListener("hashchange",function(){show(location.hash.slice(1))});', + 'show(location.hash.slice(1)||"start");', + // Search filters entries, hides groups left with none, and OPENS the groups + // that still have some — a match hidden inside a collapsed group reads as + // no match at all, which is worse than not having search. + 'var box=document.getElementById("srch");', + 'var xall=document.getElementById("xall");', + 'var groups=[].slice.call(document.querySelectorAll("details.wk-g"));', + 'function filter(){', + ' var q=box.value.trim().toLowerCase();', + ' pages.forEach(function(p){', + ' [].slice.call(p.querySelectorAll(".wk-e")).forEach(function(e){', + ' e.style.display=(!q||e.textContent.toLowerCase().indexOf(q)>=0)?"":"none"})});', + ' groups.forEach(function(g){', + ' var any=[].slice.call(g.querySelectorAll(".wk-e")).some(function(e){', + ' return e.style.display!=="none"});', + ' g.style.display=any?"":"none";', + ' g.open=xall.checked||(!!q&&any)}); }', + 'box.addEventListener("input",filter);', + 'box.addEventListener("keydown",function(e){if(e.key==="Escape"){box.value="";filter()}});', + // Everything is collapsed by default because three of these pages run past + // 40,000px open. This is the way back to one long document, which is what + // the browser's own find needs. + 'xall.addEventListener("change",filter);' + ].join('\n'); +} + +function MOD_wikiBuild() { + var nav = '', body = ''; + MOD_WIKI_PAGES.forEach(function (pg) { + nav += '' + + MOD_WIKI.safe(pg.label) + ''; + var inner; + /* One page throwing must not take the wiki down with it — the page says so + and the rest still renders. */ + try { inner = pg.build(); } + catch (e) { + inner = '

' + MOD_WIKI.safe(pg.label) + '

This page ' + + 'could not be built: ' + MOD_WIKI.safe(e.message) + '

'; + console.warn('[mod] wiki page "' + pg.id + '" failed: ' + e.message); + } + body += '
' + inner + '
'; + }); + + var stamp = new Date().toLocaleString(); + return '' + + '' + + '' + MOD_WIKI.safe(MOD_WIKI.title) + '' + + '' + + '' + + '
' + body + + '
Generated from the running game at ' + MOD_WIKI.safe(stamp) + + ' — mod ' + MOD_WIKI.safe(MOD.version) + '. Everything here is read from the ' + + 'live data, including your own save, so it cannot fall out of step with the ' + + 'game. The game is truezhangwei\'s; the mod is not endorsed by the author.' + + '
' + + '' +if [ -z "$removed" ] && [ "$added" == "$expected" ]; then + echo "ok index.html is the author's plus the one script tag" +else + echo "FAIL index.html changes more than the script tag against $REF:" + printf '%s\n' "$diff_out" | head -40 + fail=1 +fi +exit $fail diff --git a/tests/backups.mjs b/tests/backups.mjs index 165fc9c..6a94c88 100644 --- a/tests/backups.mjs +++ b/tests/backups.mjs @@ -1,4 +1,4 @@ -import { chromium } from 'playwright'; +import { launch } from './lib/browser.mjs'; import fs from 'node:fs'; // Save backups (sections 0 and 44). @@ -14,7 +14,7 @@ import fs from 'node:fs'; // PORT=8080 node tests/backups.mjs const HOST = `http://127.0.0.1:${process.env.PORT || 8080}`; -const b = await chromium.launch(process.env.CHROMIUM ? { executablePath: process.env.CHROMIUM } : {}); +const b = await launch(); const p = await b.newPage(); const errs = []; p.on('pageerror', e => errs.push(String(e).slice(0, 200))); p.on('dialog', d => d.accept()); @@ -39,15 +39,28 @@ console.log('\n--- a new version backs up the save it is about to load'); const blob = await p.evaluate(() => { you.name = 'Alpha'; save(); localStorage.setItem(MOD.backup_ver, '4.2'); return localStorage.getItem('v0.3'); }); await p.reload({ waitUntil: 'load' }); await settle(); -const after = await p.evaluate(() => { const l = MOD_backupList(); +const after = await p.evaluate(() => { const l = MOD_backupList().filter(b => b.n <= MOD.backup_count); + const daily = MOD_backupRead(MOD.backup_daily); return { n: l.length, reason: l[0] && l[0].rec.reason, live: l[0] && l[0].rec.live, num: l[0] && l[0].n, - ver: localStorage.getItem(MOD.backup_ver) }; }); -check(after.n === 1, `one backup made (${after.n})`); + ver: localStorage.getItem(MOD.backup_ver), daily: daily && daily.reason, dailyLive: daily && daily.live }; }); +check(after.n === 1, `one version backup made (${after.n})`); check(after.live === blob, 'holding exactly the save the game was about to read'); check(/from v4\.2/.test(after.reason || ''), `and saying why (${after.reason})`); +check(after.daily === 'daily' && after.dailyLive === blob, 'and the first daily backup, in its own place'); await p.reload({ waitUntil: 'load' }); await settle(); -const again = await p.evaluate(() => MOD_backupList().length); -check(again === 1, `the same version does not back up again (${again})`); +const again = await p.evaluate(() => ({ n: MOD_backupList().filter(b => b.n <= MOD.backup_count).length, + dailyT: MOD_backupRead(MOD.backup_daily).t })); +check(again.n === 1, `the same version does not back up again (${again.n})`); + +console.log('\n--- the daily backup'); +await p.evaluate(() => { const r = MOD_backupRead(MOD.backup_daily); r.t = Date.now() - 2 * 86400000; + localStorage.setItem(MOD.backup_key + MOD.backup_daily, JSON.stringify(r)); }); +await p.reload({ waitUntil: 'load' }); await settle(); +const d1 = await p.evaluate(() => MOD_backupRead(MOD.backup_daily).t); +await p.reload({ waitUntil: 'load' }); await settle(); +const d2 = await p.evaluate(() => MOD_backupRead(MOD.backup_daily).t); +check(Date.now() - d1 < 60000, 'a day-old daily backup is replaced on the next load'); +check(d2 === d1, 'and not again the same day'); console.log('\n--- restoring puts the character back, and can be undone'); await p.evaluate(() => { you.name = 'Beta'; save(); }); @@ -62,11 +75,15 @@ check(restored.undo && /Beta/.test(restored.undoWho), `and the one it replaced w console.log('\n--- three kept, oldest replaced'); const rot = await p.evaluate(() => { + const dailyBefore = MOD_backupRead(MOD.backup_daily).seq; for (let i = 0; i < 5; i++) MOD_makeBackup('rotation ' + i); - return MOD_backupList().map(b => b.rec.reason); + const l = MOD_backupList(); + return { reasons: l.filter(b => b.n <= MOD.backup_count).map(b => b.rec.reason), + dailyKept: MOD_backupRead(MOD.backup_daily).seq === dailyBefore }; }); -check(rot.length === 3, `${rot.length} backups kept`); -check(rot.includes('rotation 4') && !rot.includes('rotation 0'), `the newest survive (${rot.join(' / ')})`); +check(rot.dailyKept, 'rotation never touches the daily backup'); +check(rot.reasons.length === 3, `${rot.reasons.length} backups kept`); +check(rot.reasons.includes('rotation 4') && !rot.reasons.includes('rotation 0'), `the newest survive (${rot.reasons.join(' / ')})`); console.log('\n--- no room is not an error'); const full = await p.evaluate(() => { @@ -85,6 +102,22 @@ check(!full.threw && full.n === 0, 'a full localStorage skips the backup quietly check(full.before - 1 <= full.after && full.after >= 2, `and costs at most the one it was replacing (${full.before} -> ${full.after}); it once deleted them all`); +console.log('\n--- a backup as a file, and back'); +const dl = p.waitForEvent('download'); +const want = await p.evaluate(() => { const b = MOD_backupList()[0]; MOD_downloadBackup(b.n); return b.rec.live; }); +const file = await dl; +const text = await (await import('node:fs')).promises.readFile(await file.path(), 'utf8'); +let parsed = null; try { parsed = JSON.parse(text); } catch (e) {} +check(/^proto23-backup-\d{4}-\d\d-\d\d-v[\d.]+\.json$/.test(file.suggestedFilename()), `downloads as ${file.suggestedFilename()}`); +check(parsed && parsed.live === want, 'holding the save itself'); +await p.evaluate(() => { MOD_pickBackupFile(); }); +await p.setInputFiles('#mod_backup_file', { name: 'b.json', mimeType: 'application/json', buffer: Buffer.from(text) }); +await p.waitForTimeout(400); +const loaded = await p.evaluate(w => MOD_backupList().some(b => /from a file/.test(b.rec.reason) && b.rec.live === w), want); +check(loaded, 'and a downloaded file loads back into the list, ready to restore'); +const junk = await p.evaluate(() => MOD_loadBackupText('{"hello":1}')); +check(junk === 0, 'a file that is not a backup is refused'); + console.log('\n--- the panel'); const panel = await p.evaluate(() => { MOD_toggleSlotPanel(true); const t = MOD_SLOTUI.panel.textContent; diff --git a/tests/basefixes.mjs b/tests/basefixes.mjs new file mode 100644 index 0000000..7c572ba --- /dev/null +++ b/tests/basefixes.mjs @@ -0,0 +1,118 @@ +import { launch } from './lib/browser.mjs'; + +// Section 46's three fixes to the author's code, and section 40's shop prices. +// +// Death energy kept on death rises with the skill instead of falling to +// zero at level 10 and below zero after it +// Shield the defence bracket is floored at zero, so defence can no longer be +// added to the damage you take -- and nothing changes where it was fine +// Luck the author's crit multiplier reaches the roll, measured through the +// real attack path, capped at 50% from luck, and seen by the enemy +// model's probe +// +// PORT=8080 node tests/basefixes.mjs + +const HOST = `http://127.0.0.1:${process.env.PORT || 8080}`; +const b = await launch(); +const p = await b.newPage(); +const errs = []; p.on('pageerror', e => errs.push(String(e).slice(0, 200))); +await p.goto(`${HOST}/index.html`, { waitUntil: 'load' }); +await p.waitForTimeout(4500); + +const fail = []; +const check = (c, what) => { if (!c) fail.push(what); console.log(` ${c ? 'ok ' : 'FAIL'} ${what}`); }; + +await p.evaluate(() => { + window.__real = random; + window.dice = v => { random = function () { return v; }; }; + window.undice = () => { random = window.__real; }; + window.__gse = giveSkExp; giveSkExp = function () {}; +}); + +console.log('--- Death'); +const death = await p.evaluate(() => { + const die = (lvl) => { skl.dth.lvl = lvl; skl.dth.exp = 0; you.satmax = 200; you.sat = 100; you.hp = 0; + you.res.death = 1; const d0 = global.stat.deadt; you.onDeath(); you.alive = true; + return { lvl, died: global.stat.deadt > d0, kept: you.sat / 100 }; }; + const rows = [0, 5, 10, 11, 20, 60, 110].map(die); + // the "you avoid death" branch runs the same function without dying + skl.dth.lvl = 20; you.sat = 100; you.res.death = 0; dice(0.5); you.onDeath(); undice(); + const avoided = you.sat; you.res.death = 1; + return { rows, avoided, original10: 0.55 * (1 - 10 * 0.1), original11: 0.55 * (1 - 11 * 0.1) }; +}); +death.rows.forEach(r => console.log(` level ${String(r.lvl).padStart(3)}: keeps ${(r.kept * 100).toFixed(1)}%`)); +check(Math.abs(death.rows[0].kept - 0.55) < 1e-9, 'level 0 keeps 55%, exactly as the original did'); +check(death.rows.every((r, i, a) => i === 0 || r.kept > a[i - 1].kept), 'and it only improves with level'); +check(death.rows.every(r => r.kept > 0 && r.kept < 1), 'never nothing, never everything'); +check(death.original10 === 0 && death.original11 < 0, + `where the original kept ${death.original10} at level 10 and ${death.original11.toFixed(3)} at 11`); +check(death.avoided === 100, 'avoiding death leaves your energy alone'); + +console.log('\n--- Shield Mastery'); +const shield = await p.evaluate(() => { + const z = area.frstn1a2; global.current_z = z; + const m = mon_gen(z.pop[0].crt); lvlup(m, 4); global.current_m = m; global.target = you.eqp[2]; global.t_n = 2; + you.eqp[1].aff = you.eqp[1].aff || {}; const at = m.atype, keep = you.eqp[1].aff[at]; + const hit = (fn) => { dice(0.5); try { return fn(m, you, abl.default); } finally { undice(); } }; + // fine before: a positive bracket must come out exactly as the original + you.eqp[1].aff[at] = 1; skl.shdc.lvl = 5; + const fineOrig = hit(MOD_dmg_calc_before_fixes), fineFixed = hit(MOD_dmg_calc_original); + // broken: a negative bracket + you.eqp[1].aff[at] = 4; skl.shdc.lvl = 110; + const floor = MOD_shieldFloorLevel(m); + const badOrig = hit(MOD_dmg_calc_before_fixes), badFixed = hit(MOD_dmg_calc_original); + // at the floor the whole defence term is zero: what is left is the attack alone + skl.shdc.lvl = floor; const atFloor = hit(MOD_dmg_calc_before_fixes); skl.shdc.lvl = 110; + const restored = skl.shdc.lvl; + you.eqp[1].aff[at] = keep; + return { fineOrig, fineFixed, floor, badOrig, badFixed, atFloor, restored }; +}); +check(shield.fineOrig === shield.fineFixed, `where the bracket was positive nothing changes (${shield.fineOrig})`); +check(shield.floor !== null && shield.badFixed < shield.badOrig, + `where it had gone negative, defence no longer adds damage (${shield.badOrig} -> ${shield.badFixed})`); +check(shield.badFixed === shield.atFloor, 'the fixed hit is the bracket held at exactly zero'); +check(shield.restored === 110, 'and the skill level is put back after the hit'); + +console.log('\n--- Luck'); +const luck = await p.evaluate(() => { + const z = area.frstn1a2; global.current_z = z; + const m = mon_gen(z.pop[0].crt); lvlup(m, 3); global.current_m = m; global.target = you.eqp[2]; global.t_n = 2; + you.sat = you.satmax; const crflt0 = you.mods.crflt; + const rows = [0, 10, 100].map(L => { you.luck = L; allbuff(you); let c = 0; const N = 30000; + for (let i = 0; i < N; i++) { global.flags.crti = false; abl.default.f(you, m); if (global.flags.crti) c++; } + return { L, measured: c / N, model: MOD_critRate(you) }; }); + // a lot of luck, a lot of base crit: capped at 50% from luck + const keepCrt = you.crt; you.crt = 0.2; you.luck = 223; const capped = MOD_critRate(you); you.crt = keepCrt; + // the probe's path is the fixed one, so enemies are sized against it + const probeSeesIt = MOD_meanDamage.toString().indexOf('MOD_dmg_calc_original') >= 0; + you.luck = 0; + return { rows, capped, crfltExact: you.mods.crflt === crflt0, probeSeesIt }; +}); +luck.rows.forEach(r => console.log(` luck ${String(r.L).padStart(3)}: crit ${(r.measured * 100).toFixed(2)}% measured, ${(r.model * 100).toFixed(2)}% modelled`)); +check(luck.rows[2].measured > luck.rows[0].measured * 2, 'luck now raises the crit chance on real swings'); +check(luck.rows.every(r => Math.abs(r.measured - r.model) < 0.004), "and the model the enemy probe weights by agrees with the swings"); +check(Math.abs(luck.capped - 0.5) < 1e-9, `luck alone cannot push crit past 50% (${luck.capped})`); +check(luck.crfltExact, 'the saved crflt field is put back exactly, not add-then-subtract'); +check(luck.probeSeesIt, "the enemy model's probe measures through the fixed path"); + +console.log('\n--- shop prices'); +const shop = await p.evaluate(async () => { + const tick = () => new Promise(r => setTimeout(r, 60)); + const big = chs('"Tribulation Pill" 7000000● x1', false); await tick(); + const drawn = big.textContent; + big.innerHTML = '"Tribulation Pill" 38000● x2'; await tick(); + const rewritten = big.textContent; + const small = chs('"Bread" 31● x3', false); await tick(); + setNumberFormat('game'); const raw = chs('"x" 45000●', false); await tick(); setNumberFormat('short'); + return { drawn, rewritten, small: small.textContent, raw: raw.textContent }; +}); +check(/7\.00M●/.test(shop.drawn), `a big price is formatted when the shop draws it (${shop.drawn})`); +check(/38\.0K●/.test(shop.rewritten), 'and when a purchase rewrites the line'); +check(/31●/.test(shop.small), "the base game's own prices, all under 10,000, are untouched"); +check(/45000●/.test(shop.raw), '"As the original" leaves every price raw'); + +console.log('\nerrors:', errs.length ? errs : 'none'); +if (errs.length) fail.push('page errors: ' + JSON.stringify(errs)); +await b.close(); +if (fail.length) { console.error('\nFAIL:\n - ' + fail.join('\n - ')); process.exit(1); } +console.log('\nPASS — death, shield and luck do what their text says, and shop prices read.'); diff --git a/tests/baseline.mjs b/tests/baseline.mjs new file mode 100644 index 0000000..917439f --- /dev/null +++ b/tests/baseline.mjs @@ -0,0 +1,122 @@ +import { launch } from './lib/browser.mjs'; +import fs from 'node:fs'; + +// Balance baselines: the numbers that define the game's balance, written to +// tests/baselines/balance.json and compared on every run, so a change that +// shifts balance shows up as a diff in git instead of passing silently. +// +// Only numbers that are identical run to run go in: the skill curve, the dials, +// the realm and tribulation costs, prices, and each area's per-spawn fight +// targets (_modKillT / _modDieT), which come from the level band and the +// creature's shape rather than from any dice. A target that differs between two +// spawns of the same creature is left out and counted, not recorded -- a +// baseline that flaps is worse than none. +// +// PORT=8080 node tests/baseline.mjs compare +// UPDATE=1 PORT=8080 node tests/baseline.mjs accept the current numbers +// +// The player for the fight targets is allareas.mjs's own, lifted from that file +// so the two cannot describe different characters. + +const HOST = `http://127.0.0.1:${process.env.PORT || 8080}`; +const FILE = new URL('./baselines/balance.json', import.meta.url); +const b = await launch(); +const p = await b.newPage(); +const errs = []; p.on('pageerror', e => errs.push(String(e).slice(0, 200))); +await p.goto(`${HOST}/index.html`, { waitUntil: 'load' }); +await p.waitForTimeout(4500); +// section 8's model is what this measures, so measure it everywhere: the +// default (Fights: Original) leaves the author's areas unscaled -- fights.mjs +const fightsDefault = await p.evaluate(() => getFights()); +await p.evaluate(() => setFights('scaled')); + +const aa = fs.readFileSync(new URL('./allareas.mjs', import.meta.url), 'utf8'); +const helpers = aa.slice(aa.indexOf(' const freshYou = () => {'), aa.indexOf(' // dmg_calc grants skill exp')); +if (!helpers.includes('buildPlayer')) { console.error('could not lift buildPlayer from allareas.mjs'); process.exit(1); } + +const now = await p.evaluate(`(() => { +${helpers} + const sig = x => (typeof x === 'number' && isFinite(x)) ? +x.toPrecision(5) : x; + const out = {}; + const put = (k, v) => { out[k] = sig(v); }; + + [1, 5, 9, 10, 15, 30, 60, 90, 109].forEach(l => put('curve.cost.lv' + l, MOD_expnextFor(l))); + MOD_TIERS.forEach(t => put('curve.total.cap' + t.cap, MOD_xpToReach(t.cap))); + put('curve.ratio', MOD_XP.ratio); put('curve.vanillaTo', MOD_XP.vanillaTo); + put('rate.skill_xp_mult', MOD.skill_xp_mult); put('rate.coin_drop', MOD_MONEY.chance); + + Object.keys(MOD_ENEMY).forEach(k => { if (typeof MOD_ENEMY[k] === 'number') put('enemy.' + k, MOD_ENEMY[k]); }); + put('rank.margin', MOD_RANK_MARGIN); put('rank.step', MOD_RANK_STEP); + put('luck.cap', MOD_LUCK_CAP); put('conv.sectionBonus', MOD_CONV.sectionBonus); + + MOD_REALMS.forEach(r => { if (!r.n) return; + put('realm' + r.n + '.qic', r.qic); put('realm' + r.n + '.mult', r.mult); put('realm' + r.n + '.hp', r.hp); + put('realm' + r.n + '.insight', MOD_insightNeed(r.n)); + put('realm' + r.n + '.seclusionSec', MOD_secludeFull(r.n)); + put('realm' + r.n + '.deviationMin', MOD_devMinutes(r.n)); + if (r.n >= MOD_CULT.tribFrom) put('realm' + r.n + '.tribulation', MOD_tribulationCost(r.n)); }); + Object.keys(MOD_CULT).forEach(k => { if (typeof MOD_CULT[k] === 'number') put('cult.' + k, MOD_CULT[k]); }); + + const keep = global.flags.mod_slipsused; + [0, 10, 50].forEach(u => { global.flags.mod_slipsused = u; put('price.slip.used' + u, MOD_slipPrice()); }); + global.flags.mod_slipsused = keep; + for (const k in item) { const it = item[k]; if (k.indexOf('mod_') === 0 && it && typeof it.v === 'number') put('value.' + k, it.v); } + const stock = (vk) => { const v = vendor[vk]; if (!v || !v.items) return; + v.items.forEach(s => { if (s.item && s.item.id && typeof s.p === 'number') put('shop.' + vk + '.' + s.item.id, s.p); }); }; + stock('pha1'); stock('mod_pltwr'); + put('meat.chance', MOD_MEAT.chance); + // the meat each fight setting drops (the default setting is read before + // this script switches to Scaled, below the page load) + put('meat.killSecOriginal', MOD_MEAT.killSecOriginal); + MOD_MEAT_DROPS.forEach((d, i) => { put('meat.drop' + i + '.author', d._modChance0); put('meat.drop' + i + '.scaled', d.chance); }); + + // per-spawn fight targets: every area's first creature, three story tiers + const quietG = giveSkExp; giveSkExp = function () {}; + let unstable = 0; + const areas = Object.keys(area).filter(k => area[k] && area[k].pop && area[k].pop.length && k !== 'nwh' && k !== 'tst'); + [0, 5, 9].forEach(ti => { + buildPlayer(ti, 'all'); + areas.forEach(ak => { + const z = area[ak], e = z.pop[0]; global.current_z = z; + const a = mon_gen(e.crt); lvlup(a, e.lvlmin); + const c = mon_gen(e.crt); lvlup(c, e.lvlmin); + if (!(a._modKillT > 0)) return; + if (sig(a._modKillT) !== sig(c._modKillT) || sig(a._modDieT) !== sig(c._modDieT)) { unstable++; return; } + put('target.t' + ti + '.' + ak + '.kill', a._modKillT); + put('target.t' + ti + '.' + ak + '.die', a._modDieT); + }); + }); + giveSkExp = quietG; + return { values: out, unstable }; +})()`); + +now.values['fights.default'] = fightsDefault; + +const fail = []; +const check = (c, what) => { if (!c) fail.push(what); console.log(` ${c ? 'ok ' : 'FAIL'} ${what}`); }; +const keys = Object.keys(now.values).sort(); +console.log(`${keys.length} numbers recorded; ${now.unstable} per-spawn targets left out as unstable`); + +if (process.env.UPDATE || !fs.existsSync(FILE)) { + const sorted = {}; keys.forEach(k => { sorted[k] = now.values[k]; }); + fs.writeFileSync(FILE, JSON.stringify(sorted, null, 1) + '\n'); + console.log(`\nwrote ${FILE.pathname} — commit it; the diff is the balance change`); +} else { + const was = JSON.parse(fs.readFileSync(FILE, 'utf8')); + const changed = [], added = [], gone = []; + keys.forEach(k => { if (!(k in was)) added.push(k); else if (was[k] !== now.values[k]) changed.push(`${k}: ${was[k]} -> ${now.values[k]}`); }); + Object.keys(was).forEach(k => { if (!(k in now.values)) gone.push(k); }); + changed.slice(0, 40).forEach(c => console.log(' changed ' + c)); + added.slice(0, 20).forEach(c => console.log(' new ' + c)); + gone.slice(0, 20).forEach(c => console.log(' gone ' + c)); + check(changed.length === 0 && added.length === 0 && gone.length === 0, + `the balance matches the committed baseline (${changed.length} changed, ${added.length} new, ${gone.length} gone)` + + (changed.length + added.length + gone.length ? ' — if intended, run with UPDATE=1 and commit the file' : '')); +} +check(now.unstable < keys.length / 10, `almost every target is stable enough to record (${now.unstable} not)`); + +console.log('\nerrors:', errs.length ? errs : 'none'); +if (errs.length) fail.push('page errors: ' + JSON.stringify(errs)); +await b.close(); +if (fail.length) { console.error('\nFAIL:\n - ' + fail.join('\n - ')); process.exit(1); } +console.log('\nPASS — the balance is what the baseline says it is.'); diff --git a/tests/baselines/balance.json b/tests/baselines/balance.json new file mode 100644 index 0000000..d731e90 --- /dev/null +++ b/tests/baselines/balance.json @@ -0,0 +1,348 @@ +{ + "conv.sectionBonus": 0.03, + "cult.brinkChance": 0.25, + "cult.brinkFrac": 0.2, + "cult.devBody": 0.7, + "cult.devMinutes": 720, + "cult.devStat": 0.55, + "cult.insightBase": 1, + "cult.insightStep": 2, + "cult.layerBody": 0.03, + "cult.layerMult": 0.02, + "cult.layers": 9, + "cult.meditateChance": 0.004, + "cult.secludeChance": 0.012, + "cult.secludeFull": 900, + "cult.secludeOdds": 0.3, + "cult.tribBase": 0.55, + "cult.tribBoltStep": 2, + "cult.tribBolts": 3, + "cult.tribFrom": 8, + "cult.tribStep": 0.15, + "cult.wallShare": 0.05, + "curve.cost.lv1": 55, + "curve.cost.lv10": 26311, + "curve.cost.lv109": 461570, + "curve.cost.lv15": 30407, + "curve.cost.lv30": 46933, + "curve.cost.lv5": 1003, + "curve.cost.lv60": 111810, + "curve.cost.lv9": 25561, + "curve.cost.lv90": 266360, + "curve.ratio": 1.0294, + "curve.total.cap10": 47986, + "curve.total.cap110": 15335000, + "curve.total.cap15": 187500, + "curve.total.cap20": 348730, + "curve.total.cap30": 750390, + "curve.total.cap40": 1286800, + "curve.total.cap50": 2003300, + "curve.total.cap60": 2960200, + "curve.total.cap75": 5029900, + "curve.total.cap90": 8224600, + "curve.vanillaTo": 10, + "enemy.areaScale": 1.4, + "enemy.armor": 1, + "enemy.atkSpread": 0.3, + "enemy.bossDie": 0.7, + "enemy.bossKill": 2, + "enemy.die": 20, + "enemy.expRate": 0.5, + "enemy.hit": 0.45, + "enemy.hpSpread": 0.6, + "enemy.kill": 8, + "enemy.margin": 1.7, + "fights.default": "original", + "luck.cap": 0.5, + "meat.chance": 0.18, + "meat.drop0.author": 0.06, + "meat.drop0.scaled": 0.18, + "meat.drop1.author": 0.06, + "meat.drop1.scaled": 0.18, + "meat.killSecOriginal": 5, + "price.slip.used0": 900, + "price.slip.used10": 1537, + "price.slip.used50": 13088, + "rank.margin": 1.4, + "rank.step": 0.07, + "rate.coin_drop": 0, + "rate.skill_xp_mult": 1, + "realm1.deviationMin": 900, + "realm1.hp": 1.2, + "realm1.insight": 1, + "realm1.mult": 1.1, + "realm1.qic": 1, + "realm1.seclusionSec": 900, + "realm10.deviationMin": 131670, + "realm10.hp": 8.5, + "realm10.insight": 1580, + "realm10.mult": 6, + "realm10.qic": 110, + "realm10.seclusionSec": 131670, + "realm10.tribulation": 0.85, + "realm2.deviationMin": 900, + "realm2.hp": 1.5, + "realm2.insight": 3, + "realm2.mult": 1.3, + "realm2.qic": 8, + "realm2.seclusionSec": 900, + "realm3.deviationMin": 5000, + "realm3.hp": 1.9, + "realm3.insight": 60, + "realm3.mult": 1.55, + "realm3.qic": 18, + "realm3.seclusionSec": 5000, + "realm4.deviationMin": 8667, + "realm4.hp": 2.4, + "realm4.insight": 104, + "realm4.mult": 1.85, + "realm4.qic": 30, + "realm4.seclusionSec": 8667, + "realm5.deviationMin": 16083, + "realm5.hp": 3, + "realm5.insight": 193, + "realm5.mult": 2.2, + "realm5.qic": 45, + "realm5.seclusionSec": 16083, + "realm6.deviationMin": 20833, + "realm6.hp": 3.8, + "realm6.insight": 250, + "realm6.mult": 2.65, + "realm6.qic": 58, + "realm6.seclusionSec": 20833, + "realm7.deviationMin": 27667, + "realm7.hp": 4.7, + "realm7.insight": 332, + "realm7.mult": 3.15, + "realm7.qic": 70, + "realm7.seclusionSec": 27667, + "realm8.deviationMin": 39083, + "realm8.hp": 5.8, + "realm8.insight": 469, + "realm8.mult": 3.8, + "realm8.qic": 82, + "realm8.seclusionSec": 39083, + "realm8.tribulation": 0.55, + "realm9.deviationMin": 39083, + "realm9.hp": 7, + "realm9.insight": 469, + "realm9.mult": 4.8, + "realm9.qic": 90, + "realm9.seclusionSec": 39083, + "realm9.tribulation": 0.7, + "shop.mod_pltwr.4215": 120000, + "shop.mod_pltwr.4216": 340000, + "shop.mod_pltwr.4217": 540000, + "shop.mod_pltwr.4218": 560000, + "shop.mod_pltwr.4219": 6500000, + "shop.mod_pltwr.4222": 90000, + "shop.mod_pltwr.4223": 480000, + "shop.pha1.3002": 40, + "shop.pha1.3003": 400, + "shop.pha1.3005": 70, + "shop.pha1.3006": 20, + "shop.pha1.3007": 230, + "shop.pha1.3008": 690, + "shop.pha1.3010": 20, + "shop.pha1.3013": 6, + "shop.pha1.40043": 100, + "shop.pha1.40044": 150, + "shop.pha1.40047": 480, + "shop.pha1.40048": 600, + "shop.pha1.4211": 900, + "shop.pha1.4212": 3200, + "shop.pha1.4213": 11000, + "shop.pha1.4214": 38000, + "shop.pha1.4220": 2600, + "shop.pha1.4221": 14000, + "shop.pha1.4230": 31000, + "shop.pha1.4231": 5200, + "shop.pha1.9011": 150, + "target.t0.clg.die": 19.983, + "target.t0.clg.kill": 4, + "target.t0.frstn1a2.die": 23.723, + "target.t0.frstn1a2.kill": 6.3176, + "target.t0.frstn1a3.die": 21.682, + "target.t0.frstn1a3.kill": 7.5634, + "target.t0.frstn1a4.die": 18.347, + "target.t0.frstn1a4.kill": 10.793, + "target.t0.frstn2a2.die": 24.657, + "target.t0.frstn2a2.kill": 5.8482, + "target.t0.frstn9a1.die": 16.998, + "target.t0.frstn9a1.kill": 9.9988, + "target.t0.hmbsmnt.die": 19.323, + "target.t0.hmbsmnt.kill": 4.1653, + "target.t0.mod_hollow.die": 16.89, + "target.t0.mod_hollow.kill": 4.5055, + "target.t0.mod_rank1.die": 48.926, + "target.t0.mod_rank1.kill": 34.947, + "target.t0.mod_rank10.die": 33.565, + "target.t0.mod_rank10.kill": 19.744, + "target.t0.mod_rank2.die": 42.942, + "target.t0.mod_rank2.kill": 30.673, + "target.t0.mod_rank3.die": 48.487, + "target.t0.mod_rank3.kill": 34.634, + "target.t0.mod_rank4.die": 42.67, + "target.t0.mod_rank4.kill": 30.478, + "target.t0.mod_rank5.die": 27.055, + "target.t0.mod_rank5.kill": 19.325, + "target.t0.mod_rank6.die": 38.495, + "target.t0.mod_rank6.kill": 27.497, + "target.t0.mod_rank7.die": 39.443, + "target.t0.mod_rank7.kill": 28.174, + "target.t0.mod_rank8.die": 25.742, + "target.t0.mod_rank8.kill": 18.387, + "target.t0.mod_rank9.die": 23.579, + "target.t0.mod_rank9.kill": 15.352, + "target.t0.mod_spire.die": 27.391, + "target.t0.mod_spire.kill": 4.5494, + "target.t0.mod_vigil.die": 27.41, + "target.t0.mod_vigil.kill": 4.5432, + "target.t0.trn.die": 35.104, + "target.t0.trn.kill": 4, + "target.t0.trn1.die": 21.66, + "target.t0.trn1.kill": 7.2754, + "target.t0.trn2.die": 26.667, + "target.t0.trn2.kill": 4.8, + "target.t0.trn3.die": 23.549, + "target.t0.trn3.kill": 6.1551, + "target.t0.trne1.die": 18.471, + "target.t0.trne1.kill": 9.8043, + "target.t0.trne2.die": 18.5, + "target.t0.trne2.kill": 9.7735, + "target.t0.trne3.die": 18.51, + "target.t0.trne3.kill": 9.7636, + "target.t0.trne4.die": 18.529, + "target.t0.trne4.kill": 9.7433, + "target.t0.trnf.die": 35.104, + "target.t0.trnf.kill": 4, + "target.t5.clg.die": 19.983, + "target.t5.clg.kill": 4, + "target.t5.frstn1a2.die": 23.723, + "target.t5.frstn1a2.kill": 6.3176, + "target.t5.frstn1a3.die": 21.682, + "target.t5.frstn1a3.kill": 7.5634, + "target.t5.frstn1a4.die": 18.347, + "target.t5.frstn1a4.kill": 10.793, + "target.t5.frstn2a2.die": 24.657, + "target.t5.frstn2a2.kill": 5.8482, + "target.t5.frstn9a1.die": 16.998, + "target.t5.frstn9a1.kill": 9.9988, + "target.t5.hmbsmnt.die": 19.323, + "target.t5.hmbsmnt.kill": 4.1653, + "target.t5.mod_hollow.die": 16.89, + "target.t5.mod_hollow.kill": 4.5055, + "target.t5.mod_rank1.die": 48.926, + "target.t5.mod_rank1.kill": 34.947, + "target.t5.mod_rank10.die": 33.565, + "target.t5.mod_rank10.kill": 19.744, + "target.t5.mod_rank2.die": 42.942, + "target.t5.mod_rank2.kill": 30.673, + "target.t5.mod_rank3.die": 48.487, + "target.t5.mod_rank3.kill": 34.634, + "target.t5.mod_rank4.die": 42.67, + "target.t5.mod_rank4.kill": 30.478, + "target.t5.mod_rank5.die": 27.055, + "target.t5.mod_rank5.kill": 19.325, + "target.t5.mod_rank6.die": 38.495, + "target.t5.mod_rank6.kill": 27.497, + "target.t5.mod_rank7.die": 39.443, + "target.t5.mod_rank7.kill": 28.174, + "target.t5.mod_rank8.die": 25.742, + "target.t5.mod_rank8.kill": 18.387, + "target.t5.mod_rank9.die": 23.579, + "target.t5.mod_rank9.kill": 15.352, + "target.t5.mod_spire.die": 27.391, + "target.t5.mod_spire.kill": 4.5494, + "target.t5.mod_vigil.die": 27.41, + "target.t5.mod_vigil.kill": 4.5432, + "target.t5.trn.die": 35.104, + "target.t5.trn.kill": 4, + "target.t5.trn1.die": 21.66, + "target.t5.trn1.kill": 7.2754, + "target.t5.trn2.die": 26.667, + "target.t5.trn2.kill": 4.8, + "target.t5.trn3.die": 23.549, + "target.t5.trn3.kill": 6.1551, + "target.t5.trne1.die": 18.471, + "target.t5.trne1.kill": 9.8043, + "target.t5.trne2.die": 18.5, + "target.t5.trne2.kill": 9.7735, + "target.t5.trne3.die": 18.51, + "target.t5.trne3.kill": 9.7636, + "target.t5.trne4.die": 18.529, + "target.t5.trne4.kill": 9.7433, + "target.t5.trnf.die": 35.104, + "target.t5.trnf.kill": 4, + "target.t9.clg.die": 19.983, + "target.t9.clg.kill": 4, + "target.t9.frstn1a2.die": 23.723, + "target.t9.frstn1a2.kill": 6.3176, + "target.t9.frstn1a3.die": 21.682, + "target.t9.frstn1a3.kill": 7.5634, + "target.t9.frstn1a4.die": 18.347, + "target.t9.frstn1a4.kill": 10.793, + "target.t9.frstn2a2.die": 24.657, + "target.t9.frstn2a2.kill": 5.8482, + "target.t9.frstn9a1.die": 16.998, + "target.t9.frstn9a1.kill": 9.9988, + "target.t9.hmbsmnt.die": 19.323, + "target.t9.hmbsmnt.kill": 4.1653, + "target.t9.mod_hollow.die": 16.89, + "target.t9.mod_hollow.kill": 4.5055, + "target.t9.mod_rank1.die": 48.926, + "target.t9.mod_rank1.kill": 34.947, + "target.t9.mod_rank10.die": 33.565, + "target.t9.mod_rank10.kill": 19.744, + "target.t9.mod_rank2.die": 42.942, + "target.t9.mod_rank2.kill": 30.673, + "target.t9.mod_rank3.die": 48.487, + "target.t9.mod_rank3.kill": 34.634, + "target.t9.mod_rank4.die": 42.67, + "target.t9.mod_rank4.kill": 30.478, + "target.t9.mod_rank5.die": 27.055, + "target.t9.mod_rank5.kill": 19.325, + "target.t9.mod_rank6.die": 38.495, + "target.t9.mod_rank6.kill": 27.497, + "target.t9.mod_rank7.die": 39.443, + "target.t9.mod_rank7.kill": 28.174, + "target.t9.mod_rank8.die": 25.742, + "target.t9.mod_rank8.kill": 18.387, + "target.t9.mod_rank9.die": 23.579, + "target.t9.mod_rank9.kill": 15.352, + "target.t9.mod_spire.die": 27.391, + "target.t9.mod_spire.kill": 4.5494, + "target.t9.mod_vigil.die": 27.41, + "target.t9.mod_vigil.kill": 4.5432, + "target.t9.trn.die": 35.104, + "target.t9.trn.kill": 4, + "target.t9.trn1.die": 21.66, + "target.t9.trn1.kill": 7.2754, + "target.t9.trn2.die": 26.667, + "target.t9.trn2.kill": 4.8, + "target.t9.trn3.die": 23.549, + "target.t9.trn3.kill": 6.1551, + "target.t9.trne1.die": 18.471, + "target.t9.trne1.kill": 9.8043, + "target.t9.trne2.die": 18.5, + "target.t9.trne2.kill": 9.7735, + "target.t9.trne3.die": 18.51, + "target.t9.trne3.kill": 9.7636, + "target.t9.trne4.die": 18.529, + "target.t9.trne4.kill": 9.7433, + "target.t9.trnf.die": 35.104, + "target.t9.trnf.kill": 4, + "value.mod_bp1": 390, + "value.mod_bp10": 2117500, + "value.mod_bp2": 1014, + "value.mod_bp3": 2636, + "value.mod_bp4": 6855, + "value.mod_bp5": 17822, + "value.mod_bp6": 46337, + "value.mod_bp7": 120480, + "value.mod_bp8": 313240, + "value.mod_bp9": 814430, + "value.mod_rootpill": 26000, + "value.mod_settlepill": 4200, + "value.mod_slip": 225 +} diff --git a/tests/capreach.mjs b/tests/capreach.mjs index c6b0f12..7ee9920 100644 --- a/tests/capreach.mjs +++ b/tests/capreach.mjs @@ -1,4 +1,4 @@ -import { chromium } from 'playwright'; +import { launch } from './lib/browser.mjs'; // Can a skill actually REACH the story cap? Measures the game's own exp curve // against the xp a skill really receives per tick through the mod's grant path @@ -6,7 +6,7 @@ import { chromium } from 'playwright'; // PORT=8080 node tests/capreach.mjs const HOST = `http://127.0.0.1:${process.env.PORT || 8080}`; -const b = await chromium.launch(process.env.CHROMIUM ? { executablePath: process.env.CHROMIUM } : {}); +const b = await launch(); const p = await b.newPage(); const errs = []; p.on('pageerror', e => errs.push(String(e).slice(0, 200))); await p.goto(`${HOST}/index.html`, { waitUntil: 'load' }); diff --git a/tests/combat.mjs b/tests/combat.mjs index 39531b9..077701f 100644 --- a/tests/combat.mjs +++ b/tests/combat.mjs @@ -1,4 +1,4 @@ -import { chromium } from 'playwright'; +import { launch } from './lib/browser.mjs'; // The whole story ladder, measured through the game's OWN combat math. // @@ -13,11 +13,14 @@ import { chromium } from 'playwright'; // PORT=8080 node tests/combat.mjs const HOST = `http://127.0.0.1:${process.env.PORT || 8080}`; -const b = await chromium.launch(process.env.CHROMIUM ? { executablePath: process.env.CHROMIUM } : {}); +const b = await launch(); const p = await b.newPage(); const errs = []; p.on('pageerror', e => errs.push(String(e).slice(0, 200))); await p.goto(`${HOST}/index.html`, { waitUntil: 'load' }); await p.waitForTimeout(4500); +// section 8's model is what this measures, so measure it everywhere: the +// default (Fights: Original) leaves the author's areas unscaled -- fights.mjs +await p.evaluate(() => setFights('scaled')); const R = await p.evaluate(() => { const SAMPLES = 200; diff --git a/tests/convergence.mjs b/tests/convergence.mjs index a077224..b8a6a49 100644 --- a/tests/convergence.mjs +++ b/tests/convergence.mjs @@ -1,4 +1,4 @@ -import { chromium } from 'playwright'; +import { launch } from './lib/browser.mjs'; // Skill rarity, and the convergence. // @@ -25,7 +25,7 @@ import { chromium } from 'playwright'; // PORT=8080 node tests/convergence.mjs const HOST = `http://127.0.0.1:${process.env.PORT || 8080}`; -const b = await chromium.launch(process.env.CHROMIUM ? { executablePath: process.env.CHROMIUM } : {}); +const b = await launch(); const p = await b.newPage(); const errs = []; p.on('pageerror', e => errs.push(String(e).slice(0, 200))); await p.goto(`${HOST}/index.html`, { waitUntil: 'load' }); diff --git a/tests/crafting.mjs b/tests/crafting.mjs index 48a0e59..6e09055 100644 --- a/tests/crafting.mjs +++ b/tests/crafting.mjs @@ -1,4 +1,4 @@ -import { chromium } from 'playwright'; +import { launch } from './lib/browser.mjs'; // The crafting ladder (section 35). // @@ -19,7 +19,7 @@ import { chromium } from 'playwright'; // PORT=8080 node tests/crafting.mjs const HOST = `http://127.0.0.1:${process.env.PORT || 8080}`; -const b = await chromium.launch(process.env.CHROMIUM ? { executablePath: process.env.CHROMIUM } : {}); +const b = await launch(); const p = await b.newPage(); const errs = []; p.on('pageerror', e => errs.push(String(e).slice(0, 200))); await p.goto(`${HOST}/index.html`, { waitUntil: 'load' }); diff --git a/tests/cultivation.mjs b/tests/cultivation.mjs index d6c733a..66673c8 100644 --- a/tests/cultivation.mjs +++ b/tests/cultivation.mjs @@ -1,4 +1,4 @@ -import { chromium } from 'playwright'; +import { launch } from './lib/browser.mjs'; // Cultivation realms and elemental mastery. // @@ -16,7 +16,7 @@ import { chromium } from 'playwright'; // PORT=8080 node tests/cultivation.mjs const HOST = `http://127.0.0.1:${process.env.PORT || 8080}`; -const b = await chromium.launch(process.env.CHROMIUM ? { executablePath: process.env.CHROMIUM } : {}); +const b = await launch(); const p = await b.newPage(); const errs = []; p.on('pageerror', e => errs.push(String(e).slice(0, 200))); await p.goto(`${HOST}/index.html`, { waitUntil: 'load' }); diff --git a/tests/dojo.mjs b/tests/dojo.mjs index f4e49e6..e8573a1 100644 --- a/tests/dojo.mjs +++ b/tests/dojo.mjs @@ -1,4 +1,4 @@ -import { chromium } from 'playwright'; +import { launch } from './lib/browser.mjs'; // The dojo's Level Advancement ladder, carried to character level 110. // @@ -12,7 +12,7 @@ import { chromium } from 'playwright'; // PORT=8080 node tests/dojo.mjs const HOST = `http://127.0.0.1:${process.env.PORT || 8080}`; -const b = await chromium.launch(process.env.CHROMIUM ? { executablePath: process.env.CHROMIUM } : {}); +const b = await launch(); const p = await b.newPage(); const errs = []; p.on('pageerror', e => errs.push(String(e).slice(0, 200))); await p.goto(`${HOST}/index.html`, { waitUntil: 'load' }); diff --git a/tests/earlybal.mjs b/tests/earlybal.mjs index 766f23f..4178d36 100644 --- a/tests/earlybal.mjs +++ b/tests/earlybal.mjs @@ -1,4 +1,4 @@ -import { chromium } from 'playwright'; +import { launch } from './lib/browser.mjs'; // EARLY GAME balance. Every other balance script models the player sitting AT a // story cap; this one walks the first few hours instead, when the character is @@ -13,11 +13,14 @@ import { chromium } from 'playwright'; // PORT=8080 node tests/earlybal.mjs const HOST = `http://127.0.0.1:${process.env.PORT || 8080}`; -const b = await chromium.launch(process.env.CHROMIUM ? { executablePath: process.env.CHROMIUM } : {}); +const b = await launch(); const p = await b.newPage(); const errs = []; p.on('pageerror', e => errs.push(String(e).slice(0, 200))); await p.goto(`${HOST}/index.html`, { waitUntil: 'load' }); await p.waitForTimeout(4500); +// section 8's model is what this measures, so measure it everywhere: the +// default (Fights: Original) leaves the author's areas unscaled -- fights.mjs +await p.evaluate(() => setFights('scaled')); const R = await p.evaluate((BASELINE) => { const SAMPLES = 300; diff --git a/tests/econ.mjs b/tests/econ.mjs index fa5fd1c..79fcb89 100644 --- a/tests/econ.mjs +++ b/tests/econ.mjs @@ -1,48 +1,180 @@ -import { chromium } from 'playwright'; +import { launch } from './lib/browser.mjs'; + +// The economy with the original's coin drop (none), measured against what the +// ladder asks you to pay. +// +// Income per hour is measured at every story tier from the sources actually +// open there -- fighting in each area (the real swings a kill takes, through +// dmg_calc, for a player at that tier's cap, with the area's real drop table +// sold at the shops' rate) and gathering at each resource node -- and the best +// one is taken. Then every price the progression REQUIRES is held to one rule: +// +// no required price costs more than 10% of the climb it gates, +// counted in hours of the best income open when you get there +// +// (or one hour of it, where the climb is shorter than ten). A realm pill gates +// the Circulate Qi climb from the realm below. The Jade Slips gate nothing, so +// all hundred of them together are held to 10% of everything the game pays +// from the moment the Archive opens. +// +// The rule is held on the Original preset, the default: his fights, no coin +// drop. "Mod before 4.2" (scaled fights, a 15% coin drop, and the old curve, +// whose early climbs are an hour long) is printed for comparison and not held +// to it -- it is kept exactly as it was, prices included. +// +// PORT=8080 node tests/econ.mjs -// point at any local server: PORT=9000 node tests/econ.mjs const HOST = `http://127.0.0.1:${process.env.PORT || 8080}`; -// Playwright finds its own Chromium locally; set CHROMIUM=/path/to/chrome to override -const b = await chromium.launch(process.env.CHROMIUM ? { executablePath: process.env.CHROMIUM } : {}); -const p = await b.newPage(); -await p.goto(`${HOST}/index.html`, {waitUntil:'load'}); -await p.waitForTimeout(4500); -const r = await p.evaluate(()=>{ - const DFL = 0.25; - // expected sell income per kill, from the real drop tables - function perKill(a){ - let exp = 0; - for (const pop of a.pop){ - const c = pop.crt; const w = pop.c === undefined ? 1/a.pop.length : pop.c; - let e = 0; - for (const d of (c.drop||[])) if (d.item) e += (d.chance||0) * MOD_itemValue(d.item) * DFL; - exp += w * e; - } - for (const d of (a.drop||[])) if (d.item) exp += (d.c||0) * MOD_itemValue(d.item) * DFL; - return exp; - } - const areas = ['frstn1a2','frstn1a3','frstn9a1','hmbsmnt','mod_hollow','mod_spire','mod_vigil'] - .filter(k=>area[k]).map(k=>({name:area[k].name||k, key:k, perKill:+perKill(area[k]).toFixed(2)})); +const b = await launch(); +const errs = []; +const fail = []; +const check = (c, what) => { if (!c) fail.push(what); console.log(` ${c ? 'ok ' : 'FAIL'} ${what}`); }; - // what things cost to buy - const prices=[]; - for (const v in vendor) for (const e of (vendor[v].items||[])) if (typeof e.p==='number') prices.push(e.p); - prices.sort((x,y)=>x-y); - const med = prices[prices.length>>1]; +async function run(preset) { + const p = await b.newPage(); + p.on('pageerror', e => errs.push(`[${preset}] ` + String(e).slice(0, 200))); + await p.goto(`${HOST}/index.html`, { waitUntil: 'load' }); + await p.waitForTimeout(4500); + const R = await p.evaluate((preset) => { + setPacing(preset); + const SAMPLES = 60; + const freshYou = () => { + you.lvl = 1; you.str_r = 1; you.agl_r = 1; you.int_r = 1; you.spd_r = 1; + you.hp_r = 39; you.hpmax = 39; you.hp = 39; you.sat_r = 200; you.satmax = 200; you.sat = 200; + you.stra = you.agla = you.inta = you.spda = you.hpa = you.sata = 0; + you.strm = you.aglm = you.intm = you.spdm = you.hpm = you.satm = 1; + you.stat_p = [1, 1, 1, 1]; you.mods.sbonus = 0; you.mods.cpwr = 1; + for (const k in skl) { const s = skl[k]; if (!s || typeof s !== 'object') continue; + s.lvl = 0; s.exp = 0; if (s.mlstn) s.mlstn.forEach(m => { m.g = false; }); } + global.flags.mod_hpm = 1; global.flags.mod_satm = 1; global.flags.mod_realm = 0; + you.stat_r(); + }; + const setTier = (ti) => { + global.flags.tr3_win = ti >= 1; + global.flags.mod_t_forest = ti >= 2; global.flags.mod_t_deep = ti >= 3; + global.flags.mod_t_cata = ti >= 4; global.flags.trne1e1 = ti >= 5; global.flags.trne3e1 = ti >= 6; + global.flags.trne4e1 = ti >= 6; + global.flags.mod_prog = { hollow: ti >= 7 ? 99 : 0, spire: ti >= 8 ? 99 : 0, vigil: ti >= 9 ? 99 : 0 }; + MOD_CAP.current = -1; return MOD_levelCap(); + }; + const CHARLVL = [1, 8, 14, 20, 28, 36, 46, 60, 76, 92]; + const buildPlayer = (ti) => { + freshYou(); const cap = setTier(ti); + for (let i = 1; i < CHARLVL[ti]; i++) { try { lvlup(you, 1); } catch (e) {} } + for (const k in skl) { const s = skl[k]; + if (!s || typeof s !== 'object' || k === 'rnwn') continue; + s.lvl = cap; + if (s.mlstn) s.mlstn.forEach(m => { m.g = false; + if (m.lv <= cap) { try { m.f(); m.g = true; } catch (e) {} } }); } + you.stat_r(); + global.flags.inside = true; global.flags.isday = true; global.flags.btl = false; + global.flags.iscold = global.flags.iswet = global.flags.isdark = false; + you.hp = you.hpmax; you.sat = you.satmax; allbuff(you); + return cap; + }; + const quiet = (fn) => { const g = giveSkExp, c = global.flags.crti; giveSkExp = function () {}; + try { return fn(); } finally { giveSkExp = g; global.flags.crti = c; } }; + const mean = (a, d) => { let s = 0; for (let i = 0; i < SAMPLES; i++) { global.flags.crti = false; + s += Math.max(0, Math.round(abl.default.f(a, d))); } return s / SAMPLES; }; + + // The author's coin drop, averaged over its own rand() -- zero on Original. + const coinPerKill = (m) => { + const ch = you.mods.enmondren; if (!(ch > 0)) return 0; + const lo = m.lvl << 0, hi = (m.lvl / 4) << 0, ex = (1 + m.rnk / 5) << 0; + let s = 0; for (let v = Math.min(lo, hi); v <= Math.max(lo, hi); v++) s += 1 + v ** ex * you.mods.enmondrts; + return ch * s / (Math.abs(lo - hi) + 1); + }; + // One exchange a tick (index.html's ontick drives fght once a second), a + // kill never shorter than a tick, and the next spawn on the next one. + // Time spent getting your health back is charged as the share of it the + // fight took: a kill at kill/die of 0.4 costs 40% more time. + const areaIncome = (ak) => { + const z = area[ak]; let perKill = 0, ticks = 0, wsum = 0; + z.pop.forEach((e, i) => { + const w = z.popc && z.popc[i] ? z.popc[i][1] - z.popc[i][0] : 1 / z.pop.length; + const lv = Math.round((e.lvlmin + e.lvlmax) / 2); + global.current_z = z; const m = mon_gen(e.crt); lvlup(m, lv); + global.current_m = m; global.target = you.eqp[2]; global.t_n = 2; + allbuff(you); allbuff(m); + const hy = Math.max(0, Math.min(100, hit_calc(1))) / 100, hm = Math.max(0, Math.min(100, hit_calc(2))) / 100; + let o, inc; quiet(() => { o = mean(you, m); inc = mean(m, you); }); + const kill = o * hy > 0 ? m.hpmax / (o * hy) : Infinity; + const die = inc * hm > 0 ? you.hpmax / (inc * hm) : Infinity; + const t = Math.max(1, Math.ceil(kill)) * (1 + Math.min(kill / die, 1)) + 1; + let sell = 0; (e.crt.drop || []).forEach(d => { if (d.item) sell += (d.chance || 0) * MOD_sellPrice(d.item); }); + perKill += w * (sell + coinPerKill(m)); ticks += w * t; wsum += w; + }); + (z.drop || []).forEach(d => { if (d.item) perKill += wsum * (d.c || 0) * MOD_sellPrice(d.item); }); + return { perKill: perKill / wsum, sec: ticks / wsum, perHour: perKill / wsum * 3600 / (ticks / wsum) }; + }; + // where each source first opens, by tier index (MOD_TIERS) + const AREAS = { trn: 1, frstn1a2: 1, frstn2a2: 1, frstn1a3: 2, hmbsmnt: 2, frstn9a1: 3, + mod_hollow: 6, mod_spire: 7, mod_vigil: 8 }; + const NODES = { 2: 3, 3: 4, 4: 6, 5: 9 }; // node rung -> tier its gate opens at + const tiers = []; + for (let ti = 0; ti < 10; ti++) { + const cap = buildPlayer(ti); + const src = []; + Object.keys(AREAS).forEach(ak => { if (AREAS[ak] <= ti && area[ak]) { + const r = areaIncome(ak); src.push({ what: area[ak].name + ' (' + ak + ')', perHour: r.perHour, sec: r.sec, perKill: r.perKill }); } }); + Object.keys(NODES).forEach(t => { if (NODES[t] <= ti) { + const ch = MOD_gatherChance(); + const per = [MOD_MAT['ore' + t], MOD_MAT['weave' + t], MOD_MAT['ess' + t]].map(it => MOD_sellPrice(it)); + const avg = per.reduce((a, x) => a + x, 0) / per.length; + src.push({ what: MOD_craftTier(+t).node + ' (gathering)', perHour: ch * avg * 3600 }); } }); + src.sort((x, y) => y.perHour - x.perHour); + tiers.push({ ti, cap, best: src[0] || { what: 'nothing', perHour: 0 }, src }); + } + // realm climbs on the live curve, at Circulate Qi's own 0.9 a second + const realms = MOD_REALMS.filter(r => r.n >= 2).map(r => { + const prev = MOD_REALMS[r.n - 1]; + const hours = (MOD_xpToReach(r.qic) - MOD_xpToReach(prev.qic)) / 0.9 / 3600; + // Neither shop is inside the dojo: the Herbalist is in the marketplace and + // the Pill Tower on the Village Center, so a pill is bought at tier 1 at + // the earliest, whatever tier its Qi Circulation level opens in. + const ti = Math.max(MOD_TIERS.findIndex(t => t.cap >= r.qic), 1); + const shops = []; + [vendor.pha1, vendor.mod_pltwr].forEach(v => (v.items || []).forEach(e => { + if (e.item === item['mod_bp' + r.n]) shops.push({ shop: v === vendor.pha1 ? 'Herbalist' : 'Pill Tower', p: e.p }); })); + return { n: r.n, name: r.name, hours, ti, shops }; + }); + // the Archive opens with dj1rw6, the character level 30 rung: tier 4 here + let slips = 0; for (let i = 0; i < 100; i++) slips += Math.min(Math.round(MOD_CONV.slipBase * Math.pow(MOD_CONV.slipStep, i)), MOD_CONV.slipCap); + setTier(0); + return { tiers, realms, slips, slipsFrom: 4, fights: getFights(), coin: MOD_MONEY.chance }; + }, preset); + await p.close(); + return R; +} - // highest-value single items you can actually loot in the endgame areas - const loot=[]; - for (const k of ['mod_vigil','mod_spire']) if(area[k]) for(const pop of area[k].pop) - for(const d of (pop.crt.drop||[])) if(d.item) loot.push([d.item.name, MOD_itemValue(d.item), d.chance]); - loot.sort((a,b)=>b[1]-a[1]); +const fmt = n => n >= 1e6 ? (n / 1e6).toFixed(2) + 'M' : n >= 1e4 ? (n / 1e3).toFixed(1) + 'K' : Math.round(n).toLocaleString(); +for (const preset of ['original', 'fast']) { + const R = await run(preset); + console.log(`\n=== pacing '${preset}': fights ${R.fights}, coin drop ${Math.round(R.coin * 100)}%`); + console.log(' tier cap best income an hour, and where'); + R.tiers.forEach(t => console.log(` ${String(t.ti).padStart(4)} ${String(t.cap).padStart(3)} ${fmt(t.best.perHour).padStart(8)} ${t.best.what}` + + (t.best.sec ? ` (${t.best.sec.toFixed(1)}s a kill, ${t.best.perKill.toFixed(1)} a kill)` : ''))); + console.log(' realm pill climb budget price hours of income'); + R.realms.forEach(r => { + const inc = R.tiers[r.ti].best.perHour; + const budget = Math.max(r.hours * 0.10, 1) * inc; + r.shops.forEach(s => { + const hrs = s.p / inc; + console.log(` ${String(r.n).padStart(2)} ${r.name.padEnd(26)} ${(r.hours.toFixed(0) + 'h').padStart(6)} ${fmt(budget).padStart(8)} ${fmt(s.p).padStart(8)} ${hrs.toFixed(1)}h (${s.shop})`); + if (preset === 'original') check(s.p <= budget, `[${preset}] realm ${r.n} pill at the ${s.shop}: ${fmt(s.p)} is ${hrs.toFixed(1)}h of income, ` + + `inside 10% of a ${r.hours.toFixed(0)}h climb`); + }); + if (!r.shops.length) console.log(` ${String(r.n).padStart(2)} ${r.name.padEnd(26)} (not sold)`); + }); + // everything the game pays from the Archive opening to the top: each realm's + // climb at the income of the tier it opens in + let pays = 0; R.realms.forEach(r => { if (r.ti >= R.slipsFrom) pays += r.hours * R.tiers[r.ti].best.perHour; }); + console.log(` all 100 Jade Slips: ${fmt(R.slips)}, against ${fmt(pays)} earned from the Archive to the cap`); + if (preset === 'original') check(R.slips <= pays * 0.10, `[${preset}] folding every skill is ${(R.slips / pays * 100).toFixed(1)}% of that, inside 10%`); +} - return {areas, buyPrices:{count:prices.length, median:med, max:prices[prices.length-1]}, - topEndgameLoot: loot.slice(0,4), - enemyMoneyDefault: you.mods.enmondren}; -}); -console.log('expected SELL income per kill (using each area\'s real drop table):'); -r.areas.forEach(a=>console.log(' '+a.name.padEnd(22)+String(a.perKill).padStart(8))); -console.log('\nvendor buy prices: n='+r.buyPrices.count+' median='+r.buyPrices.median+' max='+r.buyPrices.max); -console.log('enemy money drop chance by default: '+r.enemyMoneyDefault); -console.log('\nhighest-value endgame loot:', JSON.stringify(r.topEndgameLoot)); +console.log('\nerrors:', errs.length ? errs : 'none'); +if (errs.length) fail.push('page errors'); await b.close(); +if (fail.length) { console.error('\nFAIL:\n - ' + fail.join('\n - ')); process.exit(1); } +console.log('\nPASS — on Original pacing every price the ladder requires is inside 10% of the climb it gates.'); diff --git a/tests/fights.mjs b/tests/fights.mjs new file mode 100644 index 0000000..2893635 --- /dev/null +++ b/tests/fights.mjs @@ -0,0 +1,238 @@ +import { launch } from './lib/browser.mjs'; + +// Fights: Original (section 47), against the author's own page. +// +// Two pages from one server; in the first the inside content closing the block early, so that is the\n one thing scrubbed. */\n safe: function (h) {\n return String(h === undefined || h === null ? '' : h).replace(/<\\/script/gi, '<\\\\/script');\n }\n};\n\n/* Descriptions carry real HTML (coloured spans, the game's separator div), so\n they are passed through rather than escaped — but the separator becomes a\n rule, since a wiki row is not a tooltip. */\nfunction MOD_wikiDesc(d) {\n var s;\n try { s = (typeof d === 'function') ? d() : d; } catch (e) { s = ''; }\n if (!s) return '';\n s = String(s).split('
 
').join('
');\n return MOD_WIKI.safe(s);\n}\n\nfunction MOD_wikiNum(n) {\n if (!isFinite(n)) return '—';\n return Math.round(n).toLocaleString();\n}\n\n/* An effect line at a level the player has not reached yet. The effect\n functions read the skill they are handed, so the level is borrowed and put\n back; anything that throws simply gets no projection. */\nfunction MOD_wikiEffectAt(key, sk, lvl) {\n var eff = MOD_EFFECTS[key];\n if (!eff) return '';\n var was = sk.lvl, out = '';\n try { sk.lvl = lvl; out = eff(sk) || ''; }\n catch (e) { out = ''; }\n finally { sk.lvl = was; }\n return MOD_WIKI.safe(out);\n}\n\n/* --- page builders --------------------------------------------------------\n Each returns the inner HTML of one page. `wk-e` marks a searchable entry and\n `wk-g` a group that hides itself when all its entries are filtered out.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_PAGES = [];\n\nfunction MOD_wikiPage(id, label, build) {\n MOD_WIKI_PAGES.push({ id: id, label: label, build: build });\n}\n\n/* A collapsible group.\n\n Not decoration: with every skill, item and title listed, three of the eleven\n pages ran past 40,000 pixels — a hundred screens of scrolling, which is the\n same as not having the content at all. Collapsed, each of those pages opens\n as an index you can read in one screen and expand where you want.\n\n
rather than a click handler, so it still works with JavaScript off,\n the browser's own find can be made to reach inside it, and the summary is\n keyboard-reachable for free. `level` is the heading level for the summary,\n since a group is sometimes a section of a page and sometimes a subsection. */\nfunction MOD_wikiGroup(heading, count, inner, level) {\n var hn = 'h' + (level || 2);\n return '
<' + hn + '>' + heading +\n (count !== null && count !== undefined\n ? ' (' + count + ')' : '') +\n '
' + inner + '
';\n}\n\n/* --- 1. orientation ------------------------------------------------------- */\nMOD_wikiPage('start', 'Start here', function () {\n var cap = MOD_levelCap();\n var realm = MOD_realm();\n var h = '

proto23

' +\n '

An idle RPG by truezhangwei, plus a mod. You train skills by ' +\n 'doing things, the things you can do open up as the story does, and none of ' +\n 'it is in a hurry.

' +\n\n '

Where you are right now

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Character level' + MOD_wikiNum(you.lvl) + '
Skill level cap' + cap + ' — ' +\n MOD_WIKI.safe(MOD_CAP.name) + '
Cultivation realm' + MOD_WIKI.safe(realm.name) +\n ' (' + realm.n + ' of ' + (MOD_REALMS.length - 1) + ')
Titles held' + MOD_titleCount() + ' — renown ' +\n (skl.rnwn ? skl.rnwn.lvl : 0) + '
Coin' + MOD_wikiNum(you.wealth || 0) + '
' +\n\n '

The four things worth knowing early

' +\n '
' +\n '
Your skill levels are capped by the story, not by grinding.
' +\n '
The cap starts at 10 and ends at 110, and it moves when you reach a new ' +\n 'place — not when you earn enough experience. If a skill has stopped ' +\n 'levelling, you are not doing it wrong; you need to go somewhere. See ' +\n 'Progression.
' +\n\n '
Armour is subtraction, so levels matter more than they look.
' +\n '
A hit does the attacker\\'s STR minus the defender\\'s STR. That means a ' +\n 'small gap in strength is a large gap in damage, and a big enough one takes ' +\n 'your damage to nothing at all rather than merely reducing it. See ' +\n 'Mechanics.
' +\n\n '
Most skills have a parent that pulls them along.
' +\n '
Every section has a parent skill that earns a share of everything its ' +\n 'children do, and any child lagging behind the parent trains faster — up to ' +\n 'twice as fast at ' + MOD_PARENT.maxGap + ' levels behind. A neglected skill is ' +\n 'cheap to bring up later, which is the only reason having this many is ' +\n 'workable.
' +\n\n '
Cultivation is a second ladder, and it is not automatic.
' +\n '
Circulating qi levels a skill; the skill does not raise your realm. ' +\n 'Reaching the level opens a bottleneck, and breaking through it costs a pill ' +\n 'and can fail. See Cultivation.
' +\n '
' +\n\n '

What is the mod and what is the game

' +\n '

The game is index.html and it is entirely truezhangwei\\'s ' +\n 'work. Everything in mod.js is a modification of it and is not ' +\n 'endorsed by the author. Pages here mark added content with ' +\n 'mod where the distinction matters. The full ' +\n 'list of changes is in the changelog, on the bottom bar.

';\n return h;\n});\n\n/* --- 2. progression ------------------------------------------------------- */\nMOD_wikiPage('progress', 'Progression', function () {\n var cap = MOD_levelCap();\n var h = '

Progression

' +\n '

Skill levels are capped by how far the story has gone. Ten ' +\n 'rungs, 10 through 110. The cap applies to every skill at once; Renown is ' +\n 'the single exception, since it is counted rather than trained.

' +\n '' +\n '';\n\n MOD_TIERS.forEach(function (t) {\n var got = null;\n try { got = MOD_tierFlag(t); } catch (e) {}\n var here = (t.cap === cap);\n var need = t.needText ? t.needText\n : (t.flags.length ? t.flags.join(' or ') : 'the start of the game');\n h += '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
CapStageWhat opens it
' + t.cap + '' + MOD_WIKI.safe(t.name) + '' + MOD_WIKI.safe(need) + '' + (here ? 'you are here'\n : got ? 'reached' : '') + '
' +\n\n '

The experience curve

' +\n '

The game\\'s own skill curve is super-exponential — level 109 to 110 ' +\n 'alone costs ' + MOD_wikiNum(MOD_vanillaExpnext(109)) + ' experience, so the top of the ladder ' +\n 'is unreachable by a factor of about ten trillion. But the author only ever ' +\n 'designed skills up to level 15, and below that his curve is fine.

' +\n (MOD_XP.vanillaTo > 0\n ? '

So the mod keeps it. Below level ' + MOD_XP.vanillaTo + ' a skill costs ' +\n 'exactly what the original charges; from there it rises by ×' +\n MOD_XP.ratio.toFixed(4) + ' a level, a ratio solved so the climb to 110 takes ' +\n 'the same time it always did.'\n : '

You have chosen the Mod before 4.2 pacing: a plain geometric curve, ' +\n '×' + MOD_XP.ratio.toFixed(3) + ' a level from level 0, much faster than the ' +\n 'original early on. The Original pacing in settings puts back the ' +\n 'author\\'s own cost below level 10.') +\n ' Character level uses the game\\'s own curve, untouched.

' +\n '' +\n '';\n [1, 5, 9, 10, 15, 25, 50, 75, 90, 109].forEach(function (l) {\n h += '';\n });\n h += '
LevelExperience for the nextOriginal game
' + l +\n '' + MOD_wikiNum(MOD_expnextFor(l)) + '' +\n MOD_wikiNum(MOD_vanillaExpnext(l)) + '
';\n return h;\n});\n\n/* --- 3. skills ------------------------------------------------------------ */\nMOD_wikiPage('skills', 'Skills', function () {\n var cap = MOD_levelCap();\n var bySection = {};\n var keys = [];\n for (var k in skl) {\n var s = skl[k];\n if (!s || typeof s !== 'object' || !s.name) continue;\n keys.push(k);\n }\n keys.forEach(function (k) {\n var s = skl[k];\n var sec = MOD_sectionOf(s);\n (bySection[sec] = bySection[sec] || []).push(k);\n });\n\n var h = '

Skills

' +\n '

' + keys.length + ' skills in ' +\n Object.keys(MOD_SECTIONS).length + ' sections. Every one has perks at ' +\n MOD_LADDER.join(', ') + ', and every one that has milestones grants titles at ' +\n MOD_TITLE_RUNGS.join(', ') + '. Levels and ticks below are yours.

';\n\n Object.keys(MOD_SECTIONS).forEach(function (sec) {\n var list = bySection[sec];\n if (!list || !list.length) return;\n var parentKey = MOD_PARENT_KEY[sec];\n var g = '';\n if (parentKey && skl[parentKey]) {\n g += '

Parent skill: ' +\n MOD_WIKI.safe(skl[parentKey].bname || skl[parentKey].name) +\n ' — earns ' + Math.round(MOD_PARENT.feed * 100) +\n '% of what its children earn, and speeds up any child that lags behind it.

';\n }\n list.sort(function (a, b) { return skl[a].name < skl[b].name ? -1 : 1; });\n list.forEach(function (k) {\n var s = skl[k];\n var lvl = Number(s.lvl) || 0;\n g += '
' +\n '

' + MOD_WIKI.safe(s.bname || s.name) +\n (s.bname ? ' (' + MOD_WIKI.safe(s.name) + ')' : '') +\n '= cap ? ' capped' : '') + '\">lv ' + lvl +\n (lvl >= cap ? ' — at the cap' : '') + '

' +\n '
' + MOD_wikiDesc(s.desc) + '
';\n\n var at110 = MOD_wikiEffectAt(k, s, 110);\n if (at110) g += '
At level 110: ' + at110 + '
';\n\n if (s.mlstn && s.mlstn.length) {\n g += '';\n s.mlstn.forEach(function (m) {\n g += '' +\n '' +\n '' +\n '';\n });\n g += '
' + m.lv + '' + MOD_WIKI.safe(m.p || '—') + '' + (m.g === true ? '✓' : '') + '
';\n }\n g += '
';\n });\n h += MOD_wikiGroup(MOD_WIKI.safe(MOD_SECTIONS[sec]), list.length, g, 2);\n });\n return h;\n});\n\n/* --- 4. cultivation ------------------------------------------------------- */\nMOD_wikiPage('cultivation', 'Cultivation', function () {\n var now = MOD_realm();\n var elig = 0;\n try { elig = MOD_realmEligible().n; } catch (e) {}\n var qic = skl.qic ? skl.qic.lvl : 0;\n\n var h = '

Cultivation

' +\n '

Ten realms above Mortal. Circulating qi levels the Qi ' +\n 'Circulation skill; the skill does not advance the realm. Reaching the level ' +\n 'opens a bottleneck, and breaking through it costs a pill and can fail — the ' +\n 'pill is spent either way.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Realm' + MOD_WIKI.safe(MOD_realmTitle()) + '
Spiritual Root' + MOD_WIKI.safe(MOD_root().name) + '
Qi Circulationlevel ' + qic + '
Insight' + MOD_insight() + '
Bottleneck open' +\n (elig > now.n ? 'yes — ' + MOD_WIKI.safe(MOD_REALMS[now.n + 1].name) +\n ' is within reach' : 'no') + '
Qi Deviation' + (MOD_deviated()\n ? MOD_deviationLeft() + ' in-game minutes left' : 'no') + '
' +\n '

Realm thresholds are the same levels that earn a title rank, so realm N ' +\n 'is the level at which a rank N title arrives. Every realm multiplies all ' +\n 'stats and health, and the multipliers do not stack with each other — the ' +\n 'realm you are in is the one that applies.

' +\n '' +\n '';\n\n MOD_REALMS.forEach(function (r) {\n var pill = r.n >= 1 ? item['mod_bp' + r.n] : null;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmNameQi Circ.StatsHealthPill
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + r.qic + '×' + r.mult + '×' + r.hp + '' + (pill ? MOD_WIKI.safe(pill.name) : '—') +\n '
' +\n\n '

Where the pills come from

' +\n '

Every breakthrough pill has a source in normal play. None of them are ' +\n 'craftable, and none are cheap — a bottleneck you can buy in bulk is not a ' +\n 'bottleneck.

' +\n '' +\n '' +\n '' +\n '' +\n '
RealmsSource
1The dojo instructor, when qi ' +\n 'circulation is first taught
2–5The Herbalist in the village ' +\n '(vendor.pha1), restocked
6–10The dojo\\'s Level Advancement ' +\n 'ladder, and the Pill Tower
' +\n '

Realm 1 is deliberately not sold at the marketplace: ' +\n 'the marketplace is behind a 40%-a-visit chance encounter, and realm 1 opens ' +\n 'long before that is reliable.

' +\n\n '

Breaking through

' +\n '

A bottleneck wants three things, which is what the genre says it wants: ' +\n 'a pill, insight, and enough consolidation to make the ' +\n 'attempt worth making. The pill and the insight are both spent whether the ' +\n 'attempt works or not.

' +\n '

The odds improve the further past the threshold you are, plus your root, ' +\n 'plus time sat in Closed Door Training, and cap short of certain.

' +\n '' +\n '';\n [0, 2, 4, 6, 8].forEach(function (o) {\n var base = Math.min(0.55 + o * 0.05 + MOD_root().odds, 0.95);\n h += '';\n });\n h += '
Levels past the thresholdChanceAfter full seclusion
+' + o + '' +\n Math.round(base * 100) + '%' +\n Math.round(Math.min(base + MOD_CULT.secludeOdds, 0.95) * 100) + '%
' +\n\n '

Insight

' +\n '

Insight is the half of the price that cannot be bought. It comes from ' +\n 'three places, and they are the three the genre names: meditating, fights you ' +\n 'nearly lost, and going somewhere you have not been.

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
SourceRate
Circulate Qi, at a wall' +\n (MOD_CULT.meditateChance * 100).toFixed(1) + '% a second
Closed Door Training' +\n (MOD_CULT.secludeChance * 100).toFixed(1) + '% a second
A kill made under ' +\n Math.round(MOD_CULT.brinkFrac * 100) + '% health' +\n Math.round(MOD_CULT.brinkChance * 100) + '% a kill
Entering an area for the first timealways
' +\n '

The insight a realm asks for is sized to the climb to it: about ' +\n Math.round(MOD_CULT.wallShare * 100) + '% of the Circulate Qi time the climb took, ' +\n 'spent at the wall. Seclusion earns it three times as fast, and full ' +\n 'consolidation takes as long as that on average.

' +\n '' +\n '' +\n '';\n var wkHours = function (sec) {\n return sec >= 7200 ? (sec / 3600).toFixed(sec >= 36000 ? 0 : 1) + ' h' : Math.round(sec / 60) + ' min';\n };\n MOD_REALMS.forEach(function (r) {\n if (!r.n) return;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmClimb (Circulate Qi, 1x)Insight to enterFull seclusionQi Deviation (in-game)
' + MOD_WIKI.safe(r.name) + '' + wkHours(MOD_realmClimbSec(r.n)) + '' + MOD_insightNeed(r.n) + '' + wkHours(MOD_secludeFull(r.n)) + '' + (MOD_devMinutes(r.n) / 1440).toFixed(1) + ' days
' +\n\n '

The nine layers

' +\n '

Each realm is nine layers deep, derived from where Qi Circulation sits ' +\n 'between this realm\\'s threshold and the next one\\'s — so breaking through ' +\n 'puts you at the first layer of the new realm on its own. Layers are worth ' +\n '×' + MOD_CULT.layerMult + ' stats and ×' + MOD_CULT.layerBody +\n ' body each above the first, and the top realm has no rung above it to ' +\n 'measure against, so it stays at its first layer.

' +\n '' +\n '';\n for (var li = 1; li <= MOD_CULT.layers; li++) {\n h += '';\n }\n h += '
LayerCalled
' +\n li + '' + MOD_LAYER_ORD[li] + ' layer, ' +\n MOD_WIKI.safe(MOD_layerStage(li)) + '
' +\n '

At the threshold of the next realm and before breaking ' +\n 'through, you are a half step to it — the genre\\'s own name for the ' +\n 'state this mod calls a bottleneck.

' +\n\n '

Closed Door Training

' +\n '

Seclusion does two things. It consolidates — a full sitting ' +\n '(the table above) is worth +' + Math.round(MOD_CULT.secludeOdds * 100) +\n '% on the attempt — and it shelters: a breakthrough attempted while ' +\n 'you are still sitting in it cannot end in Qi Deviation, only in a lost ' +\n 'pill. It will not start unless you are actually at a wall.

' +\n\n '

Qi Deviation

' +\n '

Failing a breakthrough out in the open leaves your cultivation base ' +\n 'unstable for as long as the seclusion you skipped (the table above, never ' +\n 'under ' + MOD_CULT.devMinutes + ' in-game minutes): stats at ×' +\n MOD_CULT.devStat + ', body at ×' + MOD_CULT.devBody + ', and no second ' +\n 'attempt until it passes. It clears on its own, or at once with a ' +\n 'Qi Settling Pill. It never kills — the death system stays out of it.

' +\n\n '

Heavenly Tribulation

' +\n '

From realm ' + MOD_CULT.tribFrom + ' up, a successful breakthrough is ' +\n 'answered. The cost is a fraction of your maximum health, so it is a gate on ' +\n 'walking in healthy rather than a damage race; the realm bonus you are ' +\n 'standing on is what pays it. Fall to it and the realm does not open.

' +\n '' +\n '';\n MOD_REALMS.forEach(function (r) {\n if (!r.n || r.n < MOD_CULT.tribFrom) return;\n h += '' +\n '' +\n '';\n });\n h += '
RealmBoltsOf your max HP
' + MOD_WIKI.safe(r.name) + '' + MOD_tribulationBolts(r.n) + '' + Math.round(MOD_tribulationCost(r.n) * 100) + '%
' +\n\n '

Spiritual Root

' +\n '

Rolled once, when the dojo finishes teaching you to fight. It is never a ' +\n 'dead end: the worst grade costs cultivation speed, not access, and a ' +\n 'Root Cleansing Pill moves you up a grade.

' +\n '' +\n '';\n var rootTotal = 0;\n MOD_ROOTS.forEach(function (r) { rootTotal += r.weight; });\n MOD_ROOTS.forEach(function (r) {\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
GradeRootChanceCultivationBreakthrough
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + Math.round(r.weight / rootTotal * 100) + '%×' + r.xp + '' + (r.odds >= 0 ? '+' : '') +\n Math.round(r.odds * 100) + '%
';\n return h;\n});\n\n/* --- 5. elemental mastery ------------------------------------------------- */\nMOD_wikiPage('mastery', 'Elemental mastery', function () {\n var h = '

Elemental mastery mod

' +\n '

Six masteries, each with a technique that fires on its own ' +\n 'during a fight. Combat is automatic and there is no ability picker, so a ' +\n 'technique is a chance on each of your swings to become something else — one ' +\n 'that scales with INT and the element rather than with your weapon.

' +\n '

Nothing fires while you are still Mortal: the channels have to be open ' +\n 'first. Chance is ' + (MOD_TECH.perLevel * 100).toFixed(1) + '% per mastery ' +\n 'level plus ' + (MOD_TECH.perRealm * 100).toFixed(1) + '% per realm, capped at ' +\n Math.round(MOD_TECH.cap * 100) + '%; a technique lands for ×' +\n MOD_TECH.power + ' of a normal strike.

' +\n '' +\n '';\n\n var realm = 0;\n try { realm = MOD_realm().n; } catch (e) {}\n MOD_MASTERY.forEach(function (m) {\n var lvl = Number(m.skill.lvl) || 0;\n var ch = realm < 1 ? 0\n : Math.min(lvl * MOD_TECH.perLevel + realm * MOD_TECH.perRealm, MOD_TECH.cap);\n h += '' +\n '' +\n '' +\n '';\n });\n h += '
MasteryTechniqueLevelChance now
' +\n MOD_WIKI.safe(m.skill.name) + '' + MOD_WIKI.safe(m.name) + '' + lvl + '' + (realm < 1 ? 'Mortal'\n : (ch * 100).toFixed(1) + '%') + '
';\n return h;\n});\n\n/* --- 6. titles ------------------------------------------------------------ */\nMOD_wikiPage('titles', 'Titles', function () {\n var perm = MOD_permanentRank();\n var held = MOD_titleCount();\n var total = 0; for (var kk in ttl) if (ttl[kk] && ttl[kk].name) total++;\n\n var h = '

Titles

' +\n '

A title\\'s rank runs 1 to 10 and is derived from the level ' +\n 'that earns it, so a rank is a statement about where in the game you are. ' +\n 'Only the title you are wearing applies — until renown makes the lower ranks ' +\n 'permanent.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '
Titles held' + held + ' of ' + total + '
Renown level' + (skl.rnwn ? skl.rnwn.lvl : 0) +\n ' of ' + MOD_RENOWN_MAX + '
Ranks that apply without wearing' +\n (perm >= 1 ? '1–' + perm : 'none yet') + '
' +\n\n '

Ranks

' +\n '';\n MOD_RANK_AT.forEach(function (lv, i) {\n var st = MOD_rankStyle(i + 1);\n h += '' +\n '';\n });\n h += '
RankEarned from levelColour
' + (i + 1) + '' + lv + '' + '●●●' + '
' +\n\n '

Renown

' +\n '

Renown is not trained. It counts the titles you hold, and each level ' +\n 'multiplies every stat by ' + (1 + MOD_RENOWN_RATE) + ' — compounding, so ' +\n 'level ' + MOD_RENOWN_MAX + ' is ×' +\n (Math.round(MOD_renownMult(MOD_RENOWN_MAX) * 1000) / 1000) + '. At renown N, ' +\n 'every title of rank N or below applies permanently.

' +\n '' +\n '';\n MOD_RENOWN_STEPS.forEach(function (n, i) {\n h += '= n ? 'done' : '') + '\">' +\n '';\n });\n h += '
RenownTitles neededAll statsPassive ranks
' + (i + 1) +\n '' + n + '×' +\n (Math.round(MOD_renownMult(i + 1) * 1000) / 1000) + '1–' + (i + 1) + '
' +\n\n '

Every title

' +\n '

Grouped by the skill that grants it. What is left is either ' +\n 'a ladder of its own — creatures killed, items gathered, the ' +\n 'cultivation realms — or a one-off from the story.

';\n\n var groups = {}, fams = {}, loose = [];\n for (var k in ttl) {\n var t = ttl[k];\n /* Unlike the picker, the wiki KEEPS the author's broken entries and says\n so — same call as area.clg, which is listed and tagged \"nothing spawns\"\n rather than quietly dropped. A reader looking for why a title never\n appears is better served by the row than by its absence. */\n if (!t || t.name === undefined || t.name === null) continue;\n var sk = MOD_TITLE_SKILL[k];\n if (sk && skl[sk]) { (groups[sk] = groups[sk] || []).push(k); continue; }\n var stem = MOD_titleStem(k);\n if (stem) { (fams[stem] = fams[stem] || []).push(k); continue; }\n loose.push(k);\n }\n /* Same rule as the picker: a family of one is not a family. */\n Object.keys(fams).forEach(function (stem) {\n if (fams[stem].length < 2) { loose.push(fams[stem][0]); delete fams[stem]; }\n });\n var order = Object.keys(groups).sort(function (a, b) {\n return skl[a].name < skl[b].name ? -1 : 1;\n });\n\n var row = function (k) {\n var t = ttl[k];\n var st = MOD_rankStyle(t.rar);\n var gen = null;\n for (var i = 0; i < MOD_SKILL_TITLES.length; i++) {\n if (MOD_SKILL_TITLES[i].key === k) { gen = MOD_SKILL_TITLES[i]; break; }\n }\n /* The author left a handful unfinished: ttl.ddcd carries the literal string\n \"null\" for both name and description, and shpt2/shpt3/mone3 are blank.\n None is granted by anything, so none can be earned. */\n var broken = !t.name || t.name === 'null';\n return '
' +\n '' +\n MOD_WIKI.safe(t.name || '(unnamed)') + '' +\n (broken ? 'unfinished' : '') +\n 'rank ' + (t.rar || 1) + '' +\n (gen ? 'lv ' + gen.rung + ' · +' +\n Math.round(gen.xp * 100) + '% ' + MOD_WIKI.safe(skl[gen.skill].name) +\n ' exp' : '') +\n (t.have === true ? 'held' : '') +\n '
' + MOD_wikiDesc(t.desc) + '
';\n };\n\n var byRank = function (a, b) { return (ttl[a].rar || 1) - (ttl[b].rar || 1); };\n order.forEach(function (sk) {\n groups[sk].sort(byRank);\n h += MOD_wikiGroup(MOD_WIKI.safe(skl[sk].bname || skl[sk].name),\n groups[sk].length, groups[sk].map(row).join(''), 3);\n });\n /* The ladders that belong to no skill, by the same derivation the picker\n uses — so the two cannot drift apart. Sorted by the label, since that is\n what the reader scans. */\n var famList = Object.keys(fams)\n .map(function (stem) {\n fams[stem].sort(byRank);\n return { stem: stem,\n label: MOD_titleFamilyLabel(stem, fams[stem].map(function (k) { return ttl[k]; })) };\n })\n .sort(function (a, b) { return a.label < b.label ? -1 : a.label > b.label ? 1 : 0; });\n if (famList.length) {\n /* Nested one level down rather than sitting beside the skills. Eleven more\n top-level summaries cost ~530px on a page that is already the tallest in\n the wiki, and these are a different KIND of thing from a skill anyway. */\n var famInner = '', famTotal = 0;\n famList.forEach(function (f) {\n famTotal += fams[f.stem].length;\n famInner += MOD_wikiGroup(MOD_WIKI.safe(f.label), fams[f.stem].length,\n fams[f.stem].map(row).join(''), 4);\n });\n h += MOD_wikiGroup('Ladders of their own', famTotal, famInner, 3);\n }\n if (loose.length) {\n loose.sort(byRank);\n /* The story titles are the one group with no skill to divide them, and\n there are enough of them to need dividing. Rank rather than the alphabet:\n it is the axis they are already sorted on, and the one a reader cares\n about — a rank is a statement about where in the game a title comes from. */\n var inner;\n if (loose.length <= MOD_WIKI_ALPHA_AT) {\n inner = loose.map(row).join('');\n } else {\n var byRankNo = {};\n loose.forEach(function (k) {\n var r = Math.min(Math.max(Number(ttl[k].rar) || 1, 1), 10);\n (byRankNo[r] = byRankNo[r] || []).push(k);\n });\n inner = '';\n Object.keys(byRankNo).sort(function (a, b) { return a - b; }).forEach(function (r) {\n var st = MOD_rankStyle(r);\n inner += MOD_wikiGroup('Rank ' + r + '', byRankNo[r].length,\n byRankNo[r].map(row).join(''), 4);\n });\n }\n h += MOD_wikiGroup('From the story', loose.length, inner, 3);\n }\n return h;\n});\n\n/* --- 7. areas ------------------------------------------------------------- */\nMOD_wikiPage('areas', 'Areas & enemies', function () {\n var h = '

Areas & enemies

' +\n '

Every area, what lives in it and at what levels, and what it ' +\n 'drops. Enemy strength is not read off these levels — see the note below the ' +\n 'table.

';\n\n /* Two of the game's 21 areas are not places and listing them misleads:\n nwh \"Somewhere\" — where global.current_z parks whenever you are NOT in\n a fight, populated by creature.default (\"Nothing\").\n tst \"Test\" — reached only from chss.tst, which has id -1 and is in no\n sector, so nothing in the game can travel there.\n The test for \"is this a place\" is whether you can get to it, and neither\n can be. Everything else is listed whether or not you have been. */\n var MOD_WIKI_NOT_PLACES = { nwh: 1, tst: 1 };\n\n var keys = [];\n for (var k in area) {\n if (area[k] && area[k].pop && !MOD_WIKI_NOT_PLACES[k]) keys.push(k);\n }\n /* Ordered by the middle of the level band, which is the closest thing the\n game has to a difficulty order. */\n keys.sort(function (a, b) {\n var mid = function (z) {\n if (!z.pop.length) return 0;\n var s = 0; z.pop.forEach(function (e) { s += (e.lvlmin + e.lvlmax) / 2; });\n return s / z.pop.length;\n };\n return mid(area[a]) - mid(area[b]);\n });\n\n keys.forEach(function (k) {\n var z = area[k];\n /* An area whose baked bands are not finite can never spawn anything — see\n the note rendered under its table. Derived rather than listed by name, so\n it stays true if the game changes. */\n var broken = !z.popc || z.popc.length !== z.pop.length ||\n z.popc.some(function (band) { return !band || !isFinite(band[1] - band[0]); });\n h += '

' + MOD_WIKI.safe(z.name) +\n ' ' + k + '' +\n (broken ? 'nothing spawns' : '') +\n '' + (z.size === -1 ? 'endless' : z.size + ' to clear') +\n '

';\n if (z.pop.length) {\n /* The spawn share is NOT pop[i].c. z_bake normalises those weights into\n area.popc — a list of [lo,hi] bands that mon_gen rolls a uniform\n random against — and the weights do not have to sum to 1, so they are\n off wherever they don't. The Southern forest's .35/.45/.25 are really\n 33/43/24%. Read the bands the game actually rolls against. */\n var bands = z.popc || [];\n h += '';\n z.pop.forEach(function (e, i) {\n var c = e.crt || {};\n var band = bands[i];\n var share = band ? band[1] - band[0] : NaN;\n h += '' +\n '' +\n '';\n });\n h += '
' + MOD_WIKI.safe(c.name || '?') + 'lv ' + e.lvlmin +\n (e.lvlmax !== e.lvlmin ? '–' + e.lvlmax : '') + '' + (isFinite(share)\n ? Math.round(share * 100) + '% of spawns'\n : '—') + '
';\n if (broken) {\n h += '
Nothing spawns here. The area\\'s entries carry ' +\n 'no spawn weight, so the table the game rolls against comes out as ' +\n 'NaN and no creature can ever match it. A bug in the base game, left ' +\n 'as it is — nothing in the game travels to this area either.
';\n }\n }\n if (z.drop && z.drop.length) {\n var d = z.drop.map(function (x) {\n return MOD_WIKI.safe((x.item && x.item.name) || '?') +\n ' ' + ((x.c || 0) * 100).toFixed(2) + '%';\n }).join(', ');\n h += '
Drops: ' + d + '
';\n }\n h += '
';\n });\n\n var fightsOrig = MOD_FIGHTS.mode === 'original';\n h += '

How hard an enemy is

' +\n '

It depends on the Fights setting, which is ' +\n (fightsOrig ? 'Original' : 'Scaled') + ' now. On Original (the default) ' +\n 'every area the author wrote is exactly as he wrote it: his creatures, his ' +\n 'level ranges, and you out-grow them as you do in his game. On Scaled every ' +\n 'area is sized to you. The three areas past the golem arena and the rank ' +\n 'duels are sized to you either way.

' +\n '

Sizing to you works like this. Enemies are not fitted to a level. Each spawn is solved against your ' +\n 'actual power at the moment it appears, aiming at roughly ' + MOD_ENEMY.kill +\n ' swings to kill it and ' + MOD_ENEMY.die + ' before it kills you, with a ' +\n 'guaranteed margin between the two so a fight is never a coin flip. Variety ' +\n 'comes from the area\\'s level band and each creature\\'s own stat weighting, ' +\n 'not from difficulty scaling.

' +\n '

The reason for that: within the first stage alone you go from 1 STR to ' +\n 'around 265, so anything anchored to a level is either trivial at the end of ' +\n 'a stage or impossible at the start of one.

';\n return h;\n});\n\n/* --- 8. items -------------------------------------------------------------\n The one place where \"list everything\" needs help: 321 of the game's 371\n plain items are the same inventory type, so grouping by type alone leaves a\n single group of 321. Anything past MOD_WIKI_ALPHA_AT gets an alphabetical\n sub-split as well — the oldest index pattern there is, and derived rather\n than invented, which a hand-made taxonomy would not be.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_ALPHA_AT = 60;\n\n/* The game's own inventory tabs (isort): ALL / WPN / EQP / USE / OTHER. Using\n its categories rather than new ones means a group here is a tab there. */\nvar MOD_WIKI_STYPE = { 2: 'Weapons', 3: 'Equipment', 4: 'Usable', 5: 'Other' };\n\nfunction MOD_wikiItemRow(it) {\n var val = it.v !== undefined ? it.v : (it.val !== undefined ? it.val : null);\n return '
' +\n '

' + MOD_WIKI.safe(it.name) +\n (it.rar > 1 ? 'rarity ' + it.rar + '' : '') +\n (val !== null ? '' + MOD_wikiNum(val) + ' coin' : '') +\n (it.amount > 0 ? '×' + it.amount + '' : '') +\n '

' + MOD_wikiDesc(it.desc) + '
';\n}\n\n/* The letter a name files under. Several items are quoted — the master's\n manuals are literally named \"Sword Saint Manual\" — and filing those under a\n quotation mark is no use to anyone looking for S. */\nfunction MOD_wikiLetter(name) {\n var m = String(name || '').replace(/^[^0-9A-Za-z]+/, '');\n return (m.charAt(0) || '?').toUpperCase();\n}\n\nfunction MOD_wikiByLetter(a, b) {\n var x = MOD_wikiLetter(a.name) + String(a.name || '').toLowerCase();\n var y = MOD_wikiLetter(b.name) + String(b.name || '').toLowerCase();\n return x < y ? -1 : (x > y ? 1 : 0);\n}\n\n/* Flat below the threshold, alphabetically sub-grouped above it. Ranges are cut\n from the entries actually present, so they stay even as content is added. */\nfunction MOD_wikiAlpha(entries, render, level) {\n if (entries.length <= MOD_WIKI_ALPHA_AT) return entries.map(render).join('');\n var per = Math.ceil(entries.length / Math.ceil(entries.length / 40));\n var out = '';\n for (var i = 0; i < entries.length; i += per) {\n var chunk = entries.slice(i, i + per);\n var first = MOD_wikiLetter(chunk[0].name);\n var last = MOD_wikiLetter(chunk[chunk.length - 1].name);\n out += MOD_wikiGroup(first === last ? first : first + '–' + last,\n chunk.length, chunk.map(render).join(''), level || 4);\n }\n return out;\n}\n\nMOD_wikiPage('items', 'Items', function () {\n var h = '

Items

' +\n '

Everything the game defines, whether or not you have seen ' +\n 'it. Grouped the way your inventory groups it. Values are the base sell ' +\n 'price where the game gives one.

';\n\n /* item[] first, split by the inventory tab it appears under. */\n var buckets = {};\n for (var k in item) {\n var it = item[k];\n if (!it || !it.name || it.name === 'dummy') continue;\n var lab = MOD_WIKI_STYPE[it.stype] || 'Uncategorised';\n (buckets[lab] = buckets[lab] || []).push(it);\n }\n ['Usable', 'Other', 'Weapons', 'Equipment', 'Uncategorised'].forEach(function (lab) {\n var list = buckets[lab];\n if (!list || !list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(lab, list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n\n /* and the equipment objects, which live in their own globals */\n [['Weapons', wpn], ['Armour', eqp], ['Shields', sld], ['Accessories', acc]]\n .forEach(function (g) {\n var list = [];\n for (var kk in g[1]) {\n if (g[1][kk] && g[1][kk].name && g[1][kk].name !== 'dummy') list.push(g[1][kk]);\n }\n if (!list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(g[0], list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n return h;\n});\n\n/* --- 9. actions ----------------------------------------------------------- */\nMOD_wikiPage('actions', 'Actions', function () {\n var unlock = {};\n MOD_ACTION_UNLOCKS.forEach(function (u) {\n if (u.act) unlock[u.act.id] = (skl[u.skill] ? skl[u.skill].name : u.skill) +\n ' level ' + u.lv;\n });\n if (typeof act.mod_qi !== 'undefined') {\n unlock[act.mod_qi.id] = 'clearing the dojo\\'s Easiest, Easy and Normal fights';\n }\n\n var h = '

Actions

' +\n '

What you can be doing. One at a time, and only where the ' +\n 'action makes sense — unless \"Unrestricted actions\" is ticked in settings, ' +\n 'which lets several run at once anywhere. Both limits are deliberate; the ' +\n 'switch is there because they are not to everyone\\'s taste.

';\n\n var keys = [];\n for (var k in act) if (act[k] && act[k].name && act[k].name !== 'dummy') keys.push(k);\n keys.sort(function (a, b) { return act[a].name < act[b].name ? -1 : 1; });\n\n keys.forEach(function (k) {\n var a = act[k];\n var how = unlock[a.id];\n h += '
' +\n '

' + MOD_WIKI.safe(a.name) +\n (a.have === true ? 'unlocked'\n : 'locked') +\n (how ? 'from ' + MOD_WIKI.safe(how) + '' : '') +\n '

' + MOD_wikiDesc(a.desc) + '
';\n });\n return h;\n});\n\n/* --- 10. the dojo --------------------------------------------------------- */\nMOD_wikiPage('dojo', 'The dojo', function () {\n var h = '

The dojo

' +\n '

The instructor promises a reward every five levels. The base ' +\n 'game stops delivering at 30; the mod carries the same ladder to 110, on the ' +\n 'same terms.

' +\n '

Only the lowest rung you have not claimed is offered, which is how the ' +\n 'first six behave. Rewards are sized against the character experience curve ' +\n '(4×level3 + level2) — a level at 110 costs 5.3 ' +\n 'million, which is why the base game\\'s best pill is worth 0.3% of one there.

' +\n '' +\n '';\n\n MOD_DOJO_RUNGS.forEach(function (r) {\n var pill = item[r.pill];\n var extra = [];\n if (r.realmPill) extra.push('breakthrough pill for realm ' + r.realmPill);\n if (r.manual) extra.push('a choice of master\\'s manual');\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
LevelRewardCoinAlso
' + r.lv + '' + (pill ? MOD_WIKI.safe(pill.name) + ' ×' + r.n\n : '—') + '' + MOD_wikiNum(r.coin) + '' + (extra.join(', ') || '—') + '' + (global.flags[r.flag] ? 'claimed' : '') +\n '
' +\n\n '

Spirit pills

' +\n '

Character experience in a bottle. The four the mod adds continue the ' +\n 'game\\'s own series up to the new curve.

' +\n '' +\n '';\n MOD_PILLS.forEach(function (p) {\n h += '';\n });\n h += '
PillExperience
' + MOD_WIKI.safe(p[2]) + '' +\n p[3].toLocaleString() + '
' +\n\n '

Master\\'s manuals

' +\n '

A third grade of skillbook, offered at levels 50, 75 and 100 as a choice ' +\n 'the same way the first one was. Each is worth +' +\n Math.round(MOD_MANUAL_RATE * 100) + '% experience in its skill, permanently.

' +\n '' +\n '';\n MOD_MANUALS.forEach(function (m) {\n h += '';\n });\n h += '
ManualSkill
' + MOD_WIKI.safe(m[2]) + '' +\n MOD_WIKI.safe(skl[m[0]] ? (skl[m[0]].bname || skl[m[0]].name) : m[0]) +\n '
';\n return h;\n});\n\n/* --- 11. mechanics -------------------------------------------------------- */\nMOD_wikiPage('mechanics', 'Mechanics', function () {\n return '

Mechanics

' +\n\n '

Damage is subtractive, in both directions

' +\n '

A hit does the attacker\\'s strength minus the defender\\'s strength, plus ' +\n 'the weapon, plus one, before criticals. A defender\\'s STR is their ' +\n 'armour — there is no separate armour stat doing that job.

' +\n '

Two consequences worth carrying around:

' +\n '
    ' +\n '
  • Damage does not taper, it stops. Once a defender\\'s STR passes ' +\n 'yours, your hits do not get small; they land for nothing. Progress against ' +\n 'something too strong is not slow, it is zero.
  • ' +\n '
  • A stat ratio tells you nothing. Comparing your HP to an enemy\\'s ' +\n 'strength ignores the subtraction entirely and reads several times off in ' +\n 'either direction.
  • ' +\n '
' +\n '

This is also the reason enemies are never scaled by raising ' +\n 'their strength: it would raise their offence and their armour together, and ' +\n 'the second effect arrives first.

' +\n\n '

Criticals carry the late game

' +\n '

Critical rate reaches about a third at the top of the ladder and a ' +\n 'critical is roughly eight times a normal swing, so around three quarters of ' +\n 'all damage done at that point is critical damage. Anything that raises ' +\n 'critical rate or critical damage is worth more than it looks; Sharp Eye and ' +\n 'War are the two that do.

' +\n\n '

Hit chance

' +\n '

Landing a hit is a contest of agility. It is a ratio rather than a ' +\n 'subtraction, so agility behaves more gently than strength does — falling ' +\n 'behind on it costs you accuracy, not everything.

' +\n\n '

Skill sections and parents

' +\n '

Each of the ' + Object.keys(MOD_SECTIONS).length + ' sections has a parent ' +\n 'skill. A parent earns ' + Math.round(MOD_PARENT.feed * 100) + '% of everything ' +\n 'its children earn and grants no stats of its own. In return, a child below ' +\n 'the parent\\'s level trains ' + Math.round(MOD_PARENT.perLevel * 100) +\n '% faster per level of lag, up to ' + MOD_PARENT.maxGap + ' levels behind. ' +\n 'Bringing one section up makes every skill in it cheaper to catch up.

' +\n\n '

Saving

' +\n '

Three slots, switched from the bottom bar. The game builds its world once ' +\n 'at startup and cannot unload a save, so switching slots or starting a new ' +\n 'game reloads the page. Exporting a save before running anything unusual is ' +\n 'always the safe move.

';\n});\n\n/* --- the document ---------------------------------------------------------\n One string, no fetches, no dependencies: this has to work from file:// as\n readily as from a server.\n ------------------------------------------------------------------------- */\n\nfunction MOD_wikiCss() {\n return [\n ':root{color-scheme:dark}',\n '*{box-sizing:border-box}',\n 'body{margin:0;background:#060a1c;color:#dfe6f5;font:14px/1.6 \"MS Gothic\",' +\n '\"MS Mincho\",monospace;display:flex;min-height:100vh}',\n 'a{color:#7cb0ff}',\n 'nav{width:210px;flex:0 0 210px;background:#081040;border-right:1px solid #46a;' +\n 'position:sticky;top:0;height:100vh;overflow:auto;padding:14px 0}',\n 'nav h2{font-size:13px;color:#9fb4d8;margin:0 0 10px 14px;letter-spacing:1px}',\n 'nav a{display:block;padding:6px 14px;text-decoration:none;color:#cfe0ff;' +\n 'border-left:3px solid transparent}',\n 'nav a:hover{background:#154080}',\n 'nav a.on{background:#123070;border-left-color:#7cb0ff;color:#fff}',\n 'nav .srch{margin:0 10px 12px;width:calc(100% - 20px);background:#050a20;' +\n 'color:#dfe6f5;border:1px solid #46a;padding:5px;font:inherit}',\n 'nav .xall{display:block;margin:12px 10px 0;color:#8296b8;font-size:12px;' +\n 'cursor:pointer;-webkit-user-select:none;user-select:none}',\n 'nav .xall input{vertical-align:-1px;margin-right:4px}',\n 'details.wk-g{border:1px solid #17224a;background:#0a1130;margin:8px 0}',\n 'details.wk-g>summary{cursor:pointer;padding:7px 12px;list-style:none;' +\n 'background:#0d1740}',\n 'details.wk-g>summary:hover{background:#154080}',\n 'details.wk-g>summary::-webkit-details-marker{display:none}',\n // a caret drawn rather than borrowed, so it is the same in every browser\n 'details.wk-g>summary::before{content:\"\\\\25B8\";color:#7cb0ff;' +\n 'display:inline-block;width:1em;transition:transform .12s}',\n 'details.wk-g[open]>summary::before{transform:rotate(90deg)}',\n 'details.wk-g>summary h2,details.wk-g>summary h3{display:inline;margin:0;' +\n 'font-size:15px;color:#fff}',\n '.wk-gi{padding:2px 12px 10px}',\n 'details.wk-g details.wk-g>summary h4{display:inline;margin:0;font-size:13px;' +\n 'color:#cfe0ff;font-weight:normal}',\n 'details.wk-g details.wk-g>summary{padding:5px 10px;background:#0b1435}',\n 'main{flex:1;min-width:0;padding:22px 30px 80px;max-width:1000px}',\n 'h1{font-size:26px;margin:0 0 6px;color:#fff;border-bottom:1px solid #46a;' +\n 'padding-bottom:8px}',\n 'h2{font-size:19px;color:#bcd4ff;margin:30px 0 8px}',\n 'h3{font-size:15px;color:#fff;margin:0 0 4px}',\n 'p.lede{color:#a8bcdc;margin:0 0 18px}',\n '.dim{color:#8296b8;font-weight:normal}',\n '.flav{font-size:12px;font-style:italic}',\n '.note{background:#0b1638;border-left:3px solid #46a;padding:8px 12px;' +\n 'color:#a8bcdc;margin:14px 0}',\n 'code,pre{background:#0b1638;border:1px solid #24356e;padding:1px 5px;' +\n 'color:#9fd0ff}',\n 'pre{padding:10px;overflow-x:auto}',\n 'dl dt{color:#fff;margin-top:14px}',\n 'dl dd{margin:2px 0 0 0;color:#a8bcdc}',\n 'ul{color:#a8bcdc}',\n 'table.wk-tbl{border-collapse:collapse;width:100%;margin:10px 0;' +\n 'display:block;overflow-x:auto}',\n 'table.wk-tbl th{text-align:left;color:#9fb4d8;font-weight:normal;' +\n 'border-bottom:1px solid #46a;padding:5px 9px;white-space:nowrap}',\n 'table.wk-tbl td{border-bottom:1px solid #17224a;padding:5px 9px;' +\n 'vertical-align:top}',\n 'table.wk-tbl tr.done td{color:#cfe0ff}',\n 'table.wk-tbl tr.here{background:#123070}',\n '.num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}',\n '.wk-e{border:1px solid #17224a;background:#081040;padding:9px 12px;' +\n 'margin:7px 0;border-radius:2px}',\n 'tr.wk-e{border:0;background:none}',\n '.wk-e.done{border-color:#2a4a8c}',\n '.wk-e h3{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}',\n '.lv{margin-left:auto;font-size:12px;color:#8296b8;font-weight:normal}',\n '.lv.capped{color:#ffd24a}',\n '.rank{font-size:12px;color:#8296b8;font-weight:normal}',\n '.desc{color:#a8bcdc;font-size:13px}',\n '.proj{color:#ffd24a;font-size:12px;margin-top:3px}',\n '.tag{font-size:11px;border:1px solid #46a;color:#9fb4d8;padding:0 5px;' +\n 'border-radius:2px;font-weight:normal}',\n '.tag.mod{border-color:#7a4ea8;color:#c07bff}',\n '.tag.done{border-color:#2e7d4f;color:#6fdc9a}',\n '.tag.here{border-color:#ffd24a;color:#ffd24a}',\n '.tag.warn{border-color:#a8562e;color:#ff9a5c}',\n 'table.wk-perks{border-collapse:collapse;margin:6px 0 0;width:100%}',\n 'table.wk-perks td{padding:2px 8px 2px 0;font-size:12px;color:#8296b8;' +\n 'border-bottom:1px solid #101a3c}',\n 'table.wk-perks tr.done td{color:#cfe0ff}',\n 'table.wk-perks td.tick{color:#6fdc9a;width:1em;text-align:right}',\n // fixed columns so the level bands line up between one area and the next\n 'table.wk-pop{table-layout:fixed}',\n 'table.wk-pop td:first-child{width:60%}',\n 'table.wk-pop td:nth-child(2){width:20%}',\n '.wk-title .tname{font-weight:bold}',\n '.wk-now{background:#0b1638;border:1px solid #24356e;padding:10px 14px;' +\n 'margin:0 0 18px;display:inline-block;min-width:320px}',\n '.wk-now h3{color:#9fb4d8;font-weight:normal;font-size:13px;' +\n 'letter-spacing:1px;margin-bottom:6px}',\n '.wk-now td{padding:2px 16px 2px 0}',\n '.wk-now td:last-child{color:#fff;text-align:right}',\n '.pg{display:none}.pg.on{display:block}',\n 'footer{color:#5a6c8c;font-size:12px;border-top:1px solid #17224a;' +\n 'margin-top:40px;padding-top:12px}',\n '@media(max-width:760px){body{display:block}nav{width:auto;height:auto;' +\n 'position:static;flex:none}main{padding:16px}}'\n ].join('\\n');\n}\n\n/* Filtering, page switching and the hash. Small enough to inline, and inlining\n it is the point — the document has to stand on its own. */\nfunction MOD_wikiJs() {\n return [\n 'var pages=[].slice.call(document.querySelectorAll(\".pg\"));',\n 'var links=[].slice.call(document.querySelectorAll(\"nav a[data-p]\"));',\n 'function show(id){',\n ' var found=false;',\n ' pages.forEach(function(p){var on=p.id===\"pg-\"+id;if(on)found=true;',\n ' p.className=\"pg\"+(on?\" on\":\"\")});',\n ' if(!found&&pages.length){pages[0].className=\"pg on\";id=pages[0].id.slice(3)}',\n ' links.forEach(function(a){a.className=a.dataset.p===id?\"on\":\"\"});',\n ' window.scrollTo(0,0);return id;}',\n 'links.forEach(function(a){a.addEventListener(\"click\",function(e){',\n ' e.preventDefault();location.hash=a.dataset.p;show(a.dataset.p)})});',\n 'window.addEventListener(\"hashchange\",function(){show(location.hash.slice(1))});',\n 'show(location.hash.slice(1)||\"start\");',\n // Search filters entries, hides groups left with none, and OPENS the groups\n // that still have some — a match hidden inside a collapsed group reads as\n // no match at all, which is worse than not having search.\n 'var box=document.getElementById(\"srch\");',\n 'var xall=document.getElementById(\"xall\");',\n 'var groups=[].slice.call(document.querySelectorAll(\"details.wk-g\"));',\n 'function filter(){',\n ' var q=box.value.trim().toLowerCase();',\n ' pages.forEach(function(p){',\n ' [].slice.call(p.querySelectorAll(\".wk-e\")).forEach(function(e){',\n ' e.style.display=(!q||e.textContent.toLowerCase().indexOf(q)>=0)?\"\":\"none\"})});',\n ' groups.forEach(function(g){',\n ' var any=[].slice.call(g.querySelectorAll(\".wk-e\")).some(function(e){',\n ' return e.style.display!==\"none\"});',\n ' g.style.display=any?\"\":\"none\";',\n ' g.open=xall.checked||(!!q&&any)}); }',\n 'box.addEventListener(\"input\",filter);',\n 'box.addEventListener(\"keydown\",function(e){if(e.key===\"Escape\"){box.value=\"\";filter()}});',\n // Everything is collapsed by default because three of these pages run past\n // 40,000px open. This is the way back to one long document, which is what\n // the browser's own find needs.\n 'xall.addEventListener(\"change\",filter);'\n ].join('\\n');\n}\n\nfunction MOD_wikiBuild() {\n var nav = '', body = '';\n MOD_WIKI_PAGES.forEach(function (pg) {\n nav += '' +\n MOD_WIKI.safe(pg.label) + '';\n var inner;\n /* One page throwing must not take the wiki down with it — the page says so\n and the rest still renders. */\n try { inner = pg.build(); }\n catch (e) {\n inner = '

' + MOD_WIKI.safe(pg.label) + '

This page ' +\n 'could not be built: ' + MOD_WIKI.safe(e.message) + '

';\n console.warn('[mod] wiki page \"' + pg.id + '\" failed: ' + e.message);\n }\n body += '
' + inner + '
';\n });\n\n var stamp = new Date().toLocaleString();\n return '' +\n '' +\n '' + MOD_WIKI.safe(MOD_WIKI.title) + '' +\n '' +\n '' +\n '
' + body +\n '
Generated from the running game at ' + MOD_WIKI.safe(stamp) +\n ' — mod ' + MOD_WIKI.safe(MOD.version) + '. Everything here is read from the ' +\n 'live data, including your own save, so it cannot fall out of step with the ' +\n 'game. The game is truezhangwei\\'s; the mod is not endorsed by the author.' +\n '
' +\n ' inside content closing the block early, so that is the\n one thing scrubbed. */\n safe: function (h) {\n return String(h === undefined || h === null ? '' : h).replace(/<\\/script/gi, '<\\\\/script');\n }\n};\n\n/* Descriptions carry real HTML (coloured spans, the game's separator div), so\n they are passed through rather than escaped — but the separator becomes a\n rule, since a wiki row is not a tooltip. */\nfunction MOD_wikiDesc(d) {\n var s;\n try { s = (typeof d === 'function') ? d() : d; } catch (e) { s = ''; }\n if (!s) return '';\n s = String(s).split('
 
').join('
');\n return MOD_WIKI.safe(s);\n}\n\nfunction MOD_wikiNum(n) {\n if (!isFinite(n)) return '—';\n return Math.round(n).toLocaleString();\n}\n\n/* An effect line at a level the player has not reached yet. The effect\n functions read the skill they are handed, so the level is borrowed and put\n back; anything that throws simply gets no projection. */\nfunction MOD_wikiEffectAt(key, sk, lvl) {\n var eff = MOD_EFFECTS[key];\n if (!eff) return '';\n var was = sk.lvl, out = '';\n try { sk.lvl = lvl; out = eff(sk) || ''; }\n catch (e) { out = ''; }\n finally { sk.lvl = was; }\n return MOD_WIKI.safe(out);\n}\n\n/* --- page builders --------------------------------------------------------\n Each returns the inner HTML of one page. `wk-e` marks a searchable entry and\n `wk-g` a group that hides itself when all its entries are filtered out.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_PAGES = [];\n\nfunction MOD_wikiPage(id, label, build) {\n MOD_WIKI_PAGES.push({ id: id, label: label, build: build });\n}\n\n/* A collapsible group.\n\n Not decoration: with every skill, item and title listed, three of the eleven\n pages ran past 40,000 pixels — a hundred screens of scrolling, which is the\n same as not having the content at all. Collapsed, each of those pages opens\n as an index you can read in one screen and expand where you want.\n\n
rather than a click handler, so it still works with JavaScript off,\n the browser's own find can be made to reach inside it, and the summary is\n keyboard-reachable for free. `level` is the heading level for the summary,\n since a group is sometimes a section of a page and sometimes a subsection. */\nfunction MOD_wikiGroup(heading, count, inner, level) {\n var hn = 'h' + (level || 2);\n return '
<' + hn + '>' + heading +\n (count !== null && count !== undefined\n ? ' (' + count + ')' : '') +\n '
' + inner + '
';\n}\n\n/* --- 1. orientation ------------------------------------------------------- */\nMOD_wikiPage('start', 'Start here', function () {\n var cap = MOD_levelCap();\n var realm = MOD_realm();\n var h = '

proto23

' +\n '

An idle RPG by truezhangwei, plus a mod. You train skills by ' +\n 'doing things, the things you can do open up as the story does, and none of ' +\n 'it is in a hurry.

' +\n\n '

Where you are right now

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Character level' + MOD_wikiNum(you.lvl) + '
Skill level cap' + cap + ' — ' +\n MOD_WIKI.safe(MOD_CAP.name) + '
Cultivation realm' + MOD_WIKI.safe(realm.name) +\n ' (' + realm.n + ' of ' + (MOD_REALMS.length - 1) + ')
Titles held' + MOD_titleCount() + ' — renown ' +\n (skl.rnwn ? skl.rnwn.lvl : 0) + '
Coin' + MOD_wikiNum(you.wealth || 0) + '
' +\n\n '

The four things worth knowing early

' +\n '
' +\n '
Your skill levels are capped by the story, not by grinding.
' +\n '
The cap starts at 10 and ends at 110, and it moves when you reach a new ' +\n 'place — not when you earn enough experience. If a skill has stopped ' +\n 'levelling, you are not doing it wrong; you need to go somewhere. See ' +\n 'Progression.
' +\n\n '
Armour is subtraction, so levels matter more than they look.
' +\n '
A hit does the attacker\\'s STR minus the defender\\'s STR. That means a ' +\n 'small gap in strength is a large gap in damage, and a big enough one takes ' +\n 'your damage to nothing at all rather than merely reducing it. See ' +\n 'Mechanics.
' +\n\n '
Most skills have a parent that pulls them along.
' +\n '
Every section has a parent skill that earns a share of everything its ' +\n 'children do, and any child lagging behind the parent trains faster — up to ' +\n 'twice as fast at ' + MOD_PARENT.maxGap + ' levels behind. A neglected skill is ' +\n 'cheap to bring up later, which is the only reason having this many is ' +\n 'workable.
' +\n\n '
Cultivation is a second ladder, and it is not automatic.
' +\n '
Circulating qi levels a skill; the skill does not raise your realm. ' +\n 'Reaching the level opens a bottleneck, and breaking through it costs a pill ' +\n 'and can fail. See Cultivation.
' +\n '
' +\n\n '

What is the mod and what is the game

' +\n '

The game is index.html and it is entirely truezhangwei\\'s ' +\n 'work. Everything in mod.js is a modification of it and is not ' +\n 'endorsed by the author. Pages here mark added content with ' +\n 'mod where the distinction matters. The full ' +\n 'list of changes is in the changelog, on the bottom bar.

';\n return h;\n});\n\n/* --- 2. progression ------------------------------------------------------- */\nMOD_wikiPage('progress', 'Progression', function () {\n var cap = MOD_levelCap();\n var h = '

Progression

' +\n '

Skill levels are capped by how far the story has gone. Ten ' +\n 'rungs, 10 through 110. The cap applies to every skill at once; Renown is ' +\n 'the single exception, since it is counted rather than trained.

' +\n '' +\n '';\n\n MOD_TIERS.forEach(function (t) {\n var got = null;\n try { got = MOD_tierFlag(t); } catch (e) {}\n var here = (t.cap === cap);\n var need = t.needText ? t.needText\n : (t.flags.length ? t.flags.join(' or ') : 'the start of the game');\n h += '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
CapStageWhat opens it
' + t.cap + '' + MOD_WIKI.safe(t.name) + '' + MOD_WIKI.safe(need) + '' + (here ? 'you are here'\n : got ? 'reached' : '') + '
' +\n\n '

The experience curve

' +\n '

The game\\'s own skill curve is super-exponential — level 109 to 110 ' +\n 'alone costs ' + MOD_wikiNum(MOD_vanillaExpnext(109)) + ' experience, so the top of the ladder ' +\n 'is unreachable by a factor of about ten trillion. But the author only ever ' +\n 'designed skills up to level 15, and below that his curve is fine.

' +\n (MOD_XP.vanillaTo > 0\n ? '

So the mod keeps it. Below level ' + MOD_XP.vanillaTo + ' a skill costs ' +\n 'exactly what the original charges; from there it rises by ×' +\n MOD_XP.ratio.toFixed(4) + ' a level, a ratio solved so the climb to 110 takes ' +\n 'the same time it always did.'\n : '

You have chosen the Mod before 4.2 pacing: a plain geometric curve, ' +\n '×' + MOD_XP.ratio.toFixed(3) + ' a level from level 0, much faster than the ' +\n 'original early on. The Original pacing in settings puts back the ' +\n 'author\\'s own cost below level 10.') +\n ' Character level uses the game\\'s own curve, untouched.

' +\n '' +\n '';\n [1, 5, 9, 10, 15, 25, 50, 75, 90, 109].forEach(function (l) {\n h += '';\n });\n h += '
LevelExperience for the nextOriginal game
' + l +\n '' + MOD_wikiNum(MOD_expnextFor(l)) + '' +\n MOD_wikiNum(MOD_vanillaExpnext(l)) + '
';\n return h;\n});\n\n/* --- 3. skills ------------------------------------------------------------ */\nMOD_wikiPage('skills', 'Skills', function () {\n var cap = MOD_levelCap();\n var bySection = {};\n var keys = [];\n for (var k in skl) {\n var s = skl[k];\n if (!s || typeof s !== 'object' || !s.name) continue;\n keys.push(k);\n }\n keys.forEach(function (k) {\n var s = skl[k];\n var sec = MOD_sectionOf(s);\n (bySection[sec] = bySection[sec] || []).push(k);\n });\n\n var h = '

Skills

' +\n '

' + keys.length + ' skills in ' +\n Object.keys(MOD_SECTIONS).length + ' sections. Every one has perks at ' +\n MOD_LADDER.join(', ') + ', and every one that has milestones grants titles at ' +\n MOD_TITLE_RUNGS.join(', ') + '. Levels and ticks below are yours.

';\n\n Object.keys(MOD_SECTIONS).forEach(function (sec) {\n var list = bySection[sec];\n if (!list || !list.length) return;\n var parentKey = MOD_PARENT_KEY[sec];\n var g = '';\n if (parentKey && skl[parentKey]) {\n g += '

Parent skill: ' +\n MOD_WIKI.safe(skl[parentKey].bname || skl[parentKey].name) +\n ' — earns ' + Math.round(MOD_PARENT.feed * 100) +\n '% of what its children earn, and speeds up any child that lags behind it.

';\n }\n list.sort(function (a, b) { return skl[a].name < skl[b].name ? -1 : 1; });\n list.forEach(function (k) {\n var s = skl[k];\n var lvl = Number(s.lvl) || 0;\n g += '
' +\n '

' + MOD_WIKI.safe(s.bname || s.name) +\n (s.bname ? ' (' + MOD_WIKI.safe(s.name) + ')' : '') +\n '= cap ? ' capped' : '') + '\">lv ' + lvl +\n (lvl >= cap ? ' — at the cap' : '') + '

' +\n '
' + MOD_wikiDesc(s.desc) + '
';\n\n var at110 = MOD_wikiEffectAt(k, s, 110);\n if (at110) g += '
At level 110: ' + at110 + '
';\n\n if (s.mlstn && s.mlstn.length) {\n g += '';\n s.mlstn.forEach(function (m) {\n g += '' +\n '' +\n '' +\n '';\n });\n g += '
' + m.lv + '' + MOD_WIKI.safe(m.p || '—') + '' + (m.g === true ? '✓' : '') + '
';\n }\n g += '
';\n });\n h += MOD_wikiGroup(MOD_WIKI.safe(MOD_SECTIONS[sec]), list.length, g, 2);\n });\n return h;\n});\n\n/* --- 4. cultivation ------------------------------------------------------- */\nMOD_wikiPage('cultivation', 'Cultivation', function () {\n var now = MOD_realm();\n var elig = 0;\n try { elig = MOD_realmEligible().n; } catch (e) {}\n var qic = skl.qic ? skl.qic.lvl : 0;\n\n var h = '

Cultivation

' +\n '

Ten realms above Mortal. Circulating qi levels the Qi ' +\n 'Circulation skill; the skill does not advance the realm. Reaching the level ' +\n 'opens a bottleneck, and breaking through it costs a pill and can fail — the ' +\n 'pill is spent either way.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Realm' + MOD_WIKI.safe(MOD_realmTitle()) + '
Spiritual Root' + MOD_WIKI.safe(MOD_root().name) + '
Qi Circulationlevel ' + qic + '
Insight' + MOD_insight() + '
Bottleneck open' +\n (elig > now.n ? 'yes — ' + MOD_WIKI.safe(MOD_REALMS[now.n + 1].name) +\n ' is within reach' : 'no') + '
Qi Deviation' + (MOD_deviated()\n ? MOD_deviationLeft() + ' in-game minutes left' : 'no') + '
' +\n '

Realm thresholds are the same levels that earn a title rank, so realm N ' +\n 'is the level at which a rank N title arrives. Every realm multiplies all ' +\n 'stats and health, and the multipliers do not stack with each other — the ' +\n 'realm you are in is the one that applies.

' +\n '' +\n '';\n\n MOD_REALMS.forEach(function (r) {\n var pill = r.n >= 1 ? item['mod_bp' + r.n] : null;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmNameQi Circ.StatsHealthPill
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + r.qic + '×' + r.mult + '×' + r.hp + '' + (pill ? MOD_WIKI.safe(pill.name) : '—') +\n '
' +\n\n '

Where the pills come from

' +\n '

Every breakthrough pill has a source in normal play. None of them are ' +\n 'craftable, and none are cheap — a bottleneck you can buy in bulk is not a ' +\n 'bottleneck.

' +\n '' +\n '' +\n '' +\n '' +\n '
RealmsSource
1The dojo instructor, when qi ' +\n 'circulation is first taught
2–5The Herbalist in the village ' +\n '(vendor.pha1), restocked
6–10The dojo\\'s Level Advancement ' +\n 'ladder, and the Pill Tower
' +\n '

Realm 1 is deliberately not sold at the marketplace: ' +\n 'the marketplace is behind a 40%-a-visit chance encounter, and realm 1 opens ' +\n 'long before that is reliable.

' +\n\n '

Breaking through

' +\n '

A bottleneck wants three things, which is what the genre says it wants: ' +\n 'a pill, insight, and enough consolidation to make the ' +\n 'attempt worth making. The pill and the insight are both spent whether the ' +\n 'attempt works or not.

' +\n '

The odds improve the further past the threshold you are, plus your root, ' +\n 'plus time sat in Closed Door Training, and cap short of certain.

' +\n '' +\n '';\n [0, 2, 4, 6, 8].forEach(function (o) {\n var base = Math.min(0.55 + o * 0.05 + MOD_root().odds, 0.95);\n h += '';\n });\n h += '
Levels past the thresholdChanceAfter full seclusion
+' + o + '' +\n Math.round(base * 100) + '%' +\n Math.round(Math.min(base + MOD_CULT.secludeOdds, 0.95) * 100) + '%
' +\n\n '

Insight

' +\n '

Insight is the half of the price that cannot be bought. It comes from ' +\n 'three places, and they are the three the genre names: meditating, fights you ' +\n 'nearly lost, and going somewhere you have not been.

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
SourceRate
Circulate Qi, at a wall' +\n (MOD_CULT.meditateChance * 100).toFixed(1) + '% a second
Closed Door Training' +\n (MOD_CULT.secludeChance * 100).toFixed(1) + '% a second
A kill made under ' +\n Math.round(MOD_CULT.brinkFrac * 100) + '% health' +\n Math.round(MOD_CULT.brinkChance * 100) + '% a kill
Entering an area for the first timealways
' +\n '

The insight a realm asks for is sized to the climb to it: about ' +\n Math.round(MOD_CULT.wallShare * 100) + '% of the Circulate Qi time the climb took, ' +\n 'spent at the wall. Seclusion earns it three times as fast, and full ' +\n 'consolidation takes as long as that on average.

' +\n '' +\n '' +\n '';\n var wkHours = function (sec) {\n return sec >= 7200 ? (sec / 3600).toFixed(sec >= 36000 ? 0 : 1) + ' h' : Math.round(sec / 60) + ' min';\n };\n MOD_REALMS.forEach(function (r) {\n if (!r.n) return;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmClimb (Circulate Qi, 1x)Insight to enterFull seclusionQi Deviation (in-game)
' + MOD_WIKI.safe(r.name) + '' + wkHours(MOD_realmClimbSec(r.n)) + '' + MOD_insightNeed(r.n) + '' + wkHours(MOD_secludeFull(r.n)) + '' + (MOD_devMinutes(r.n) / 1440).toFixed(1) + ' days
' +\n\n '

The nine layers

' +\n '

Each realm is nine layers deep, derived from where Qi Circulation sits ' +\n 'between this realm\\'s threshold and the next one\\'s — so breaking through ' +\n 'puts you at the first layer of the new realm on its own. Layers are worth ' +\n '×' + MOD_CULT.layerMult + ' stats and ×' + MOD_CULT.layerBody +\n ' body each above the first, and the top realm has no rung above it to ' +\n 'measure against, so it stays at its first layer.

' +\n '' +\n '';\n for (var li = 1; li <= MOD_CULT.layers; li++) {\n h += '';\n }\n h += '
LayerCalled
' +\n li + '' + MOD_LAYER_ORD[li] + ' layer, ' +\n MOD_WIKI.safe(MOD_layerStage(li)) + '
' +\n '

At the threshold of the next realm and before breaking ' +\n 'through, you are a half step to it — the genre\\'s own name for the ' +\n 'state this mod calls a bottleneck.

' +\n\n '

Closed Door Training

' +\n '

Seclusion does two things. It consolidates — a full sitting ' +\n '(the table above) is worth +' + Math.round(MOD_CULT.secludeOdds * 100) +\n '% on the attempt — and it shelters: a breakthrough attempted while ' +\n 'you are still sitting in it cannot end in Qi Deviation, only in a lost ' +\n 'pill. It will not start unless you are actually at a wall.

' +\n\n '

Qi Deviation

' +\n '

Failing a breakthrough out in the open leaves your cultivation base ' +\n 'unstable for as long as the seclusion you skipped (the table above, never ' +\n 'under ' + MOD_CULT.devMinutes + ' in-game minutes): stats at ×' +\n MOD_CULT.devStat + ', body at ×' + MOD_CULT.devBody + ', and no second ' +\n 'attempt until it passes. It clears on its own, or at once with a ' +\n 'Qi Settling Pill. It never kills — the death system stays out of it.

' +\n\n '

Heavenly Tribulation

' +\n '

From realm ' + MOD_CULT.tribFrom + ' up, a successful breakthrough is ' +\n 'answered. The cost is a fraction of your maximum health, so it is a gate on ' +\n 'walking in healthy rather than a damage race; the realm bonus you are ' +\n 'standing on is what pays it. Fall to it and the realm does not open.

' +\n '' +\n '';\n MOD_REALMS.forEach(function (r) {\n if (!r.n || r.n < MOD_CULT.tribFrom) return;\n h += '' +\n '' +\n '';\n });\n h += '
RealmBoltsOf your max HP
' + MOD_WIKI.safe(r.name) + '' + MOD_tribulationBolts(r.n) + '' + Math.round(MOD_tribulationCost(r.n) * 100) + '%
' +\n\n '

Spiritual Root

' +\n '

Rolled once, when the dojo finishes teaching you to fight. It is never a ' +\n 'dead end: the worst grade costs cultivation speed, not access, and a ' +\n 'Root Cleansing Pill moves you up a grade.

' +\n '' +\n '';\n var rootTotal = 0;\n MOD_ROOTS.forEach(function (r) { rootTotal += r.weight; });\n MOD_ROOTS.forEach(function (r) {\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
GradeRootChanceCultivationBreakthrough
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + Math.round(r.weight / rootTotal * 100) + '%×' + r.xp + '' + (r.odds >= 0 ? '+' : '') +\n Math.round(r.odds * 100) + '%
';\n return h;\n});\n\n/* --- 5. elemental mastery ------------------------------------------------- */\nMOD_wikiPage('mastery', 'Elemental mastery', function () {\n var h = '

Elemental mastery mod

' +\n '

Six masteries, each with a technique that fires on its own ' +\n 'during a fight. Combat is automatic and there is no ability picker, so a ' +\n 'technique is a chance on each of your swings to become something else — one ' +\n 'that scales with INT and the element rather than with your weapon.

' +\n '

Nothing fires while you are still Mortal: the channels have to be open ' +\n 'first. Chance is ' + (MOD_TECH.perLevel * 100).toFixed(1) + '% per mastery ' +\n 'level plus ' + (MOD_TECH.perRealm * 100).toFixed(1) + '% per realm, capped at ' +\n Math.round(MOD_TECH.cap * 100) + '%; a technique lands for ×' +\n MOD_TECH.power + ' of a normal strike.

' +\n '' +\n '';\n\n var realm = 0;\n try { realm = MOD_realm().n; } catch (e) {}\n MOD_MASTERY.forEach(function (m) {\n var lvl = Number(m.skill.lvl) || 0;\n var ch = realm < 1 ? 0\n : Math.min(lvl * MOD_TECH.perLevel + realm * MOD_TECH.perRealm, MOD_TECH.cap);\n h += '' +\n '' +\n '' +\n '';\n });\n h += '
MasteryTechniqueLevelChance now
' +\n MOD_WIKI.safe(m.skill.name) + '' + MOD_WIKI.safe(m.name) + '' + lvl + '' + (realm < 1 ? 'Mortal'\n : (ch * 100).toFixed(1) + '%') + '
';\n return h;\n});\n\n/* --- 6. titles ------------------------------------------------------------ */\nMOD_wikiPage('titles', 'Titles', function () {\n var perm = MOD_permanentRank();\n var held = MOD_titleCount();\n var total = 0; for (var kk in ttl) if (ttl[kk] && ttl[kk].name) total++;\n\n var h = '

Titles

' +\n '

A title\\'s rank runs 1 to 10 and is derived from the level ' +\n 'that earns it, so a rank is a statement about where in the game you are. ' +\n 'Only the title you are wearing applies — until renown makes the lower ranks ' +\n 'permanent.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '
Titles held' + held + ' of ' + total + '
Renown level' + (skl.rnwn ? skl.rnwn.lvl : 0) +\n ' of ' + MOD_RENOWN_MAX + '
Ranks that apply without wearing' +\n (perm >= 1 ? '1–' + perm : 'none yet') + '
' +\n\n '

Ranks

' +\n '';\n MOD_RANK_AT.forEach(function (lv, i) {\n var st = MOD_rankStyle(i + 1);\n h += '' +\n '';\n });\n h += '
RankEarned from levelColour
' + (i + 1) + '' + lv + '' + '●●●' + '
' +\n\n '

Renown

' +\n '

Renown is not trained. It counts the titles you hold, and each level ' +\n 'multiplies every stat by ' + (1 + MOD_RENOWN_RATE) + ' — compounding, so ' +\n 'level ' + MOD_RENOWN_MAX + ' is ×' +\n (Math.round(MOD_renownMult(MOD_RENOWN_MAX) * 1000) / 1000) + '. At renown N, ' +\n 'every title of rank N or below applies permanently.

' +\n '' +\n '';\n MOD_RENOWN_STEPS.forEach(function (n, i) {\n h += '= n ? 'done' : '') + '\">' +\n '';\n });\n h += '
RenownTitles neededAll statsPassive ranks
' + (i + 1) +\n '' + n + '×' +\n (Math.round(MOD_renownMult(i + 1) * 1000) / 1000) + '1–' + (i + 1) + '
' +\n\n '

Every title

' +\n '

Grouped by the skill that grants it. What is left is either ' +\n 'a ladder of its own — creatures killed, items gathered, the ' +\n 'cultivation realms — or a one-off from the story.

';\n\n var groups = {}, fams = {}, loose = [];\n for (var k in ttl) {\n var t = ttl[k];\n /* Unlike the picker, the wiki KEEPS the author's broken entries and says\n so — same call as area.clg, which is listed and tagged \"nothing spawns\"\n rather than quietly dropped. A reader looking for why a title never\n appears is better served by the row than by its absence. */\n if (!t || t.name === undefined || t.name === null) continue;\n var sk = MOD_TITLE_SKILL[k];\n if (sk && skl[sk]) { (groups[sk] = groups[sk] || []).push(k); continue; }\n var stem = MOD_titleStem(k);\n if (stem) { (fams[stem] = fams[stem] || []).push(k); continue; }\n loose.push(k);\n }\n /* Same rule as the picker: a family of one is not a family. */\n Object.keys(fams).forEach(function (stem) {\n if (fams[stem].length < 2) { loose.push(fams[stem][0]); delete fams[stem]; }\n });\n var order = Object.keys(groups).sort(function (a, b) {\n return skl[a].name < skl[b].name ? -1 : 1;\n });\n\n var row = function (k) {\n var t = ttl[k];\n var st = MOD_rankStyle(t.rar);\n var gen = null;\n for (var i = 0; i < MOD_SKILL_TITLES.length; i++) {\n if (MOD_SKILL_TITLES[i].key === k) { gen = MOD_SKILL_TITLES[i]; break; }\n }\n /* The author left a handful unfinished: ttl.ddcd carries the literal string\n \"null\" for both name and description, and shpt2/shpt3/mone3 are blank.\n None is granted by anything, so none can be earned. */\n var broken = !t.name || t.name === 'null';\n return '
' +\n '' +\n MOD_WIKI.safe(t.name || '(unnamed)') + '' +\n (broken ? 'unfinished' : '') +\n 'rank ' + (t.rar || 1) + '' +\n (gen ? 'lv ' + gen.rung + ' · +' +\n Math.round(gen.xp * 100) + '% ' + MOD_WIKI.safe(skl[gen.skill].name) +\n ' exp' : '') +\n (t.have === true ? 'held' : '') +\n '
' + MOD_wikiDesc(t.desc) + '
';\n };\n\n var byRank = function (a, b) { return (ttl[a].rar || 1) - (ttl[b].rar || 1); };\n order.forEach(function (sk) {\n groups[sk].sort(byRank);\n h += MOD_wikiGroup(MOD_WIKI.safe(skl[sk].bname || skl[sk].name),\n groups[sk].length, groups[sk].map(row).join(''), 3);\n });\n /* The ladders that belong to no skill, by the same derivation the picker\n uses — so the two cannot drift apart. Sorted by the label, since that is\n what the reader scans. */\n var famList = Object.keys(fams)\n .map(function (stem) {\n fams[stem].sort(byRank);\n return { stem: stem,\n label: MOD_titleFamilyLabel(stem, fams[stem].map(function (k) { return ttl[k]; })) };\n })\n .sort(function (a, b) { return a.label < b.label ? -1 : a.label > b.label ? 1 : 0; });\n if (famList.length) {\n /* Nested one level down rather than sitting beside the skills. Eleven more\n top-level summaries cost ~530px on a page that is already the tallest in\n the wiki, and these are a different KIND of thing from a skill anyway. */\n var famInner = '', famTotal = 0;\n famList.forEach(function (f) {\n famTotal += fams[f.stem].length;\n famInner += MOD_wikiGroup(MOD_WIKI.safe(f.label), fams[f.stem].length,\n fams[f.stem].map(row).join(''), 4);\n });\n h += MOD_wikiGroup('Ladders of their own', famTotal, famInner, 3);\n }\n if (loose.length) {\n loose.sort(byRank);\n /* The story titles are the one group with no skill to divide them, and\n there are enough of them to need dividing. Rank rather than the alphabet:\n it is the axis they are already sorted on, and the one a reader cares\n about — a rank is a statement about where in the game a title comes from. */\n var inner;\n if (loose.length <= MOD_WIKI_ALPHA_AT) {\n inner = loose.map(row).join('');\n } else {\n var byRankNo = {};\n loose.forEach(function (k) {\n var r = Math.min(Math.max(Number(ttl[k].rar) || 1, 1), 10);\n (byRankNo[r] = byRankNo[r] || []).push(k);\n });\n inner = '';\n Object.keys(byRankNo).sort(function (a, b) { return a - b; }).forEach(function (r) {\n var st = MOD_rankStyle(r);\n inner += MOD_wikiGroup('Rank ' + r + '', byRankNo[r].length,\n byRankNo[r].map(row).join(''), 4);\n });\n }\n h += MOD_wikiGroup('From the story', loose.length, inner, 3);\n }\n return h;\n});\n\n/* --- 7. areas ------------------------------------------------------------- */\nMOD_wikiPage('areas', 'Areas & enemies', function () {\n var h = '

Areas & enemies

' +\n '

Every area, what lives in it and at what levels, and what it ' +\n 'drops. Enemy strength is not read off these levels — see the note below the ' +\n 'table.

';\n\n /* Two of the game's 21 areas are not places and listing them misleads:\n nwh \"Somewhere\" — where global.current_z parks whenever you are NOT in\n a fight, populated by creature.default (\"Nothing\").\n tst \"Test\" — reached only from chss.tst, which has id -1 and is in no\n sector, so nothing in the game can travel there.\n The test for \"is this a place\" is whether you can get to it, and neither\n can be. Everything else is listed whether or not you have been. */\n var MOD_WIKI_NOT_PLACES = { nwh: 1, tst: 1 };\n\n var keys = [];\n for (var k in area) {\n if (area[k] && area[k].pop && !MOD_WIKI_NOT_PLACES[k]) keys.push(k);\n }\n /* Ordered by the middle of the level band, which is the closest thing the\n game has to a difficulty order. */\n keys.sort(function (a, b) {\n var mid = function (z) {\n if (!z.pop.length) return 0;\n var s = 0; z.pop.forEach(function (e) { s += (e.lvlmin + e.lvlmax) / 2; });\n return s / z.pop.length;\n };\n return mid(area[a]) - mid(area[b]);\n });\n\n keys.forEach(function (k) {\n var z = area[k];\n /* An area whose baked bands are not finite can never spawn anything — see\n the note rendered under its table. Derived rather than listed by name, so\n it stays true if the game changes. */\n var broken = !z.popc || z.popc.length !== z.pop.length ||\n z.popc.some(function (band) { return !band || !isFinite(band[1] - band[0]); });\n h += '

' + MOD_WIKI.safe(z.name) +\n ' ' + k + '' +\n (broken ? 'nothing spawns' : '') +\n '' + (z.size === -1 ? 'endless' : z.size + ' to clear') +\n '

';\n if (z.pop.length) {\n /* The spawn share is NOT pop[i].c. z_bake normalises those weights into\n area.popc — a list of [lo,hi] bands that mon_gen rolls a uniform\n random against — and the weights do not have to sum to 1, so they are\n off wherever they don't. The Southern forest's .35/.45/.25 are really\n 33/43/24%. Read the bands the game actually rolls against. */\n var bands = z.popc || [];\n h += '';\n z.pop.forEach(function (e, i) {\n var c = e.crt || {};\n var band = bands[i];\n var share = band ? band[1] - band[0] : NaN;\n h += '' +\n '' +\n '';\n });\n h += '
' + MOD_WIKI.safe(c.name || '?') + 'lv ' + e.lvlmin +\n (e.lvlmax !== e.lvlmin ? '–' + e.lvlmax : '') + '' + (isFinite(share)\n ? Math.round(share * 100) + '% of spawns'\n : '—') + '
';\n if (broken) {\n h += '
Nothing spawns here. The area\\'s entries carry ' +\n 'no spawn weight, so the table the game rolls against comes out as ' +\n 'NaN and no creature can ever match it. A bug in the base game, left ' +\n 'as it is — nothing in the game travels to this area either.
';\n }\n }\n if (z.drop && z.drop.length) {\n var d = z.drop.map(function (x) {\n return MOD_WIKI.safe((x.item && x.item.name) || '?') +\n ' ' + ((x.c || 0) * 100).toFixed(2) + '%';\n }).join(', ');\n h += '
Drops: ' + d + '
';\n }\n h += '
';\n });\n\n var fightsOrig = MOD_FIGHTS.mode === 'original';\n h += '

How hard an enemy is

' +\n '

It depends on the Fights setting, which is ' +\n (fightsOrig ? 'Original' : 'Scaled') + ' now. On Original (the default) ' +\n 'every area the author wrote is exactly as he wrote it: his creatures, his ' +\n 'level ranges, and you out-grow them as you do in his game. On Scaled every ' +\n 'area is sized to you. The three areas past the golem arena and the rank ' +\n 'duels are sized to you either way.

' +\n '

Sizing to you works like this. Enemies are not fitted to a level. Each spawn is solved against your ' +\n 'actual power at the moment it appears, aiming at roughly ' + MOD_ENEMY.kill +\n ' swings to kill it and ' + MOD_ENEMY.die + ' before it kills you, with a ' +\n 'guaranteed margin between the two so a fight is never a coin flip. Variety ' +\n 'comes from the area\\'s level band and each creature\\'s own stat weighting, ' +\n 'not from difficulty scaling.

' +\n '

The reason for that: within the first stage alone you go from 1 STR to ' +\n 'around 265, so anything anchored to a level is either trivial at the end of ' +\n 'a stage or impossible at the start of one.

';\n return h;\n});\n\n/* --- 8. items -------------------------------------------------------------\n The one place where \"list everything\" needs help: 321 of the game's 371\n plain items are the same inventory type, so grouping by type alone leaves a\n single group of 321. Anything past MOD_WIKI_ALPHA_AT gets an alphabetical\n sub-split as well — the oldest index pattern there is, and derived rather\n than invented, which a hand-made taxonomy would not be.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_ALPHA_AT = 60;\n\n/* The game's own inventory tabs (isort): ALL / WPN / EQP / USE / OTHER. Using\n its categories rather than new ones means a group here is a tab there. */\nvar MOD_WIKI_STYPE = { 2: 'Weapons', 3: 'Equipment', 4: 'Usable', 5: 'Other' };\n\nfunction MOD_wikiItemRow(it) {\n var val = it.v !== undefined ? it.v : (it.val !== undefined ? it.val : null);\n return '
' +\n '

' + MOD_WIKI.safe(it.name) +\n (it.rar > 1 ? 'rarity ' + it.rar + '' : '') +\n (val !== null ? '' + MOD_wikiNum(val) + ' coin' : '') +\n (it.amount > 0 ? '×' + it.amount + '' : '') +\n '

' + MOD_wikiDesc(it.desc) + '
';\n}\n\n/* The letter a name files under. Several items are quoted — the master's\n manuals are literally named \"Sword Saint Manual\" — and filing those under a\n quotation mark is no use to anyone looking for S. */\nfunction MOD_wikiLetter(name) {\n var m = String(name || '').replace(/^[^0-9A-Za-z]+/, '');\n return (m.charAt(0) || '?').toUpperCase();\n}\n\nfunction MOD_wikiByLetter(a, b) {\n var x = MOD_wikiLetter(a.name) + String(a.name || '').toLowerCase();\n var y = MOD_wikiLetter(b.name) + String(b.name || '').toLowerCase();\n return x < y ? -1 : (x > y ? 1 : 0);\n}\n\n/* Flat below the threshold, alphabetically sub-grouped above it. Ranges are cut\n from the entries actually present, so they stay even as content is added. */\nfunction MOD_wikiAlpha(entries, render, level) {\n if (entries.length <= MOD_WIKI_ALPHA_AT) return entries.map(render).join('');\n var per = Math.ceil(entries.length / Math.ceil(entries.length / 40));\n var out = '';\n for (var i = 0; i < entries.length; i += per) {\n var chunk = entries.slice(i, i + per);\n var first = MOD_wikiLetter(chunk[0].name);\n var last = MOD_wikiLetter(chunk[chunk.length - 1].name);\n out += MOD_wikiGroup(first === last ? first : first + '–' + last,\n chunk.length, chunk.map(render).join(''), level || 4);\n }\n return out;\n}\n\nMOD_wikiPage('items', 'Items', function () {\n var h = '

Items

' +\n '

Everything the game defines, whether or not you have seen ' +\n 'it. Grouped the way your inventory groups it. Values are the base sell ' +\n 'price where the game gives one.

';\n\n /* item[] first, split by the inventory tab it appears under. */\n var buckets = {};\n for (var k in item) {\n var it = item[k];\n if (!it || !it.name || it.name === 'dummy') continue;\n var lab = MOD_WIKI_STYPE[it.stype] || 'Uncategorised';\n (buckets[lab] = buckets[lab] || []).push(it);\n }\n ['Usable', 'Other', 'Weapons', 'Equipment', 'Uncategorised'].forEach(function (lab) {\n var list = buckets[lab];\n if (!list || !list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(lab, list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n\n /* and the equipment objects, which live in their own globals */\n [['Weapons', wpn], ['Armour', eqp], ['Shields', sld], ['Accessories', acc]]\n .forEach(function (g) {\n var list = [];\n for (var kk in g[1]) {\n if (g[1][kk] && g[1][kk].name && g[1][kk].name !== 'dummy') list.push(g[1][kk]);\n }\n if (!list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(g[0], list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n return h;\n});\n\n/* --- 9. actions ----------------------------------------------------------- */\nMOD_wikiPage('actions', 'Actions', function () {\n var unlock = {};\n MOD_ACTION_UNLOCKS.forEach(function (u) {\n if (u.act) unlock[u.act.id] = (skl[u.skill] ? skl[u.skill].name : u.skill) +\n ' level ' + u.lv;\n });\n if (typeof act.mod_qi !== 'undefined') {\n unlock[act.mod_qi.id] = 'clearing the dojo\\'s Easiest, Easy and Normal fights';\n }\n\n var h = '

Actions

' +\n '

What you can be doing. One at a time, and only where the ' +\n 'action makes sense — unless \"Unrestricted actions\" is ticked in settings, ' +\n 'which lets several run at once anywhere. Both limits are deliberate; the ' +\n 'switch is there because they are not to everyone\\'s taste.

';\n\n var keys = [];\n for (var k in act) if (act[k] && act[k].name && act[k].name !== 'dummy') keys.push(k);\n keys.sort(function (a, b) { return act[a].name < act[b].name ? -1 : 1; });\n\n keys.forEach(function (k) {\n var a = act[k];\n var how = unlock[a.id];\n h += '
' +\n '

' + MOD_WIKI.safe(a.name) +\n (a.have === true ? 'unlocked'\n : 'locked') +\n (how ? 'from ' + MOD_WIKI.safe(how) + '' : '') +\n '

' + MOD_wikiDesc(a.desc) + '
';\n });\n return h;\n});\n\n/* --- 10. the dojo --------------------------------------------------------- */\nMOD_wikiPage('dojo', 'The dojo', function () {\n var h = '

The dojo

' +\n '

The instructor promises a reward every five levels. The base ' +\n 'game stops delivering at 30; the mod carries the same ladder to 110, on the ' +\n 'same terms.

' +\n '

Only the lowest rung you have not claimed is offered, which is how the ' +\n 'first six behave. Rewards are sized against the character experience curve ' +\n '(4×level3 + level2) — a level at 110 costs 5.3 ' +\n 'million, which is why the base game\\'s best pill is worth 0.3% of one there.

' +\n '' +\n '';\n\n MOD_DOJO_RUNGS.forEach(function (r) {\n var pill = item[r.pill];\n var extra = [];\n if (r.realmPill) extra.push('breakthrough pill for realm ' + r.realmPill);\n if (r.manual) extra.push('a choice of master\\'s manual');\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
LevelRewardCoinAlso
' + r.lv + '' + (pill ? MOD_WIKI.safe(pill.name) + ' ×' + r.n\n : '—') + '' + MOD_wikiNum(r.coin) + '' + (extra.join(', ') || '—') + '' + (global.flags[r.flag] ? 'claimed' : '') +\n '
' +\n\n '

Spirit pills

' +\n '

Character experience in a bottle. The four the mod adds continue the ' +\n 'game\\'s own series up to the new curve.

' +\n '' +\n '';\n MOD_PILLS.forEach(function (p) {\n h += '';\n });\n h += '
PillExperience
' + MOD_WIKI.safe(p[2]) + '' +\n p[3].toLocaleString() + '
' +\n\n '

Master\\'s manuals

' +\n '

A third grade of skillbook, offered at levels 50, 75 and 100 as a choice ' +\n 'the same way the first one was. Each is worth +' +\n Math.round(MOD_MANUAL_RATE * 100) + '% experience in its skill, permanently.

' +\n '' +\n '';\n MOD_MANUALS.forEach(function (m) {\n h += '';\n });\n h += '
ManualSkill
' + MOD_WIKI.safe(m[2]) + '' +\n MOD_WIKI.safe(skl[m[0]] ? (skl[m[0]].bname || skl[m[0]].name) : m[0]) +\n '
';\n return h;\n});\n\n/* --- 11. mechanics -------------------------------------------------------- */\nMOD_wikiPage('mechanics', 'Mechanics', function () {\n return '

Mechanics

' +\n\n '

Damage is subtractive, in both directions

' +\n '

A hit does the attacker\\'s strength minus the defender\\'s strength, plus ' +\n 'the weapon, plus one, before criticals. A defender\\'s STR is their ' +\n 'armour — there is no separate armour stat doing that job.

' +\n '

Two consequences worth carrying around:

' +\n '
    ' +\n '
  • Damage does not taper, it stops. Once a defender\\'s STR passes ' +\n 'yours, your hits do not get small; they land for nothing. Progress against ' +\n 'something too strong is not slow, it is zero.
  • ' +\n '
  • A stat ratio tells you nothing. Comparing your HP to an enemy\\'s ' +\n 'strength ignores the subtraction entirely and reads several times off in ' +\n 'either direction.
  • ' +\n '
' +\n '

This is also the reason enemies are never scaled by raising ' +\n 'their strength: it would raise their offence and their armour together, and ' +\n 'the second effect arrives first.

' +\n\n '

Criticals carry the late game

' +\n '

Critical rate reaches about a third at the top of the ladder and a ' +\n 'critical is roughly eight times a normal swing, so around three quarters of ' +\n 'all damage done at that point is critical damage. Anything that raises ' +\n 'critical rate or critical damage is worth more than it looks; Sharp Eye and ' +\n 'War are the two that do.

' +\n\n '

Hit chance

' +\n '

Landing a hit is a contest of agility. It is a ratio rather than a ' +\n 'subtraction, so agility behaves more gently than strength does — falling ' +\n 'behind on it costs you accuracy, not everything.

' +\n\n '

Skill sections and parents

' +\n '

Each of the ' + Object.keys(MOD_SECTIONS).length + ' sections has a parent ' +\n 'skill. A parent earns ' + Math.round(MOD_PARENT.feed * 100) + '% of everything ' +\n 'its children earn and grants no stats of its own. In return, a child below ' +\n 'the parent\\'s level trains ' + Math.round(MOD_PARENT.perLevel * 100) +\n '% faster per level of lag, up to ' + MOD_PARENT.maxGap + ' levels behind. ' +\n 'Bringing one section up makes every skill in it cheaper to catch up.

' +\n\n '

Saving

' +\n '

Three slots, switched from the bottom bar. The game builds its world once ' +\n 'at startup and cannot unload a save, so switching slots or starting a new ' +\n 'game reloads the page. Exporting a save before running anything unusual is ' +\n 'always the safe move.

';\n});\n\n/* --- the document ---------------------------------------------------------\n One string, no fetches, no dependencies: this has to work from file:// as\n readily as from a server.\n ------------------------------------------------------------------------- */\n\nfunction MOD_wikiCss() {\n return [\n ':root{color-scheme:dark}',\n '*{box-sizing:border-box}',\n 'body{margin:0;background:#060a1c;color:#dfe6f5;font:14px/1.6 \"MS Gothic\",' +\n '\"MS Mincho\",monospace;display:flex;min-height:100vh}',\n 'a{color:#7cb0ff}',\n 'nav{width:210px;flex:0 0 210px;background:#081040;border-right:1px solid #46a;' +\n 'position:sticky;top:0;height:100vh;overflow:auto;padding:14px 0}',\n 'nav h2{font-size:13px;color:#9fb4d8;margin:0 0 10px 14px;letter-spacing:1px}',\n 'nav a{display:block;padding:6px 14px;text-decoration:none;color:#cfe0ff;' +\n 'border-left:3px solid transparent}',\n 'nav a:hover{background:#154080}',\n 'nav a.on{background:#123070;border-left-color:#7cb0ff;color:#fff}',\n 'nav .srch{margin:0 10px 12px;width:calc(100% - 20px);background:#050a20;' +\n 'color:#dfe6f5;border:1px solid #46a;padding:5px;font:inherit}',\n 'nav .xall{display:block;margin:12px 10px 0;color:#8296b8;font-size:12px;' +\n 'cursor:pointer;-webkit-user-select:none;user-select:none}',\n 'nav .xall input{vertical-align:-1px;margin-right:4px}',\n 'details.wk-g{border:1px solid #17224a;background:#0a1130;margin:8px 0}',\n 'details.wk-g>summary{cursor:pointer;padding:7px 12px;list-style:none;' +\n 'background:#0d1740}',\n 'details.wk-g>summary:hover{background:#154080}',\n 'details.wk-g>summary::-webkit-details-marker{display:none}',\n // a caret drawn rather than borrowed, so it is the same in every browser\n 'details.wk-g>summary::before{content:\"\\\\25B8\";color:#7cb0ff;' +\n 'display:inline-block;width:1em;transition:transform .12s}',\n 'details.wk-g[open]>summary::before{transform:rotate(90deg)}',\n 'details.wk-g>summary h2,details.wk-g>summary h3{display:inline;margin:0;' +\n 'font-size:15px;color:#fff}',\n '.wk-gi{padding:2px 12px 10px}',\n 'details.wk-g details.wk-g>summary h4{display:inline;margin:0;font-size:13px;' +\n 'color:#cfe0ff;font-weight:normal}',\n 'details.wk-g details.wk-g>summary{padding:5px 10px;background:#0b1435}',\n 'main{flex:1;min-width:0;padding:22px 30px 80px;max-width:1000px}',\n 'h1{font-size:26px;margin:0 0 6px;color:#fff;border-bottom:1px solid #46a;' +\n 'padding-bottom:8px}',\n 'h2{font-size:19px;color:#bcd4ff;margin:30px 0 8px}',\n 'h3{font-size:15px;color:#fff;margin:0 0 4px}',\n 'p.lede{color:#a8bcdc;margin:0 0 18px}',\n '.dim{color:#8296b8;font-weight:normal}',\n '.flav{font-size:12px;font-style:italic}',\n '.note{background:#0b1638;border-left:3px solid #46a;padding:8px 12px;' +\n 'color:#a8bcdc;margin:14px 0}',\n 'code,pre{background:#0b1638;border:1px solid #24356e;padding:1px 5px;' +\n 'color:#9fd0ff}',\n 'pre{padding:10px;overflow-x:auto}',\n 'dl dt{color:#fff;margin-top:14px}',\n 'dl dd{margin:2px 0 0 0;color:#a8bcdc}',\n 'ul{color:#a8bcdc}',\n 'table.wk-tbl{border-collapse:collapse;width:100%;margin:10px 0;' +\n 'display:block;overflow-x:auto}',\n 'table.wk-tbl th{text-align:left;color:#9fb4d8;font-weight:normal;' +\n 'border-bottom:1px solid #46a;padding:5px 9px;white-space:nowrap}',\n 'table.wk-tbl td{border-bottom:1px solid #17224a;padding:5px 9px;' +\n 'vertical-align:top}',\n 'table.wk-tbl tr.done td{color:#cfe0ff}',\n 'table.wk-tbl tr.here{background:#123070}',\n '.num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}',\n '.wk-e{border:1px solid #17224a;background:#081040;padding:9px 12px;' +\n 'margin:7px 0;border-radius:2px}',\n 'tr.wk-e{border:0;background:none}',\n '.wk-e.done{border-color:#2a4a8c}',\n '.wk-e h3{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}',\n '.lv{margin-left:auto;font-size:12px;color:#8296b8;font-weight:normal}',\n '.lv.capped{color:#ffd24a}',\n '.rank{font-size:12px;color:#8296b8;font-weight:normal}',\n '.desc{color:#a8bcdc;font-size:13px}',\n '.proj{color:#ffd24a;font-size:12px;margin-top:3px}',\n '.tag{font-size:11px;border:1px solid #46a;color:#9fb4d8;padding:0 5px;' +\n 'border-radius:2px;font-weight:normal}',\n '.tag.mod{border-color:#7a4ea8;color:#c07bff}',\n '.tag.done{border-color:#2e7d4f;color:#6fdc9a}',\n '.tag.here{border-color:#ffd24a;color:#ffd24a}',\n '.tag.warn{border-color:#a8562e;color:#ff9a5c}',\n 'table.wk-perks{border-collapse:collapse;margin:6px 0 0;width:100%}',\n 'table.wk-perks td{padding:2px 8px 2px 0;font-size:12px;color:#8296b8;' +\n 'border-bottom:1px solid #101a3c}',\n 'table.wk-perks tr.done td{color:#cfe0ff}',\n 'table.wk-perks td.tick{color:#6fdc9a;width:1em;text-align:right}',\n // fixed columns so the level bands line up between one area and the next\n 'table.wk-pop{table-layout:fixed}',\n 'table.wk-pop td:first-child{width:60%}',\n 'table.wk-pop td:nth-child(2){width:20%}',\n '.wk-title .tname{font-weight:bold}',\n '.wk-now{background:#0b1638;border:1px solid #24356e;padding:10px 14px;' +\n 'margin:0 0 18px;display:inline-block;min-width:320px}',\n '.wk-now h3{color:#9fb4d8;font-weight:normal;font-size:13px;' +\n 'letter-spacing:1px;margin-bottom:6px}',\n '.wk-now td{padding:2px 16px 2px 0}',\n '.wk-now td:last-child{color:#fff;text-align:right}',\n '.pg{display:none}.pg.on{display:block}',\n 'footer{color:#5a6c8c;font-size:12px;border-top:1px solid #17224a;' +\n 'margin-top:40px;padding-top:12px}',\n '@media(max-width:760px){body{display:block}nav{width:auto;height:auto;' +\n 'position:static;flex:none}main{padding:16px}}'\n ].join('\\n');\n}\n\n/* Filtering, page switching and the hash. Small enough to inline, and inlining\n it is the point — the document has to stand on its own. */\nfunction MOD_wikiJs() {\n return [\n 'var pages=[].slice.call(document.querySelectorAll(\".pg\"));',\n 'var links=[].slice.call(document.querySelectorAll(\"nav a[data-p]\"));',\n 'function show(id){',\n ' var found=false;',\n ' pages.forEach(function(p){var on=p.id===\"pg-\"+id;if(on)found=true;',\n ' p.className=\"pg\"+(on?\" on\":\"\")});',\n ' if(!found&&pages.length){pages[0].className=\"pg on\";id=pages[0].id.slice(3)}',\n ' links.forEach(function(a){a.className=a.dataset.p===id?\"on\":\"\"});',\n ' window.scrollTo(0,0);return id;}',\n 'links.forEach(function(a){a.addEventListener(\"click\",function(e){',\n ' e.preventDefault();location.hash=a.dataset.p;show(a.dataset.p)})});',\n 'window.addEventListener(\"hashchange\",function(){show(location.hash.slice(1))});',\n 'show(location.hash.slice(1)||\"start\");',\n // Search filters entries, hides groups left with none, and OPENS the groups\n // that still have some — a match hidden inside a collapsed group reads as\n // no match at all, which is worse than not having search.\n 'var box=document.getElementById(\"srch\");',\n 'var xall=document.getElementById(\"xall\");',\n 'var groups=[].slice.call(document.querySelectorAll(\"details.wk-g\"));',\n 'function filter(){',\n ' var q=box.value.trim().toLowerCase();',\n ' pages.forEach(function(p){',\n ' [].slice.call(p.querySelectorAll(\".wk-e\")).forEach(function(e){',\n ' e.style.display=(!q||e.textContent.toLowerCase().indexOf(q)>=0)?\"\":\"none\"})});',\n ' groups.forEach(function(g){',\n ' var any=[].slice.call(g.querySelectorAll(\".wk-e\")).some(function(e){',\n ' return e.style.display!==\"none\"});',\n ' g.style.display=any?\"\":\"none\";',\n ' g.open=xall.checked||(!!q&&any)}); }',\n 'box.addEventListener(\"input\",filter);',\n 'box.addEventListener(\"keydown\",function(e){if(e.key===\"Escape\"){box.value=\"\";filter()}});',\n // Everything is collapsed by default because three of these pages run past\n // 40,000px open. This is the way back to one long document, which is what\n // the browser's own find needs.\n 'xall.addEventListener(\"change\",filter);'\n ].join('\\n');\n}\n\nfunction MOD_wikiBuild() {\n var nav = '', body = '';\n MOD_WIKI_PAGES.forEach(function (pg) {\n nav += '' +\n MOD_WIKI.safe(pg.label) + '';\n var inner;\n /* One page throwing must not take the wiki down with it — the page says so\n and the rest still renders. */\n try { inner = pg.build(); }\n catch (e) {\n inner = '

' + MOD_WIKI.safe(pg.label) + '

This page ' +\n 'could not be built: ' + MOD_WIKI.safe(e.message) + '

';\n console.warn('[mod] wiki page \"' + pg.id + '\" failed: ' + e.message);\n }\n body += '
' + inner + '
';\n });\n\n var stamp = new Date().toLocaleString();\n return '' +\n '' +\n '' + MOD_WIKI.safe(MOD_WIKI.title) + '' +\n '' +\n '' +\n '
' + body +\n '
Generated from the running game at ' + MOD_WIKI.safe(stamp) +\n ' — mod ' + MOD_WIKI.safe(MOD.version) + '. Everything here is read from the ' +\n 'live data, including your own save, so it cannot fall out of step with the ' +\n 'game. The game is truezhangwei\\'s; the mod is not endorsed by the author.' +\n '
' +\n ' inside content closing the block early, so that is the\n one thing scrubbed. */\n safe: function (h) {\n return String(h === undefined || h === null ? '' : h).replace(/<\\/script/gi, '<\\\\/script');\n }\n};\n\n/* Descriptions carry real HTML (coloured spans, the game's separator div), so\n they are passed through rather than escaped — but the separator becomes a\n rule, since a wiki row is not a tooltip. */\nfunction MOD_wikiDesc(d) {\n var s;\n try { s = (typeof d === 'function') ? d() : d; } catch (e) { s = ''; }\n if (!s) return '';\n s = String(s).split('
 
').join('
');\n return MOD_WIKI.safe(s);\n}\n\nfunction MOD_wikiNum(n) {\n if (!isFinite(n)) return '—';\n return Math.round(n).toLocaleString();\n}\n\n/* An effect line at a level the player has not reached yet. The effect\n functions read the skill they are handed, so the level is borrowed and put\n back; anything that throws simply gets no projection. */\nfunction MOD_wikiEffectAt(key, sk, lvl) {\n var eff = MOD_EFFECTS[key];\n if (!eff) return '';\n var was = sk.lvl, out = '';\n try { sk.lvl = lvl; out = eff(sk) || ''; }\n catch (e) { out = ''; }\n finally { sk.lvl = was; }\n return MOD_WIKI.safe(out);\n}\n\n/* --- page builders --------------------------------------------------------\n Each returns the inner HTML of one page. `wk-e` marks a searchable entry and\n `wk-g` a group that hides itself when all its entries are filtered out.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_PAGES = [];\n\nfunction MOD_wikiPage(id, label, build) {\n MOD_WIKI_PAGES.push({ id: id, label: label, build: build });\n}\n\n/* A collapsible group.\n\n Not decoration: with every skill, item and title listed, three of the eleven\n pages ran past 40,000 pixels — a hundred screens of scrolling, which is the\n same as not having the content at all. Collapsed, each of those pages opens\n as an index you can read in one screen and expand where you want.\n\n
rather than a click handler, so it still works with JavaScript off,\n the browser's own find can be made to reach inside it, and the summary is\n keyboard-reachable for free. `level` is the heading level for the summary,\n since a group is sometimes a section of a page and sometimes a subsection. */\nfunction MOD_wikiGroup(heading, count, inner, level) {\n var hn = 'h' + (level || 2);\n return '
<' + hn + '>' + heading +\n (count !== null && count !== undefined\n ? ' (' + count + ')' : '') +\n '
' + inner + '
';\n}\n\n/* --- 1. orientation ------------------------------------------------------- */\nMOD_wikiPage('start', 'Start here', function () {\n var cap = MOD_levelCap();\n var realm = MOD_realm();\n var h = '

proto23

' +\n '

An idle RPG by truezhangwei, plus a mod. You train skills by ' +\n 'doing things, the things you can do open up as the story does, and none of ' +\n 'it is in a hurry.

' +\n\n '

Where you are right now

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Character level' + MOD_wikiNum(you.lvl) + '
Skill level cap' + cap + ' — ' +\n MOD_WIKI.safe(MOD_CAP.name) + '
Cultivation realm' + MOD_WIKI.safe(realm.name) +\n ' (' + realm.n + ' of ' + (MOD_REALMS.length - 1) + ')
Titles held' + MOD_titleCount() + ' — renown ' +\n (skl.rnwn ? skl.rnwn.lvl : 0) + '
Coin' + MOD_wikiNum(you.wealth || 0) + '
' +\n\n '

The four things worth knowing early

' +\n '
' +\n '
Your skill levels are capped by the story, not by grinding.
' +\n '
The cap starts at 10 and ends at 110, and it moves when you reach a new ' +\n 'place — not when you earn enough experience. If a skill has stopped ' +\n 'levelling, you are not doing it wrong; you need to go somewhere. See ' +\n 'Progression.
' +\n\n '
Armour is subtraction, so levels matter more than they look.
' +\n '
A hit does the attacker\\'s STR minus the defender\\'s STR. That means a ' +\n 'small gap in strength is a large gap in damage, and a big enough one takes ' +\n 'your damage to nothing at all rather than merely reducing it. See ' +\n 'Mechanics.
' +\n\n '
Most skills have a parent that pulls them along.
' +\n '
Every section has a parent skill that earns a share of everything its ' +\n 'children do, and any child lagging behind the parent trains faster — up to ' +\n 'twice as fast at ' + MOD_PARENT.maxGap + ' levels behind. A neglected skill is ' +\n 'cheap to bring up later, which is the only reason having this many is ' +\n 'workable.
' +\n\n '
Cultivation is a second ladder, and it is not automatic.
' +\n '
Circulating qi levels a skill; the skill does not raise your realm. ' +\n 'Reaching the level opens a bottleneck, and breaking through it costs a pill ' +\n 'and can fail. See Cultivation.
' +\n '
' +\n\n '

What is the mod and what is the game

' +\n '

The game is index.html and it is entirely truezhangwei\\'s ' +\n 'work. Everything in mod.js is a modification of it and is not ' +\n 'endorsed by the author. Pages here mark added content with ' +\n 'mod where the distinction matters. The full ' +\n 'list of changes is in the changelog, on the bottom bar.

';\n return h;\n});\n\n/* --- 2. progression ------------------------------------------------------- */\nMOD_wikiPage('progress', 'Progression', function () {\n var cap = MOD_levelCap();\n var h = '

Progression

' +\n '

Skill levels are capped by how far the story has gone. Ten ' +\n 'rungs, 10 through 110. The cap applies to every skill at once; Renown is ' +\n 'the single exception, since it is counted rather than trained.

' +\n '' +\n '';\n\n MOD_TIERS.forEach(function (t) {\n var got = null;\n try { got = MOD_tierFlag(t); } catch (e) {}\n var here = (t.cap === cap);\n var need = t.needText ? t.needText\n : (t.flags.length ? t.flags.join(' or ') : 'the start of the game');\n h += '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
CapStageWhat opens it
' + t.cap + '' + MOD_WIKI.safe(t.name) + '' + MOD_WIKI.safe(need) + '' + (here ? 'you are here'\n : got ? 'reached' : '') + '
' +\n\n '

The experience curve

' +\n '

The game\\'s own skill curve is super-exponential — level 109 to 110 ' +\n 'alone costs ' + MOD_wikiNum(MOD_vanillaExpnext(109)) + ' experience, so the top of the ladder ' +\n 'is unreachable by a factor of about ten trillion. But the author only ever ' +\n 'designed skills up to level 15, and below that his curve is fine.

' +\n (MOD_XP.vanillaTo > 0\n ? '

So the mod keeps it. Below level ' + MOD_XP.vanillaTo + ' a skill costs ' +\n 'exactly what the original charges; from there it rises by ×' +\n MOD_XP.ratio.toFixed(4) + ' a level, a ratio solved so the climb to 110 takes ' +\n 'the same time it always did.'\n : '

You have chosen the Mod before 4.2 pacing: a plain geometric curve, ' +\n '×' + MOD_XP.ratio.toFixed(3) + ' a level from level 0, much faster than the ' +\n 'original early on. The Original pacing in settings puts back the ' +\n 'author\\'s own cost below level 10.') +\n ' Character level uses the game\\'s own curve, untouched.

' +\n '' +\n '';\n [1, 5, 9, 10, 15, 25, 50, 75, 90, 109].forEach(function (l) {\n h += '';\n });\n h += '
LevelExperience for the nextOriginal game
' + l +\n '' + MOD_wikiNum(MOD_expnextFor(l)) + '' +\n MOD_wikiNum(MOD_vanillaExpnext(l)) + '
';\n return h;\n});\n\n/* --- 3. skills ------------------------------------------------------------ */\nMOD_wikiPage('skills', 'Skills', function () {\n var cap = MOD_levelCap();\n var bySection = {};\n var keys = [];\n for (var k in skl) {\n var s = skl[k];\n if (!s || typeof s !== 'object' || !s.name) continue;\n keys.push(k);\n }\n keys.forEach(function (k) {\n var s = skl[k];\n var sec = MOD_sectionOf(s);\n (bySection[sec] = bySection[sec] || []).push(k);\n });\n\n var h = '

Skills

' +\n '

' + keys.length + ' skills in ' +\n Object.keys(MOD_SECTIONS).length + ' sections. Every one has perks at ' +\n MOD_LADDER.join(', ') + ', and every one that has milestones grants titles at ' +\n MOD_TITLE_RUNGS.join(', ') + '. Levels and ticks below are yours.

';\n\n Object.keys(MOD_SECTIONS).forEach(function (sec) {\n var list = bySection[sec];\n if (!list || !list.length) return;\n var parentKey = MOD_PARENT_KEY[sec];\n var g = '';\n if (parentKey && skl[parentKey]) {\n g += '

Parent skill: ' +\n MOD_WIKI.safe(skl[parentKey].bname || skl[parentKey].name) +\n ' — earns ' + Math.round(MOD_PARENT.feed * 100) +\n '% of what its children earn, and speeds up any child that lags behind it.

';\n }\n list.sort(function (a, b) { return skl[a].name < skl[b].name ? -1 : 1; });\n list.forEach(function (k) {\n var s = skl[k];\n var lvl = Number(s.lvl) || 0;\n g += '
' +\n '

' + MOD_WIKI.safe(s.bname || s.name) +\n (s.bname ? ' (' + MOD_WIKI.safe(s.name) + ')' : '') +\n '= cap ? ' capped' : '') + '\">lv ' + lvl +\n (lvl >= cap ? ' — at the cap' : '') + '

' +\n '
' + MOD_wikiDesc(s.desc) + '
';\n\n var at110 = MOD_wikiEffectAt(k, s, 110);\n if (at110) g += '
At level 110: ' + at110 + '
';\n\n if (s.mlstn && s.mlstn.length) {\n g += '';\n s.mlstn.forEach(function (m) {\n g += '' +\n '' +\n '' +\n '';\n });\n g += '
' + m.lv + '' + MOD_WIKI.safe(m.p || '—') + '' + (m.g === true ? '✓' : '') + '
';\n }\n g += '
';\n });\n h += MOD_wikiGroup(MOD_WIKI.safe(MOD_SECTIONS[sec]), list.length, g, 2);\n });\n return h;\n});\n\n/* --- 4. cultivation ------------------------------------------------------- */\nMOD_wikiPage('cultivation', 'Cultivation', function () {\n var now = MOD_realm();\n var elig = 0;\n try { elig = MOD_realmEligible().n; } catch (e) {}\n var qic = skl.qic ? skl.qic.lvl : 0;\n\n var h = '

Cultivation

' +\n '

Ten realms above Mortal. Circulating qi levels the Qi ' +\n 'Circulation skill; the skill does not advance the realm. Reaching the level ' +\n 'opens a bottleneck, and breaking through it costs a pill and can fail — the ' +\n 'pill is spent either way.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Realm' + MOD_WIKI.safe(MOD_realmTitle()) + '
Spiritual Root' + MOD_WIKI.safe(MOD_root().name) + '
Qi Circulationlevel ' + qic + '
Insight' + MOD_insight() + '
Bottleneck open' +\n (elig > now.n ? 'yes — ' + MOD_WIKI.safe(MOD_REALMS[now.n + 1].name) +\n ' is within reach' : 'no') + '
Qi Deviation' + (MOD_deviated()\n ? MOD_deviationLeft() + ' in-game minutes left' : 'no') + '
' +\n '

Realm thresholds are the same levels that earn a title rank, so realm N ' +\n 'is the level at which a rank N title arrives. Every realm multiplies all ' +\n 'stats and health, and the multipliers do not stack with each other — the ' +\n 'realm you are in is the one that applies.

' +\n '' +\n '';\n\n MOD_REALMS.forEach(function (r) {\n var pill = r.n >= 1 ? item['mod_bp' + r.n] : null;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmNameQi Circ.StatsHealthPill
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + r.qic + '×' + r.mult + '×' + r.hp + '' + (pill ? MOD_WIKI.safe(pill.name) : '—') +\n '
' +\n\n '

Where the pills come from

' +\n '

Every breakthrough pill has a source in normal play. None of them are ' +\n 'craftable, and none are cheap — a bottleneck you can buy in bulk is not a ' +\n 'bottleneck.

' +\n '' +\n '' +\n '' +\n '' +\n '
RealmsSource
1The dojo instructor, when qi ' +\n 'circulation is first taught
2–5The Herbalist in the village ' +\n '(vendor.pha1), restocked
6–10The dojo\\'s Level Advancement ' +\n 'ladder, and the Pill Tower
' +\n '

Realm 1 is deliberately not sold at the marketplace: ' +\n 'the marketplace is behind a 40%-a-visit chance encounter, and realm 1 opens ' +\n 'long before that is reliable.

' +\n\n '

Breaking through

' +\n '

A bottleneck wants three things, which is what the genre says it wants: ' +\n 'a pill, insight, and enough consolidation to make the ' +\n 'attempt worth making. The pill and the insight are both spent whether the ' +\n 'attempt works or not.

' +\n '

The odds improve the further past the threshold you are, plus your root, ' +\n 'plus time sat in Closed Door Training, and cap short of certain.

' +\n '' +\n '';\n [0, 2, 4, 6, 8].forEach(function (o) {\n var base = Math.min(0.55 + o * 0.05 + MOD_root().odds, 0.95);\n h += '';\n });\n h += '
Levels past the thresholdChanceAfter full seclusion
+' + o + '' +\n Math.round(base * 100) + '%' +\n Math.round(Math.min(base + MOD_CULT.secludeOdds, 0.95) * 100) + '%
' +\n\n '

Insight

' +\n '

Insight is the half of the price that cannot be bought. It comes from ' +\n 'three places, and they are the three the genre names: meditating, fights you ' +\n 'nearly lost, and going somewhere you have not been.

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
SourceRate
Circulate Qi, at a wall' +\n (MOD_CULT.meditateChance * 100).toFixed(1) + '% a second
Closed Door Training' +\n (MOD_CULT.secludeChance * 100).toFixed(1) + '% a second
A kill made under ' +\n Math.round(MOD_CULT.brinkFrac * 100) + '% health — ' +\n (MOD_FIGHTS.mode === 'original'\n ? 'the author\\'s areas rarely come close once you reach them; the three past ' +\n 'the golem arena and the rank duels do, within a few fights, as does an area ' +\n 'you walk into early'\n : 'every fight costs a share of your health, so a few in a row without resting') +\n '' +\n Math.round(MOD_CULT.brinkChance * 100) + '% a kill
Entering an area for the first timealways
' +\n '

The insight a realm asks for is sized to the climb to it: about ' +\n Math.round(MOD_CULT.wallShare * 100) + '% of the Circulate Qi time the climb took, ' +\n 'spent at the wall. Seclusion earns it three times as fast, and full ' +\n 'consolidation takes as long as that on average.

' +\n '' +\n '' +\n '';\n var wkHours = function (sec) {\n return sec >= 7200 ? (sec / 3600).toFixed(sec >= 36000 ? 0 : 1) + ' h' : Math.round(sec / 60) + ' min';\n };\n MOD_REALMS.forEach(function (r) {\n if (!r.n) return;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmClimb (Circulate Qi, 1x)Insight to enterFull seclusionQi Deviation (in-game)
' + MOD_WIKI.safe(r.name) + '' + wkHours(MOD_realmClimbSec(r.n)) + '' + MOD_insightNeed(r.n) + '' + wkHours(MOD_secludeFull(r.n)) + '' + (MOD_devMinutes(r.n) / 1440).toFixed(1) + ' days
' +\n\n '

The nine layers

' +\n '

Each realm is nine layers deep, derived from where Qi Circulation sits ' +\n 'between this realm\\'s threshold and the next one\\'s — so breaking through ' +\n 'puts you at the first layer of the new realm on its own. Layers are worth ' +\n '×' + MOD_CULT.layerMult + ' stats and ×' + MOD_CULT.layerBody +\n ' body each above the first, and the top realm has no rung above it to ' +\n 'measure against, so it stays at its first layer.

' +\n '' +\n '';\n for (var li = 1; li <= MOD_CULT.layers; li++) {\n h += '';\n }\n h += '
LayerCalled
' +\n li + '' + MOD_LAYER_ORD[li] + ' layer, ' +\n MOD_WIKI.safe(MOD_layerStage(li)) + '
' +\n '

At the threshold of the next realm and before breaking ' +\n 'through, you are a half step to it — the genre\\'s own name for the ' +\n 'state this mod calls a bottleneck.

' +\n\n '

Closed Door Training

' +\n '

Seclusion does two things. It consolidates — a full sitting ' +\n '(the table above) is worth +' + Math.round(MOD_CULT.secludeOdds * 100) +\n '% on the attempt — and it shelters: a breakthrough attempted while ' +\n 'you are still sitting in it cannot end in Qi Deviation, only in a lost ' +\n 'pill. It will not start unless you are actually at a wall.

' +\n\n '

Qi Deviation

' +\n '

Failing a breakthrough out in the open leaves your cultivation base ' +\n 'unstable for as long as the seclusion you skipped (the table above, never ' +\n 'under ' + MOD_CULT.devMinutes + ' in-game minutes): stats at ×' +\n MOD_CULT.devStat + ', body at ×' + MOD_CULT.devBody + ', and no second ' +\n 'attempt until it passes. It clears on its own, or at once with a ' +\n 'Qi Settling Pill. It never kills — the death system stays out of it.

' +\n\n '

Heavenly Tribulation

' +\n '

From realm ' + MOD_CULT.tribFrom + ' up, a successful breakthrough is ' +\n 'answered. The cost is a fraction of your maximum health, so it is a gate on ' +\n 'walking in healthy rather than a damage race; the realm bonus you are ' +\n 'standing on is what pays it. Fall to it and the realm does not open.

' +\n '' +\n '';\n MOD_REALMS.forEach(function (r) {\n if (!r.n || r.n < MOD_CULT.tribFrom) return;\n h += '' +\n '' +\n '';\n });\n h += '
RealmBoltsOf your max HP
' + MOD_WIKI.safe(r.name) + '' + MOD_tribulationBolts(r.n) + '' + Math.round(MOD_tribulationCost(r.n) * 100) + '%
' +\n\n '

Spiritual Root

' +\n '

Rolled once, when the dojo finishes teaching you to fight. It is never a ' +\n 'dead end: the worst grade costs cultivation speed, not access, and a ' +\n 'Root Cleansing Pill moves you up a grade.

' +\n '' +\n '';\n var rootTotal = 0;\n MOD_ROOTS.forEach(function (r) { rootTotal += r.weight; });\n MOD_ROOTS.forEach(function (r) {\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
GradeRootChanceCultivationBreakthrough
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + Math.round(r.weight / rootTotal * 100) + '%×' + r.xp + '' + (r.odds >= 0 ? '+' : '') +\n Math.round(r.odds * 100) + '%
';\n return h;\n});\n\n/* --- 5. elemental mastery ------------------------------------------------- */\nMOD_wikiPage('mastery', 'Elemental mastery', function () {\n var h = '

Elemental mastery mod

' +\n '

Six masteries, each with a technique that fires on its own ' +\n 'during a fight. Combat is automatic and there is no ability picker, so a ' +\n 'technique is a chance on each of your swings to become something else — one ' +\n 'that scales with INT and the element rather than with your weapon.

' +\n '

Nothing fires while you are still Mortal: the channels have to be open ' +\n 'first. Chance is ' + (MOD_TECH.perLevel * 100).toFixed(1) + '% per mastery ' +\n 'level plus ' + (MOD_TECH.perRealm * 100).toFixed(1) + '% per realm, capped at ' +\n Math.round(MOD_TECH.cap * 100) + '%; a technique lands for ×' +\n MOD_TECH.power + ' of a normal strike.

' +\n '' +\n '';\n\n var realm = 0;\n try { realm = MOD_realm().n; } catch (e) {}\n MOD_MASTERY.forEach(function (m) {\n var lvl = Number(m.skill.lvl) || 0;\n var ch = realm < 1 ? 0\n : Math.min(lvl * MOD_TECH.perLevel + realm * MOD_TECH.perRealm, MOD_TECH.cap);\n h += '' +\n '' +\n '' +\n '';\n });\n h += '
MasteryTechniqueLevelChance now
' +\n MOD_WIKI.safe(m.skill.name) + '' + MOD_WIKI.safe(m.name) + '' + lvl + '' + (realm < 1 ? 'Mortal'\n : (ch * 100).toFixed(1) + '%') + '
';\n return h;\n});\n\n/* --- 6. titles ------------------------------------------------------------ */\nMOD_wikiPage('titles', 'Titles', function () {\n var perm = MOD_permanentRank();\n var held = MOD_titleCount();\n var total = 0; for (var kk in ttl) if (ttl[kk] && ttl[kk].name) total++;\n\n var h = '

Titles

' +\n '

A title\\'s rank runs 1 to 10 and is derived from the level ' +\n 'that earns it, so a rank is a statement about where in the game you are. ' +\n 'Only the title you are wearing applies — until renown makes the lower ranks ' +\n 'permanent.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '
Titles held' + held + ' of ' + total + '
Renown level' + (skl.rnwn ? skl.rnwn.lvl : 0) +\n ' of ' + MOD_RENOWN_MAX + '
Ranks that apply without wearing' +\n (perm >= 1 ? '1–' + perm : 'none yet') + '
' +\n\n '

Ranks

' +\n '';\n MOD_RANK_AT.forEach(function (lv, i) {\n var st = MOD_rankStyle(i + 1);\n h += '' +\n '';\n });\n h += '
RankEarned from levelColour
' + (i + 1) + '' + lv + '' + '●●●' + '
' +\n\n '

Renown

' +\n '

Renown is not trained. It counts the titles you hold, and each level ' +\n 'multiplies every stat by ' + (1 + MOD_RENOWN_RATE) + ' — compounding, so ' +\n 'level ' + MOD_RENOWN_MAX + ' is ×' +\n (Math.round(MOD_renownMult(MOD_RENOWN_MAX) * 1000) / 1000) + '. At renown N, ' +\n 'every title of rank N or below applies permanently.

' +\n '' +\n '';\n MOD_RENOWN_STEPS.forEach(function (n, i) {\n h += '= n ? 'done' : '') + '\">' +\n '';\n });\n h += '
RenownTitles neededAll statsPassive ranks
' + (i + 1) +\n '' + n + '×' +\n (Math.round(MOD_renownMult(i + 1) * 1000) / 1000) + '1–' + (i + 1) + '
' +\n\n '

Every title

' +\n '

Grouped by the skill that grants it. What is left is either ' +\n 'a ladder of its own — creatures killed, items gathered, the ' +\n 'cultivation realms — or a one-off from the story.

';\n\n var groups = {}, fams = {}, loose = [];\n for (var k in ttl) {\n var t = ttl[k];\n /* Unlike the picker, the wiki KEEPS the author's broken entries and says\n so — same call as area.clg, which is listed and tagged \"nothing spawns\"\n rather than quietly dropped. A reader looking for why a title never\n appears is better served by the row than by its absence. */\n if (!t || t.name === undefined || t.name === null) continue;\n var sk = MOD_TITLE_SKILL[k];\n if (sk && skl[sk]) { (groups[sk] = groups[sk] || []).push(k); continue; }\n var stem = MOD_titleStem(k);\n if (stem) { (fams[stem] = fams[stem] || []).push(k); continue; }\n loose.push(k);\n }\n /* Same rule as the picker: a family of one is not a family. */\n Object.keys(fams).forEach(function (stem) {\n if (fams[stem].length < 2) { loose.push(fams[stem][0]); delete fams[stem]; }\n });\n var order = Object.keys(groups).sort(function (a, b) {\n return skl[a].name < skl[b].name ? -1 : 1;\n });\n\n var row = function (k) {\n var t = ttl[k];\n var st = MOD_rankStyle(t.rar);\n var gen = null;\n for (var i = 0; i < MOD_SKILL_TITLES.length; i++) {\n if (MOD_SKILL_TITLES[i].key === k) { gen = MOD_SKILL_TITLES[i]; break; }\n }\n /* The author left a handful unfinished: ttl.ddcd carries the literal string\n \"null\" for both name and description, and shpt2/shpt3/mone3 are blank.\n None is granted by anything, so none can be earned. */\n var broken = !t.name || t.name === 'null';\n return '
' +\n '' +\n MOD_WIKI.safe(t.name || '(unnamed)') + '' +\n (broken ? 'unfinished' : '') +\n 'rank ' + (t.rar || 1) + '' +\n (gen ? 'lv ' + gen.rung + ' · +' +\n Math.round(gen.xp * 100) + '% ' + MOD_WIKI.safe(skl[gen.skill].name) +\n ' exp' : '') +\n (t.have === true ? 'held' : '') +\n '
' + MOD_wikiDesc(t.desc) + '
';\n };\n\n var byRank = function (a, b) { return (ttl[a].rar || 1) - (ttl[b].rar || 1); };\n order.forEach(function (sk) {\n groups[sk].sort(byRank);\n h += MOD_wikiGroup(MOD_WIKI.safe(skl[sk].bname || skl[sk].name),\n groups[sk].length, groups[sk].map(row).join(''), 3);\n });\n /* The ladders that belong to no skill, by the same derivation the picker\n uses — so the two cannot drift apart. Sorted by the label, since that is\n what the reader scans. */\n var famList = Object.keys(fams)\n .map(function (stem) {\n fams[stem].sort(byRank);\n return { stem: stem,\n label: MOD_titleFamilyLabel(stem, fams[stem].map(function (k) { return ttl[k]; })) };\n })\n .sort(function (a, b) { return a.label < b.label ? -1 : a.label > b.label ? 1 : 0; });\n if (famList.length) {\n /* Nested one level down rather than sitting beside the skills. Eleven more\n top-level summaries cost ~530px on a page that is already the tallest in\n the wiki, and these are a different KIND of thing from a skill anyway. */\n var famInner = '', famTotal = 0;\n famList.forEach(function (f) {\n famTotal += fams[f.stem].length;\n famInner += MOD_wikiGroup(MOD_WIKI.safe(f.label), fams[f.stem].length,\n fams[f.stem].map(row).join(''), 4);\n });\n h += MOD_wikiGroup('Ladders of their own', famTotal, famInner, 3);\n }\n if (loose.length) {\n loose.sort(byRank);\n /* The story titles are the one group with no skill to divide them, and\n there are enough of them to need dividing. Rank rather than the alphabet:\n it is the axis they are already sorted on, and the one a reader cares\n about — a rank is a statement about where in the game a title comes from. */\n var inner;\n if (loose.length <= MOD_WIKI_ALPHA_AT) {\n inner = loose.map(row).join('');\n } else {\n var byRankNo = {};\n loose.forEach(function (k) {\n var r = Math.min(Math.max(Number(ttl[k].rar) || 1, 1), 10);\n (byRankNo[r] = byRankNo[r] || []).push(k);\n });\n inner = '';\n Object.keys(byRankNo).sort(function (a, b) { return a - b; }).forEach(function (r) {\n var st = MOD_rankStyle(r);\n inner += MOD_wikiGroup('Rank ' + r + '', byRankNo[r].length,\n byRankNo[r].map(row).join(''), 4);\n });\n }\n h += MOD_wikiGroup('From the story', loose.length, inner, 3);\n }\n return h;\n});\n\n/* --- 7. areas ------------------------------------------------------------- */\nMOD_wikiPage('areas', 'Areas & enemies', function () {\n var h = '

Areas & enemies

' +\n '

Every area, what lives in it and at what levels, and what it ' +\n 'drops. Enemy strength is not read off these levels — see the note below the ' +\n 'table.

';\n\n /* Two of the game's 21 areas are not places and listing them misleads:\n nwh \"Somewhere\" — where global.current_z parks whenever you are NOT in\n a fight, populated by creature.default (\"Nothing\").\n tst \"Test\" — reached only from chss.tst, which has id -1 and is in no\n sector, so nothing in the game can travel there.\n The test for \"is this a place\" is whether you can get to it, and neither\n can be. Everything else is listed whether or not you have been. */\n var MOD_WIKI_NOT_PLACES = { nwh: 1, tst: 1 };\n\n var keys = [];\n for (var k in area) {\n if (area[k] && area[k].pop && !MOD_WIKI_NOT_PLACES[k]) keys.push(k);\n }\n /* Ordered by the middle of the level band, which is the closest thing the\n game has to a difficulty order. */\n keys.sort(function (a, b) {\n var mid = function (z) {\n if (!z.pop.length) return 0;\n var s = 0; z.pop.forEach(function (e) { s += (e.lvlmin + e.lvlmax) / 2; });\n return s / z.pop.length;\n };\n return mid(area[a]) - mid(area[b]);\n });\n\n keys.forEach(function (k) {\n var z = area[k];\n /* An area whose baked bands are not finite can never spawn anything — see\n the note rendered under its table. Derived rather than listed by name, so\n it stays true if the game changes. */\n var broken = !z.popc || z.popc.length !== z.pop.length ||\n z.popc.some(function (band) { return !band || !isFinite(band[1] - band[0]); });\n h += '

' + MOD_WIKI.safe(z.name) +\n ' ' + k + '' +\n (broken ? 'nothing spawns' : '') +\n '' + (z.size === -1 ? 'endless' : z.size + ' to clear') +\n '

';\n if (z.pop.length) {\n /* The spawn share is NOT pop[i].c. z_bake normalises those weights into\n area.popc — a list of [lo,hi] bands that mon_gen rolls a uniform\n random against — and the weights do not have to sum to 1, so they are\n off wherever they don't. The Southern forest's .35/.45/.25 are really\n 33/43/24%. Read the bands the game actually rolls against. */\n var bands = z.popc || [];\n h += '';\n z.pop.forEach(function (e, i) {\n var c = e.crt || {};\n var band = bands[i];\n var share = band ? band[1] - band[0] : NaN;\n h += '' +\n '' +\n '';\n });\n h += '
' + MOD_WIKI.safe(c.name || '?') + 'lv ' + e.lvlmin +\n (e.lvlmax !== e.lvlmin ? '–' + e.lvlmax : '') + '' + (isFinite(share)\n ? Math.round(share * 100) + '% of spawns'\n : '—') + '
';\n if (broken) {\n h += '
Nothing spawns here. The area\\'s entries carry ' +\n 'no spawn weight, so the table the game rolls against comes out as ' +\n 'NaN and no creature can ever match it. A bug in the base game, left ' +\n 'as it is — nothing in the game travels to this area either.
';\n }\n }\n if (z.drop && z.drop.length) {\n var d = z.drop.map(function (x) {\n return MOD_WIKI.safe((x.item && x.item.name) || '?') +\n ' ' + ((x.c || 0) * 100).toFixed(2) + '%';\n }).join(', ');\n h += '
Drops: ' + d + '
';\n }\n h += '
';\n });\n\n var fightsOrig = MOD_FIGHTS.mode === 'original';\n h += '

How hard an enemy is

' +\n '

It depends on the Fights setting, which is ' +\n (fightsOrig ? 'Original' : 'Scaled') + ' now. On Original (the default) ' +\n 'every area the author wrote is exactly as he wrote it: his creatures, his ' +\n 'level ranges, and you out-grow them as you do in his game. On Scaled every ' +\n 'area is sized to you. The three areas past the golem arena and the rank ' +\n 'duels are sized to you either way.

' +\n '

Sizing to you works like this. Enemies are not fitted to a level. Each spawn is solved against your ' +\n 'actual power at the moment it appears, aiming at roughly ' + MOD_ENEMY.kill +\n ' swings to kill it and ' + MOD_ENEMY.die + ' before it kills you, with a ' +\n 'guaranteed margin between the two so a fight is never a coin flip. Variety ' +\n 'comes from the area\\'s level band and each creature\\'s own stat weighting, ' +\n 'not from difficulty scaling.

' +\n '

The reason for that: within the first stage alone you go from 1 STR to ' +\n 'around 265, so anything anchored to a level is either trivial at the end of ' +\n 'a stage or impossible at the start of one.

';\n return h;\n});\n\n/* --- 8. items -------------------------------------------------------------\n The one place where \"list everything\" needs help: 321 of the game's 371\n plain items are the same inventory type, so grouping by type alone leaves a\n single group of 321. Anything past MOD_WIKI_ALPHA_AT gets an alphabetical\n sub-split as well — the oldest index pattern there is, and derived rather\n than invented, which a hand-made taxonomy would not be.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_ALPHA_AT = 60;\n\n/* The game's own inventory tabs (isort): ALL / WPN / EQP / USE / OTHER. Using\n its categories rather than new ones means a group here is a tab there. */\nvar MOD_WIKI_STYPE = { 2: 'Weapons', 3: 'Equipment', 4: 'Usable', 5: 'Other' };\n\nfunction MOD_wikiItemRow(it) {\n var val = it.v !== undefined ? it.v : (it.val !== undefined ? it.val : null);\n return '
' +\n '

' + MOD_WIKI.safe(it.name) +\n (it.rar > 1 ? 'rarity ' + it.rar + '' : '') +\n (val !== null ? '' + MOD_wikiNum(val) + ' coin' : '') +\n (it.amount > 0 ? '×' + it.amount + '' : '') +\n '

' + MOD_wikiDesc(it.desc) + '
';\n}\n\n/* The letter a name files under. Several items are quoted — the master's\n manuals are literally named \"Sword Saint Manual\" — and filing those under a\n quotation mark is no use to anyone looking for S. */\nfunction MOD_wikiLetter(name) {\n var m = String(name || '').replace(/^[^0-9A-Za-z]+/, '');\n return (m.charAt(0) || '?').toUpperCase();\n}\n\nfunction MOD_wikiByLetter(a, b) {\n var x = MOD_wikiLetter(a.name) + String(a.name || '').toLowerCase();\n var y = MOD_wikiLetter(b.name) + String(b.name || '').toLowerCase();\n return x < y ? -1 : (x > y ? 1 : 0);\n}\n\n/* Flat below the threshold, alphabetically sub-grouped above it. Ranges are cut\n from the entries actually present, so they stay even as content is added. */\nfunction MOD_wikiAlpha(entries, render, level) {\n if (entries.length <= MOD_WIKI_ALPHA_AT) return entries.map(render).join('');\n var per = Math.ceil(entries.length / Math.ceil(entries.length / 40));\n var out = '';\n for (var i = 0; i < entries.length; i += per) {\n var chunk = entries.slice(i, i + per);\n var first = MOD_wikiLetter(chunk[0].name);\n var last = MOD_wikiLetter(chunk[chunk.length - 1].name);\n out += MOD_wikiGroup(first === last ? first : first + '–' + last,\n chunk.length, chunk.map(render).join(''), level || 4);\n }\n return out;\n}\n\nMOD_wikiPage('items', 'Items', function () {\n var h = '

Items

' +\n '

Everything the game defines, whether or not you have seen ' +\n 'it. Grouped the way your inventory groups it. Values are the base sell ' +\n 'price where the game gives one.

';\n\n /* item[] first, split by the inventory tab it appears under. */\n var buckets = {};\n for (var k in item) {\n var it = item[k];\n if (!it || !it.name || it.name === 'dummy') continue;\n var lab = MOD_WIKI_STYPE[it.stype] || 'Uncategorised';\n (buckets[lab] = buckets[lab] || []).push(it);\n }\n ['Usable', 'Other', 'Weapons', 'Equipment', 'Uncategorised'].forEach(function (lab) {\n var list = buckets[lab];\n if (!list || !list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(lab, list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n\n /* and the equipment objects, which live in their own globals */\n [['Weapons', wpn], ['Armour', eqp], ['Shields', sld], ['Accessories', acc]]\n .forEach(function (g) {\n var list = [];\n for (var kk in g[1]) {\n if (g[1][kk] && g[1][kk].name && g[1][kk].name !== 'dummy') list.push(g[1][kk]);\n }\n if (!list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(g[0], list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n return h;\n});\n\n/* --- 9. actions ----------------------------------------------------------- */\nMOD_wikiPage('actions', 'Actions', function () {\n var unlock = {};\n MOD_ACTION_UNLOCKS.forEach(function (u) {\n if (u.act) unlock[u.act.id] = (skl[u.skill] ? skl[u.skill].name : u.skill) +\n ' level ' + u.lv;\n });\n if (typeof act.mod_qi !== 'undefined') {\n unlock[act.mod_qi.id] = 'clearing the dojo\\'s Easiest, Easy and Normal fights';\n }\n\n var h = '

Actions

' +\n '

What you can be doing. One at a time, and only where the ' +\n 'action makes sense — unless \"Unrestricted actions\" is ticked in settings, ' +\n 'which lets several run at once anywhere. Both limits are deliberate; the ' +\n 'switch is there because they are not to everyone\\'s taste.

';\n\n var keys = [];\n for (var k in act) if (act[k] && act[k].name && act[k].name !== 'dummy') keys.push(k);\n keys.sort(function (a, b) { return act[a].name < act[b].name ? -1 : 1; });\n\n keys.forEach(function (k) {\n var a = act[k];\n var how = unlock[a.id];\n h += '
' +\n '

' + MOD_WIKI.safe(a.name) +\n (a.have === true ? 'unlocked'\n : 'locked') +\n (how ? 'from ' + MOD_WIKI.safe(how) + '' : '') +\n '

' + MOD_wikiDesc(a.desc) + '
';\n });\n return h;\n});\n\n/* --- 10. the dojo --------------------------------------------------------- */\nMOD_wikiPage('dojo', 'The dojo', function () {\n var h = '

The dojo

' +\n '

The instructor promises a reward every five levels. The base ' +\n 'game stops delivering at 30; the mod carries the same ladder to 110, on the ' +\n 'same terms.

' +\n '

Only the lowest rung you have not claimed is offered, which is how the ' +\n 'first six behave. Rewards are sized against the character experience curve ' +\n '(4×level3 + level2) — a level at 110 costs 5.3 ' +\n 'million, which is why the base game\\'s best pill is worth 0.3% of one there.

' +\n '' +\n '';\n\n MOD_DOJO_RUNGS.forEach(function (r) {\n var pill = item[r.pill];\n var extra = [];\n if (r.realmPill) extra.push('breakthrough pill for realm ' + r.realmPill);\n if (r.manual) extra.push('a choice of master\\'s manual');\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
LevelRewardCoinAlso
' + r.lv + '' + (pill ? MOD_WIKI.safe(pill.name) + ' ×' + r.n\n : '—') + '' + MOD_wikiNum(r.coin) + '' + (extra.join(', ') || '—') + '' + (global.flags[r.flag] ? 'claimed' : '') +\n '
' +\n\n '

Spirit pills

' +\n '

Character experience in a bottle. The four the mod adds continue the ' +\n 'game\\'s own series up to the new curve.

' +\n '' +\n '';\n MOD_PILLS.forEach(function (p) {\n h += '';\n });\n h += '
PillExperience
' + MOD_WIKI.safe(p[2]) + '' +\n p[3].toLocaleString() + '
' +\n\n '

Master\\'s manuals

' +\n '

A third grade of skillbook, offered at levels 50, 75 and 100 as a choice ' +\n 'the same way the first one was. Each is worth +' +\n Math.round(MOD_MANUAL_RATE * 100) + '% experience in its skill, permanently.

' +\n '' +\n '';\n MOD_MANUALS.forEach(function (m) {\n h += '';\n });\n h += '
ManualSkill
' + MOD_WIKI.safe(m[2]) + '' +\n MOD_WIKI.safe(skl[m[0]] ? (skl[m[0]].bname || skl[m[0]].name) : m[0]) +\n '
';\n return h;\n});\n\n/* --- 11. mechanics -------------------------------------------------------- */\nMOD_wikiPage('mechanics', 'Mechanics', function () {\n return '

Mechanics

' +\n\n '

Damage is subtractive, in both directions

' +\n '

A hit does the attacker\\'s strength minus the defender\\'s strength, plus ' +\n 'the weapon, plus one, before criticals. A defender\\'s STR is their ' +\n 'armour — there is no separate armour stat doing that job.

' +\n '

Two consequences worth carrying around:

' +\n '
    ' +\n '
  • Damage does not taper, it stops. Once a defender\\'s STR passes ' +\n 'yours, your hits do not get small; they land for nothing. Progress against ' +\n 'something too strong is not slow, it is zero.
  • ' +\n '
  • A stat ratio tells you nothing. Comparing your HP to an enemy\\'s ' +\n 'strength ignores the subtraction entirely and reads several times off in ' +\n 'either direction.
  • ' +\n '
' +\n '

This is also the reason enemies are never scaled by raising ' +\n 'their strength: it would raise their offence and their armour together, and ' +\n 'the second effect arrives first.

' +\n\n '

Criticals carry the late game

' +\n '

Critical rate reaches about a third at the top of the ladder and a ' +\n 'critical is roughly eight times a normal swing, so around three quarters of ' +\n 'all damage done at that point is critical damage. Anything that raises ' +\n 'critical rate or critical damage is worth more than it looks; Sharp Eye and ' +\n 'War are the two that do.

' +\n\n '

Hit chance

' +\n '

Landing a hit is a contest of agility. It is a ratio rather than a ' +\n 'subtraction, so agility behaves more gently than strength does — falling ' +\n 'behind on it costs you accuracy, not everything.

' +\n\n '

Skill sections and parents

' +\n '

Each of the ' + Object.keys(MOD_SECTIONS).length + ' sections has a parent ' +\n 'skill. A parent earns ' + Math.round(MOD_PARENT.feed * 100) + '% of everything ' +\n 'its children earn and grants no stats of its own. In return, a child below ' +\n 'the parent\\'s level trains ' + Math.round(MOD_PARENT.perLevel * 100) +\n '% faster per level of lag, up to ' + MOD_PARENT.maxGap + ' levels behind. ' +\n 'Bringing one section up makes every skill in it cheaper to catch up.

' +\n\n '

Saving

' +\n '

Three slots, switched from the bottom bar. The game builds its world once ' +\n 'at startup and cannot unload a save, so switching slots or starting a new ' +\n 'game reloads the page. Exporting a save before running anything unusual is ' +\n 'always the safe move.

';\n});\n\n/* --- the document ---------------------------------------------------------\n One string, no fetches, no dependencies: this has to work from file:// as\n readily as from a server.\n ------------------------------------------------------------------------- */\n\nfunction MOD_wikiCss() {\n return [\n ':root{color-scheme:dark}',\n '*{box-sizing:border-box}',\n /* Menlo and Consolas before the generic: with MS Gothic missing (any Mac)\n \"monospace\" is Courier, which Safari sets wider than Chrome does, and\n table cells wrapped on one and not the other. */\n 'body{margin:0;background:#060a1c;color:#dfe6f5;font:14px/1.6 \"MS Gothic\",' +\n '\"MS Mincho\",Menlo,Consolas,\"DejaVu Sans Mono\",monospace;display:flex;min-height:100vh}',\n 'a{color:#7cb0ff}',\n 'nav{width:210px;flex:0 0 210px;background:#081040;border-right:1px solid #46a;' +\n 'position:sticky;top:0;height:100vh;overflow:auto;padding:14px 0}',\n 'nav h2{font-size:13px;color:#9fb4d8;margin:0 0 10px 14px;letter-spacing:1px}',\n 'nav a{display:block;padding:6px 14px;text-decoration:none;color:#cfe0ff;' +\n 'border-left:3px solid transparent}',\n 'nav a:hover{background:#154080}',\n 'nav a.on{background:#123070;border-left-color:#7cb0ff;color:#fff}',\n 'nav .srch{margin:0 10px 12px;width:calc(100% - 20px);background:#050a20;' +\n 'color:#dfe6f5;border:1px solid #46a;padding:5px;font:inherit}',\n 'nav .xall{display:block;margin:12px 10px 0;color:#8296b8;font-size:12px;' +\n 'cursor:pointer;-webkit-user-select:none;user-select:none}',\n 'nav .xall input{vertical-align:-1px;margin-right:4px}',\n 'details.wk-g{border:1px solid #17224a;background:#0a1130;margin:8px 0}',\n 'details.wk-g>summary{cursor:pointer;padding:7px 12px;list-style:none;' +\n 'background:#0d1740}',\n 'details.wk-g>summary:hover{background:#154080}',\n 'details.wk-g>summary::-webkit-details-marker{display:none}',\n // a caret drawn rather than borrowed, so it is the same in every browser\n 'details.wk-g>summary::before{content:\"\\\\25B8\";color:#7cb0ff;' +\n 'display:inline-block;width:1em;transition:transform .12s}',\n 'details.wk-g[open]>summary::before{transform:rotate(90deg)}',\n 'details.wk-g>summary h2,details.wk-g>summary h3{display:inline;margin:0;' +\n 'font-size:15px;color:#fff}',\n '.wk-gi{padding:2px 12px 10px}',\n 'details.wk-g details.wk-g>summary h4{display:inline;margin:0;font-size:13px;' +\n 'color:#cfe0ff;font-weight:normal}',\n 'details.wk-g details.wk-g>summary{padding:5px 10px;background:#0b1435}',\n 'main{flex:1;min-width:0;padding:22px 30px 80px;max-width:1000px}',\n 'h1{font-size:26px;margin:0 0 6px;color:#fff;border-bottom:1px solid #46a;' +\n 'padding-bottom:8px}',\n 'h2{font-size:19px;color:#bcd4ff;margin:30px 0 8px}',\n 'h3{font-size:15px;color:#fff;margin:0 0 4px}',\n 'p.lede{color:#a8bcdc;margin:0 0 18px}',\n '.dim{color:#8296b8;font-weight:normal}',\n '.flav{font-size:12px;font-style:italic}',\n '.note{background:#0b1638;border-left:3px solid #46a;padding:8px 12px;' +\n 'color:#a8bcdc;margin:14px 0}',\n 'code,pre{background:#0b1638;border:1px solid #24356e;padding:1px 5px;' +\n 'color:#9fd0ff}',\n 'pre{padding:10px;overflow-x:auto}',\n 'dl dt{color:#fff;margin-top:14px}',\n 'dl dd{margin:2px 0 0 0;color:#a8bcdc}',\n 'ul{color:#a8bcdc}',\n 'table.wk-tbl{border-collapse:collapse;width:100%;margin:10px 0;' +\n 'display:block;overflow-x:auto}',\n 'table.wk-tbl th{text-align:left;color:#9fb4d8;font-weight:normal;' +\n 'border-bottom:1px solid #46a;padding:5px 9px;white-space:nowrap}',\n 'table.wk-tbl td{border-bottom:1px solid #17224a;padding:5px 9px;' +\n 'vertical-align:top}',\n 'table.wk-tbl tr.done td{color:#cfe0ff}',\n 'table.wk-tbl tr.here{background:#123070}',\n '.num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}',\n '.wk-e{border:1px solid #17224a;background:#081040;padding:9px 12px;' +\n 'margin:7px 0;border-radius:2px}',\n 'tr.wk-e{border:0;background:none}',\n '.wk-e.done{border-color:#2a4a8c}',\n '.wk-e h3{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}',\n '.lv{margin-left:auto;font-size:12px;color:#8296b8;font-weight:normal}',\n '.lv.capped{color:#ffd24a}',\n '.rank{font-size:12px;color:#8296b8;font-weight:normal}',\n '.desc{color:#a8bcdc;font-size:13px}',\n '.proj{color:#ffd24a;font-size:12px;margin-top:3px}',\n '.tag{font-size:11px;border:1px solid #46a;color:#9fb4d8;padding:0 5px;' +\n 'border-radius:2px;font-weight:normal}',\n '.tag.mod{border-color:#7a4ea8;color:#c07bff}',\n '.tag.done{border-color:#2e7d4f;color:#6fdc9a}',\n '.tag.here{border-color:#ffd24a;color:#ffd24a}',\n '.tag.warn{border-color:#a8562e;color:#ff9a5c}',\n 'table.wk-perks{border-collapse:collapse;margin:6px 0 0;width:100%}',\n 'table.wk-perks td{padding:2px 8px 2px 0;font-size:12px;color:#8296b8;' +\n 'border-bottom:1px solid #101a3c}',\n 'table.wk-perks tr.done td{color:#cfe0ff}',\n 'table.wk-perks td.tick{color:#6fdc9a;width:1em;text-align:right}',\n // fixed columns so the level bands line up between one area and the next\n 'table.wk-pop{table-layout:fixed}',\n 'table.wk-pop td:first-child{width:60%}',\n 'table.wk-pop td:nth-child(2){width:20%}',\n '.wk-title .tname{font-weight:bold}',\n '.wk-now{background:#0b1638;border:1px solid #24356e;padding:10px 14px;' +\n 'margin:0 0 18px;display:inline-block;min-width:320px}',\n '.wk-now h3{color:#9fb4d8;font-weight:normal;font-size:13px;' +\n 'letter-spacing:1px;margin-bottom:6px}',\n '.wk-now td{padding:2px 16px 2px 0}',\n '.wk-now td:last-child{color:#fff;text-align:right}',\n '.pg{display:none}.pg.on{display:block}',\n 'footer{color:#5a6c8c;font-size:12px;border-top:1px solid #17224a;' +\n 'margin-top:40px;padding-top:12px}',\n '@media(max-width:760px){body{display:block}nav{width:auto;height:auto;' +\n 'position:static;flex:none}main{padding:16px}}'\n ].join('\\n');\n}\n\n/* Filtering, page switching and the hash. Small enough to inline, and inlining\n it is the point — the document has to stand on its own. */\nfunction MOD_wikiJs() {\n return [\n 'var pages=[].slice.call(document.querySelectorAll(\".pg\"));',\n 'var links=[].slice.call(document.querySelectorAll(\"nav a[data-p]\"));',\n 'function show(id){',\n ' var found=false;',\n ' pages.forEach(function(p){var on=p.id===\"pg-\"+id;if(on)found=true;',\n ' p.className=\"pg\"+(on?\" on\":\"\")});',\n ' if(!found&&pages.length){pages[0].className=\"pg on\";id=pages[0].id.slice(3)}',\n ' links.forEach(function(a){a.className=a.dataset.p===id?\"on\":\"\"});',\n ' window.scrollTo(0,0);return id;}',\n 'links.forEach(function(a){a.addEventListener(\"click\",function(e){',\n ' e.preventDefault();location.hash=a.dataset.p;show(a.dataset.p)})});',\n 'window.addEventListener(\"hashchange\",function(){show(location.hash.slice(1))});',\n 'show(location.hash.slice(1)||\"start\");',\n // Search filters entries, hides groups left with none, and OPENS the groups\n // that still have some — a match hidden inside a collapsed group reads as\n // no match at all, which is worse than not having search.\n 'var box=document.getElementById(\"srch\");',\n 'var xall=document.getElementById(\"xall\");',\n 'var groups=[].slice.call(document.querySelectorAll(\"details.wk-g\"));',\n 'function filter(){',\n ' var q=box.value.trim().toLowerCase();',\n ' pages.forEach(function(p){',\n ' [].slice.call(p.querySelectorAll(\".wk-e\")).forEach(function(e){',\n ' e.style.display=(!q||e.textContent.toLowerCase().indexOf(q)>=0)?\"\":\"none\"})});',\n ' groups.forEach(function(g){',\n ' var any=[].slice.call(g.querySelectorAll(\".wk-e\")).some(function(e){',\n ' return e.style.display!==\"none\"});',\n ' g.style.display=any?\"\":\"none\";',\n ' g.open=xall.checked||(!!q&&any)}); }',\n 'box.addEventListener(\"input\",filter);',\n 'box.addEventListener(\"keydown\",function(e){if(e.key===\"Escape\"){box.value=\"\";filter()}});',\n // Everything is collapsed by default because three of these pages run past\n // 40,000px open. This is the way back to one long document, which is what\n // the browser's own find needs.\n 'xall.addEventListener(\"change\",filter);'\n ].join('\\n');\n}\n\nfunction MOD_wikiBuild() {\n var nav = '', body = '';\n MOD_WIKI_PAGES.forEach(function (pg) {\n nav += '' +\n MOD_WIKI.safe(pg.label) + '';\n var inner;\n /* One page throwing must not take the wiki down with it — the page says so\n and the rest still renders. */\n try { inner = pg.build(); }\n catch (e) {\n inner = '

' + MOD_WIKI.safe(pg.label) + '

This page ' +\n 'could not be built: ' + MOD_WIKI.safe(e.message) + '

';\n console.warn('[mod] wiki page \"' + pg.id + '\" failed: ' + e.message);\n }\n body += '
' + inner + '
';\n });\n\n var stamp = new Date().toLocaleString();\n return '' +\n '' +\n '' + MOD_WIKI.safe(MOD_WIKI.title) + '' +\n '' +\n '' +\n '
' + body +\n '
Generated from the running game at ' + MOD_WIKI.safe(stamp) +\n ' — mod ' + MOD_WIKI.safe(MOD.version) + '. Everything here is read from the ' +\n 'live data, including your own save, so it cannot fall out of step with the ' +\n 'game. The game is truezhangwei\\'s; the mod is not endorsed by the author.' +\n '
' +\n ' inside content closing the block early, so that is the\n one thing scrubbed. */\n safe: function (h) {\n return String(h === undefined || h === null ? '' : h).replace(/<\\/script/gi, '<\\\\/script');\n }\n};\n\n/* Descriptions carry real HTML (coloured spans, the game's separator div), so\n they are passed through rather than escaped — but the separator becomes a\n rule, since a wiki row is not a tooltip. */\nfunction MOD_wikiDesc(d) {\n var s;\n try { s = (typeof d === 'function') ? d() : d; } catch (e) { s = ''; }\n if (!s) return '';\n s = String(s).split('
 
').join('
');\n return MOD_WIKI.safe(s);\n}\n\nfunction MOD_wikiNum(n) {\n if (!isFinite(n)) return '—';\n return Math.round(n).toLocaleString();\n}\n\n/* An effect line at a level the player has not reached yet. The effect\n functions read the skill they are handed, so the level is borrowed and put\n back; anything that throws simply gets no projection. */\nfunction MOD_wikiEffectAt(key, sk, lvl) {\n var eff = MOD_EFFECTS[key];\n if (!eff) return '';\n var was = sk.lvl, out = '';\n try { sk.lvl = lvl; out = eff(sk) || ''; }\n catch (e) { out = ''; }\n finally { sk.lvl = was; }\n return MOD_WIKI.safe(out);\n}\n\n/* --- page builders --------------------------------------------------------\n Each returns the inner HTML of one page. `wk-e` marks a searchable entry and\n `wk-g` a group that hides itself when all its entries are filtered out.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_PAGES = [];\n\nfunction MOD_wikiPage(id, label, build) {\n MOD_WIKI_PAGES.push({ id: id, label: label, build: build });\n}\n\n/* A collapsible group.\n\n Not decoration: with every skill, item and title listed, three of the eleven\n pages ran past 40,000 pixels — a hundred screens of scrolling, which is the\n same as not having the content at all. Collapsed, each of those pages opens\n as an index you can read in one screen and expand where you want.\n\n
rather than a click handler, so it still works with JavaScript off,\n the browser's own find can be made to reach inside it, and the summary is\n keyboard-reachable for free. `level` is the heading level for the summary,\n since a group is sometimes a section of a page and sometimes a subsection. */\nfunction MOD_wikiGroup(heading, count, inner, level) {\n var hn = 'h' + (level || 2);\n return '
<' + hn + '>' + heading +\n (count !== null && count !== undefined\n ? ' (' + count + ')' : '') +\n '
' + inner + '
';\n}\n\n/* --- 1. orientation ------------------------------------------------------- */\nMOD_wikiPage('start', 'Start here', function () {\n var cap = MOD_levelCap();\n var realm = MOD_realm();\n var h = '

proto23

' +\n '

An idle RPG by truezhangwei, plus a mod. You train skills by ' +\n 'doing things, the things you can do open up as the story does, and none of ' +\n 'it is in a hurry.

' +\n\n '

Where you are right now

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Character level' + MOD_wikiNum(you.lvl) + '
Skill level cap' + cap + ' — ' +\n MOD_WIKI.safe(MOD_CAP.name) + '
Cultivation realm' + MOD_WIKI.safe(realm.name) +\n ' (' + realm.n + ' of ' + (MOD_REALMS.length - 1) + ')
Titles held' + MOD_titleCount() + ' — renown ' +\n (skl.rnwn ? skl.rnwn.lvl : 0) + '
Coin' + MOD_wikiNum(you.wealth || 0) + '
' +\n\n '

The four things worth knowing early

' +\n '
' +\n '
Your skill levels are capped by the story, not by grinding.
' +\n '
The cap starts at 10 and ends at 110, and it moves when you reach a new ' +\n 'place — not when you earn enough experience. If a skill has stopped ' +\n 'levelling, you are not doing it wrong; you need to go somewhere. See ' +\n 'Progression.
' +\n\n '
Armour is subtraction, so levels matter more than they look.
' +\n '
A hit does the attacker\\'s STR minus the defender\\'s STR. That means a ' +\n 'small gap in strength is a large gap in damage, and a big enough one takes ' +\n 'your damage to nothing at all rather than merely reducing it. See ' +\n 'Mechanics.
' +\n\n '
Most skills have a parent that pulls them along.
' +\n '
Every section has a parent skill that earns a share of everything its ' +\n 'children do, and any child lagging behind the parent trains faster — up to ' +\n 'twice as fast at ' + MOD_PARENT.maxGap + ' levels behind. A neglected skill is ' +\n 'cheap to bring up later, which is the only reason having this many is ' +\n 'workable.
' +\n\n '
Cultivation is a second ladder, and it is not automatic.
' +\n '
Circulating qi levels a skill; the skill does not raise your realm. ' +\n 'Reaching the level opens a bottleneck, and breaking through it costs a pill ' +\n 'and can fail. See Cultivation.
' +\n '
' +\n\n '

What is the mod and what is the game

' +\n '

The game is index.html and it is entirely truezhangwei\\'s ' +\n 'work. Everything in mod.js is a modification of it and is not ' +\n 'endorsed by the author. Pages here mark added content with ' +\n 'mod where the distinction matters. The full ' +\n 'list of changes is in the changelog, on the bottom bar.

';\n return h;\n});\n\n/* --- 2. progression ------------------------------------------------------- */\nMOD_wikiPage('progress', 'Progression', function () {\n var cap = MOD_levelCap();\n var h = '

Progression

' +\n '

Skill levels are capped by how far the story has gone. Ten ' +\n 'rungs, 10 through 110. The cap applies to every skill at once; Renown is ' +\n 'the single exception, since it is counted rather than trained.

' +\n '' +\n '';\n\n MOD_TIERS.forEach(function (t) {\n var got = null;\n try { got = MOD_tierFlag(t); } catch (e) {}\n var here = (t.cap === cap);\n var need = t.needText ? t.needText\n : (t.flags.length ? t.flags.join(' or ') : 'the start of the game');\n h += '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
CapStageWhat opens it
' + t.cap + '' + MOD_WIKI.safe(t.name) + '' + MOD_WIKI.safe(need) + '' + (here ? 'you are here'\n : got ? 'reached' : '') + '
' +\n\n '

The experience curve

' +\n '

The game\\'s own skill curve is super-exponential — level 109 to 110 ' +\n 'alone costs ' + MOD_wikiNum(MOD_vanillaExpnext(109)) + ' experience, so the top of the ladder ' +\n 'is unreachable by a factor of about ten trillion. But the author only ever ' +\n 'designed skills up to level 15, and below that his curve is fine.

' +\n (MOD_XP.vanillaTo > 0\n ? '

So the mod keeps it. Below level ' + MOD_XP.vanillaTo + ' a skill costs ' +\n 'exactly what the original charges; from there it rises by ×' +\n MOD_XP.ratio.toFixed(4) + ' a level, a ratio solved so the climb to 110 takes ' +\n 'the same time it always did.'\n : '

You have chosen the Mod before 4.2 pacing: a plain geometric curve, ' +\n '×' + MOD_XP.ratio.toFixed(3) + ' a level from level 0, much faster than the ' +\n 'original early on. The Original pacing in settings puts back the ' +\n 'author\\'s own cost below level 10.') +\n ' Character level uses the game\\'s own curve, untouched.

' +\n '' +\n '';\n [1, 5, 9, 10, 15, 25, 50, 75, 90, 109].forEach(function (l) {\n h += '';\n });\n h += '
LevelExperience for the nextOriginal game
' + l +\n '' + MOD_wikiNum(MOD_expnextFor(l)) + '' +\n MOD_wikiNum(MOD_vanillaExpnext(l)) + '
';\n return h;\n});\n\n/* --- 3. skills ------------------------------------------------------------ */\nMOD_wikiPage('skills', 'Skills', function () {\n var cap = MOD_levelCap();\n var bySection = {};\n var keys = [];\n for (var k in skl) {\n var s = skl[k];\n if (!s || typeof s !== 'object' || !s.name) continue;\n keys.push(k);\n }\n keys.forEach(function (k) {\n var s = skl[k];\n var sec = MOD_sectionOf(s);\n (bySection[sec] = bySection[sec] || []).push(k);\n });\n\n var h = '

Skills

' +\n '

' + keys.length + ' skills in ' +\n Object.keys(MOD_SECTIONS).length + ' sections. Every one has perks at ' +\n MOD_LADDER.join(', ') + ', and every one that has milestones grants titles at ' +\n MOD_TITLE_RUNGS.join(', ') + '. Levels and ticks below are yours.

';\n\n Object.keys(MOD_SECTIONS).forEach(function (sec) {\n var list = bySection[sec];\n if (!list || !list.length) return;\n var parentKey = MOD_PARENT_KEY[sec];\n var g = '';\n if (parentKey && skl[parentKey]) {\n g += '

Parent skill: ' +\n MOD_WIKI.safe(skl[parentKey].bname || skl[parentKey].name) +\n ' — earns ' + Math.round(MOD_PARENT.feed * 100) +\n '% of what its children earn, and speeds up any child that lags behind it.

';\n }\n list.sort(function (a, b) { return skl[a].name < skl[b].name ? -1 : 1; });\n list.forEach(function (k) {\n var s = skl[k];\n var lvl = Number(s.lvl) || 0;\n g += '
' +\n '

' + MOD_WIKI.safe(s.bname || s.name) +\n (s.bname ? ' (' + MOD_WIKI.safe(s.name) + ')' : '') +\n '= cap ? ' capped' : '') + '\">lv ' + lvl +\n (lvl >= cap ? ' — at the cap' : '') + '

' +\n '
' + MOD_wikiDesc(s.desc) + '
';\n\n var at110 = MOD_wikiEffectAt(k, s, 110);\n if (at110) g += '
At level 110: ' + at110 + '
';\n\n if (s.mlstn && s.mlstn.length) {\n g += '';\n s.mlstn.forEach(function (m) {\n g += '' +\n '' +\n '' +\n '';\n });\n g += '
' + m.lv + '' + MOD_WIKI.safe(m.p || '—') + '' + (m.g === true ? '✓' : '') + '
';\n }\n g += '
';\n });\n h += MOD_wikiGroup(MOD_WIKI.safe(MOD_SECTIONS[sec]), list.length, g, 2);\n });\n return h;\n});\n\n/* --- 4. cultivation ------------------------------------------------------- */\nMOD_wikiPage('cultivation', 'Cultivation', function () {\n var now = MOD_realm();\n var elig = 0;\n try { elig = MOD_realmEligible().n; } catch (e) {}\n var qic = skl.qic ? skl.qic.lvl : 0;\n\n var h = '

Cultivation

' +\n '

Ten realms above Mortal. Circulating qi levels the Qi ' +\n 'Circulation skill; the skill does not advance the realm. Reaching the level ' +\n 'opens a bottleneck, and breaking through it costs a pill and can fail — the ' +\n 'pill is spent either way.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Realm' + MOD_WIKI.safe(MOD_realmTitle()) + '
Spiritual Root' + MOD_WIKI.safe(MOD_root().name) + '
Qi Circulationlevel ' + qic + '
Insight' + MOD_insight() + '
Bottleneck open' +\n (elig > now.n ? 'yes — ' + MOD_WIKI.safe(MOD_REALMS[now.n + 1].name) +\n ' is within reach' : 'no') + '
Qi Deviation' + (MOD_deviated()\n ? MOD_deviationLeft() + ' in-game minutes left' : 'no') + '
' +\n '

Realm thresholds are the same levels that earn a title rank, so realm N ' +\n 'is the level at which a rank N title arrives. Every realm multiplies all ' +\n 'stats and health, and the multipliers do not stack with each other — the ' +\n 'realm you are in is the one that applies.

' +\n '' +\n '';\n\n MOD_REALMS.forEach(function (r) {\n var pill = r.n >= 1 ? item['mod_bp' + r.n] : null;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmNameQi Circ.StatsHealthPill
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + r.qic + '×' + r.mult + '×' + r.hp + '' + (pill ? MOD_WIKI.safe(pill.name) : '—') +\n '
' +\n\n '

Where the pills come from

' +\n '

Every breakthrough pill has a source in normal play. None of them are ' +\n 'craftable, and none are cheap — a bottleneck you can buy in bulk is not a ' +\n 'bottleneck.

' +\n '' +\n '' +\n '' +\n '' +\n '
RealmsSource
1The dojo instructor, when qi ' +\n 'circulation is first taught
2–5The Herbalist in the village ' +\n '(vendor.pha1), restocked
6–10The dojo\\'s Level Advancement ' +\n 'ladder, and the Pill Tower
' +\n '

Realm 1 is deliberately not sold at the marketplace: ' +\n 'the marketplace is behind a 40%-a-visit chance encounter, and realm 1 opens ' +\n 'long before that is reliable.

' +\n\n '

Breaking through

' +\n '

A bottleneck wants three things, which is what the genre says it wants: ' +\n 'a pill, insight, and enough consolidation to make the ' +\n 'attempt worth making. The pill and the insight are both spent whether the ' +\n 'attempt works or not.

' +\n '

The odds improve the further past the threshold you are, plus your root, ' +\n 'plus time sat in Closed Door Training, and cap short of certain.

' +\n '' +\n '';\n [0, 2, 4, 6, 8].forEach(function (o) {\n var base = Math.min(0.55 + o * 0.05 + MOD_root().odds, 0.95);\n h += '';\n });\n h += '
Levels past the thresholdChanceAfter full seclusion
+' + o + '' +\n Math.round(base * 100) + '%' +\n Math.round(Math.min(base + MOD_CULT.secludeOdds, 0.95) * 100) + '%
' +\n\n '

Insight

' +\n '

Insight is the half of the price that cannot be bought. It comes from ' +\n 'three places, and they are the three the genre names: meditating, fights you ' +\n 'nearly lost, and going somewhere you have not been.

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
SourceRate
Circulate Qi, at a wall' +\n (MOD_CULT.meditateChance * 100).toFixed(1) + '% a second
Closed Door Training' +\n (MOD_CULT.secludeChance * 100).toFixed(1) + '% a second
A kill made under ' +\n Math.round(MOD_CULT.brinkFrac * 100) + '% health — ' +\n (MOD_FIGHTS.mode === 'original'\n ? 'the author\\'s areas rarely come close once you reach them; the three past ' +\n 'the golem arena and the rank duels do, within a few fights, as does an area ' +\n 'you walk into early'\n : 'every fight costs a share of your health, so a few in a row without resting') +\n '' +\n Math.round(MOD_CULT.brinkChance * 100) + '% a kill
Entering an area for the first timealways
' +\n '

The insight a realm asks for is sized to the climb to it: about ' +\n Math.round(MOD_CULT.wallShare * 100) + '% of the Circulate Qi time the climb took, ' +\n 'spent at the wall. Seclusion earns it three times as fast, and full ' +\n 'consolidation takes as long as that on average.

' +\n '' +\n '' +\n '';\n var wkHours = function (sec) {\n return sec >= 7200 ? (sec / 3600).toFixed(sec >= 36000 ? 0 : 1) + ' h' : Math.round(sec / 60) + ' min';\n };\n MOD_REALMS.forEach(function (r) {\n if (!r.n) return;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmClimb (Circulate Qi, 1x)Insight to enterFull seclusionQi Deviation (in-game)
' + MOD_WIKI.safe(r.name) + '' + wkHours(MOD_realmClimbSec(r.n)) + '' + MOD_insightNeed(r.n) + '' + wkHours(MOD_secludeFull(r.n)) + '' + (MOD_devMinutes(r.n) / 1440).toFixed(1) + ' days
' +\n\n '

The nine layers

' +\n '

Each realm is nine layers deep, derived from where Qi Circulation sits ' +\n 'between this realm\\'s threshold and the next one\\'s — so breaking through ' +\n 'puts you at the first layer of the new realm on its own. Layers are worth ' +\n '×' + MOD_CULT.layerMult + ' stats and ×' + MOD_CULT.layerBody +\n ' body each above the first, and the top realm has no rung above it to ' +\n 'measure against, so it stays at its first layer.

' +\n '' +\n '';\n for (var li = 1; li <= MOD_CULT.layers; li++) {\n h += '';\n }\n h += '
LayerCalled
' +\n li + '' + MOD_LAYER_ORD[li] + ' layer, ' +\n MOD_WIKI.safe(MOD_layerStage(li)) + '
' +\n '

At the threshold of the next realm and before breaking ' +\n 'through, you are a half step to it — the genre\\'s own name for the ' +\n 'state this mod calls a bottleneck.

' +\n\n '

Closed Door Training

' +\n '

Seclusion does two things. It consolidates — a full sitting ' +\n '(the table above) is worth +' + Math.round(MOD_CULT.secludeOdds * 100) +\n '% on the attempt — and it shelters: a breakthrough attempted while ' +\n 'you are still sitting in it cannot end in Qi Deviation, only in a lost ' +\n 'pill. It will not start unless you are actually at a wall.

' +\n\n '

Qi Deviation

' +\n '

Failing a breakthrough out in the open leaves your cultivation base ' +\n 'unstable for as long as the seclusion you skipped (the table above, never ' +\n 'under ' + MOD_CULT.devMinutes + ' in-game minutes): stats at ×' +\n MOD_CULT.devStat + ', body at ×' + MOD_CULT.devBody + ', and no second ' +\n 'attempt until it passes. It clears on its own, or at once with a ' +\n 'Qi Settling Pill. It never kills — the death system stays out of it.

' +\n\n '

Heavenly Tribulation

' +\n '

From realm ' + MOD_CULT.tribFrom + ' up, a successful breakthrough is ' +\n 'answered. The cost is a fraction of your maximum health, so it is a gate on ' +\n 'walking in healthy rather than a damage race; the realm bonus you are ' +\n 'standing on is what pays it. Fall to it and the realm does not open.

' +\n '' +\n '';\n MOD_REALMS.forEach(function (r) {\n if (!r.n || r.n < MOD_CULT.tribFrom) return;\n h += '' +\n '' +\n '';\n });\n h += '
RealmBoltsOf your max HP
' + MOD_WIKI.safe(r.name) + '' + MOD_tribulationBolts(r.n) + '' + Math.round(MOD_tribulationCost(r.n) * 100) + '%
' +\n\n '

Spiritual Root

' +\n '

Rolled once, when the dojo finishes teaching you to fight. It is never a ' +\n 'dead end: the worst grade costs cultivation speed, not access, and a ' +\n 'Root Cleansing Pill moves you up a grade.

' +\n '' +\n '';\n var rootTotal = 0;\n MOD_ROOTS.forEach(function (r) { rootTotal += r.weight; });\n MOD_ROOTS.forEach(function (r) {\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
GradeRootChanceCultivationBreakthrough
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + Math.round(r.weight / rootTotal * 100) + '%×' + r.xp + '' + (r.odds >= 0 ? '+' : '') +\n Math.round(r.odds * 100) + '%
';\n return h;\n});\n\n/* --- 5. elemental mastery ------------------------------------------------- */\nMOD_wikiPage('mastery', 'Elemental mastery', function () {\n var h = '

Elemental mastery mod

' +\n '

Six masteries, each with a technique that fires on its own ' +\n 'during a fight. Combat is automatic and there is no ability picker, so a ' +\n 'technique is a chance on each of your swings to become something else — one ' +\n 'that scales with INT and the element rather than with your weapon.

' +\n '

Nothing fires while you are still Mortal: the channels have to be open ' +\n 'first. Chance is ' + (MOD_TECH.perLevel * 100).toFixed(1) + '% per mastery ' +\n 'level plus ' + (MOD_TECH.perRealm * 100).toFixed(1) + '% per realm, capped at ' +\n Math.round(MOD_TECH.cap * 100) + '%; a technique lands for ×' +\n MOD_TECH.power + ' of a normal strike.

' +\n '' +\n '';\n\n var realm = 0;\n try { realm = MOD_realm().n; } catch (e) {}\n MOD_MASTERY.forEach(function (m) {\n var lvl = Number(m.skill.lvl) || 0;\n var ch = realm < 1 ? 0\n : Math.min(lvl * MOD_TECH.perLevel + realm * MOD_TECH.perRealm, MOD_TECH.cap);\n h += '' +\n '' +\n '' +\n '';\n });\n h += '
MasteryTechniqueLevelChance now
' +\n MOD_WIKI.safe(m.skill.name) + '' + MOD_WIKI.safe(m.name) + '' + lvl + '' + (realm < 1 ? 'Mortal'\n : (ch * 100).toFixed(1) + '%') + '
';\n return h;\n});\n\n/* --- 6. titles ------------------------------------------------------------ */\nMOD_wikiPage('titles', 'Titles', function () {\n var perm = MOD_permanentRank();\n var held = MOD_titleCount();\n var total = 0; for (var kk in ttl) if (ttl[kk] && ttl[kk].name) total++;\n\n var h = '

Titles

' +\n '

A title\\'s rank runs 1 to 10 and is derived from the level ' +\n 'that earns it, so a rank is a statement about where in the game you are. ' +\n 'Only the title you are wearing applies — until renown makes the lower ranks ' +\n 'permanent.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '
Titles held' + held + ' of ' + total + '
Renown level' + (skl.rnwn ? skl.rnwn.lvl : 0) +\n ' of ' + MOD_RENOWN_MAX + '
Ranks that apply without wearing' +\n (perm >= 1 ? '1–' + perm : 'none yet') + '
' +\n\n '

Ranks

' +\n '';\n MOD_RANK_AT.forEach(function (lv, i) {\n var st = MOD_rankStyle(i + 1);\n h += '' +\n '';\n });\n h += '
RankEarned from levelColour
' + (i + 1) + '' + lv + '' + '●●●' + '
' +\n\n '

Renown

' +\n '

Renown is not trained. It counts the titles you hold, and each level ' +\n 'multiplies every stat by ' + (1 + MOD_RENOWN_RATE) + ' — compounding, so ' +\n 'level ' + MOD_RENOWN_MAX + ' is ×' +\n (Math.round(MOD_renownMult(MOD_RENOWN_MAX) * 1000) / 1000) + '. At renown N, ' +\n 'every title of rank N or below applies permanently.

' +\n '' +\n '';\n MOD_RENOWN_STEPS.forEach(function (n, i) {\n h += '= n ? 'done' : '') + '\">' +\n '';\n });\n h += '
RenownTitles neededAll statsPassive ranks
' + (i + 1) +\n '' + n + '×' +\n (Math.round(MOD_renownMult(i + 1) * 1000) / 1000) + '1–' + (i + 1) + '
' +\n\n '

Every title

' +\n '

Grouped by the skill that grants it. What is left is either ' +\n 'a ladder of its own — creatures killed, items gathered, the ' +\n 'cultivation realms — or a one-off from the story.

';\n\n var groups = {}, fams = {}, loose = [];\n for (var k in ttl) {\n var t = ttl[k];\n /* Unlike the picker, the wiki KEEPS the author's broken entries and says\n so — same call as area.clg, which is listed and tagged \"nothing spawns\"\n rather than quietly dropped. A reader looking for why a title never\n appears is better served by the row than by its absence. */\n if (!t || t.name === undefined || t.name === null) continue;\n var sk = MOD_TITLE_SKILL[k];\n if (sk && skl[sk]) { (groups[sk] = groups[sk] || []).push(k); continue; }\n var stem = MOD_titleStem(k);\n if (stem) { (fams[stem] = fams[stem] || []).push(k); continue; }\n loose.push(k);\n }\n /* Same rule as the picker: a family of one is not a family. */\n Object.keys(fams).forEach(function (stem) {\n if (fams[stem].length < 2) { loose.push(fams[stem][0]); delete fams[stem]; }\n });\n var order = Object.keys(groups).sort(function (a, b) {\n return skl[a].name < skl[b].name ? -1 : 1;\n });\n\n var row = function (k) {\n var t = ttl[k];\n var st = MOD_rankStyle(t.rar);\n var gen = null;\n for (var i = 0; i < MOD_SKILL_TITLES.length; i++) {\n if (MOD_SKILL_TITLES[i].key === k) { gen = MOD_SKILL_TITLES[i]; break; }\n }\n /* The author left a handful unfinished: ttl.ddcd carries the literal string\n \"null\" for both name and description, and shpt2/shpt3/mone3 are blank.\n None is granted by anything, so none can be earned. */\n var broken = !t.name || t.name === 'null';\n return '
' +\n '' +\n MOD_WIKI.safe(t.name || '(unnamed)') + '' +\n (broken ? 'unfinished' : '') +\n 'rank ' + (t.rar || 1) + '' +\n (gen ? 'lv ' + gen.rung + ' · +' +\n Math.round(gen.xp * 100) + '% ' + MOD_WIKI.safe(skl[gen.skill].name) +\n ' exp' : '') +\n (t.have === true ? 'held' : '') +\n '
' + MOD_wikiDesc(t.desc) + '
';\n };\n\n var byRank = function (a, b) { return (ttl[a].rar || 1) - (ttl[b].rar || 1); };\n order.forEach(function (sk) {\n groups[sk].sort(byRank);\n h += MOD_wikiGroup(MOD_WIKI.safe(skl[sk].bname || skl[sk].name),\n groups[sk].length, groups[sk].map(row).join(''), 3);\n });\n /* The ladders that belong to no skill, by the same derivation the picker\n uses — so the two cannot drift apart. Sorted by the label, since that is\n what the reader scans. */\n var famList = Object.keys(fams)\n .map(function (stem) {\n fams[stem].sort(byRank);\n return { stem: stem,\n label: MOD_titleFamilyLabel(stem, fams[stem].map(function (k) { return ttl[k]; })) };\n })\n .sort(function (a, b) { return a.label < b.label ? -1 : a.label > b.label ? 1 : 0; });\n if (famList.length) {\n /* Nested one level down rather than sitting beside the skills. Eleven more\n top-level summaries cost ~530px on a page that is already the tallest in\n the wiki, and these are a different KIND of thing from a skill anyway. */\n var famInner = '', famTotal = 0;\n famList.forEach(function (f) {\n famTotal += fams[f.stem].length;\n famInner += MOD_wikiGroup(MOD_WIKI.safe(f.label), fams[f.stem].length,\n fams[f.stem].map(row).join(''), 4);\n });\n h += MOD_wikiGroup('Ladders of their own', famTotal, famInner, 3);\n }\n if (loose.length) {\n loose.sort(byRank);\n /* The story titles are the one group with no skill to divide them, and\n there are enough of them to need dividing. Rank rather than the alphabet:\n it is the axis they are already sorted on, and the one a reader cares\n about — a rank is a statement about where in the game a title comes from. */\n var inner;\n if (loose.length <= MOD_WIKI_ALPHA_AT) {\n inner = loose.map(row).join('');\n } else {\n var byRankNo = {};\n loose.forEach(function (k) {\n var r = Math.min(Math.max(Number(ttl[k].rar) || 1, 1), 10);\n (byRankNo[r] = byRankNo[r] || []).push(k);\n });\n inner = '';\n Object.keys(byRankNo).sort(function (a, b) { return a - b; }).forEach(function (r) {\n var st = MOD_rankStyle(r);\n inner += MOD_wikiGroup('Rank ' + r + '', byRankNo[r].length,\n byRankNo[r].map(row).join(''), 4);\n });\n }\n h += MOD_wikiGroup('From the story', loose.length, inner, 3);\n }\n return h;\n});\n\n/* --- 7. areas ------------------------------------------------------------- */\nMOD_wikiPage('areas', 'Areas & enemies', function () {\n var h = '

Areas & enemies

' +\n '

Every area, what lives in it and at what levels, and what it ' +\n 'drops. Enemy strength is not read off these levels — see the note below the ' +\n 'table.

';\n\n /* Two of the game's 21 areas are not places and listing them misleads:\n nwh \"Somewhere\" — where global.current_z parks whenever you are NOT in\n a fight, populated by creature.default (\"Nothing\").\n tst \"Test\" — reached only from chss.tst, which has id -1 and is in no\n sector, so nothing in the game can travel there.\n The test for \"is this a place\" is whether you can get to it, and neither\n can be. Everything else is listed whether or not you have been. */\n var MOD_WIKI_NOT_PLACES = { nwh: 1, tst: 1 };\n\n var keys = [];\n for (var k in area) {\n if (area[k] && area[k].pop && !MOD_WIKI_NOT_PLACES[k]) keys.push(k);\n }\n /* Ordered by the middle of the level band, which is the closest thing the\n game has to a difficulty order. */\n keys.sort(function (a, b) {\n var mid = function (z) {\n if (!z.pop.length) return 0;\n var s = 0; z.pop.forEach(function (e) { s += (e.lvlmin + e.lvlmax) / 2; });\n return s / z.pop.length;\n };\n return mid(area[a]) - mid(area[b]);\n });\n\n keys.forEach(function (k) {\n var z = area[k];\n /* An area whose baked bands are not finite can never spawn anything — see\n the note rendered under its table. Derived rather than listed by name, so\n it stays true if the game changes. */\n var broken = !z.popc || z.popc.length !== z.pop.length ||\n z.popc.some(function (band) { return !band || !isFinite(band[1] - band[0]); });\n h += '

' + MOD_WIKI.safe(z.name) +\n ' ' + k + '' +\n (broken ? 'nothing spawns' : '') +\n '' + (z.size === -1 ? 'endless' : z.size + ' to clear') +\n '

';\n if (z.pop.length) {\n /* The spawn share is NOT pop[i].c. z_bake normalises those weights into\n area.popc — a list of [lo,hi] bands that mon_gen rolls a uniform\n random against — and the weights do not have to sum to 1, so they are\n off wherever they don't. The Southern forest's .35/.45/.25 are really\n 33/43/24%. Read the bands the game actually rolls against. */\n var bands = z.popc || [];\n h += '';\n z.pop.forEach(function (e, i) {\n var c = e.crt || {};\n var band = bands[i];\n var share = band ? band[1] - band[0] : NaN;\n h += '' +\n '' +\n '';\n });\n h += '
' + MOD_WIKI.safe(c.name || '?') + 'lv ' + e.lvlmin +\n (e.lvlmax !== e.lvlmin ? '–' + e.lvlmax : '') + '' + (isFinite(share)\n ? Math.round(share * 100) + '% of spawns'\n : '—') + '
';\n if (broken) {\n h += '
Nothing spawns here. The area\\'s entries carry ' +\n 'no spawn weight, so the table the game rolls against comes out as ' +\n 'NaN and no creature can ever match it. A bug in the base game, left ' +\n 'as it is — nothing in the game travels to this area either.
';\n }\n }\n if (z.drop && z.drop.length) {\n var d = z.drop.map(function (x) {\n return MOD_WIKI.safe((x.item && x.item.name) || '?') +\n ' ' + ((x.c || 0) * 100).toFixed(2) + '%';\n }).join(', ');\n h += '
Drops: ' + d + '
';\n }\n h += '
';\n });\n\n var fightsOrig = MOD_FIGHTS.mode === 'original';\n h += '

How hard an enemy is

' +\n '

It depends on the Fights setting, which is ' +\n (fightsOrig ? 'Original' : 'Scaled') + ' now. On Original (the default) ' +\n 'every area the author wrote is exactly as he wrote it: his creatures, his ' +\n 'level ranges, and you out-grow them as you do in his game. On Scaled every ' +\n 'area is sized to you. The three areas past the golem arena and the rank ' +\n 'duels are sized to you either way.

' +\n '

Sizing to you works like this. Enemies are not fitted to a level. Each spawn is solved against your ' +\n 'actual power at the moment it appears, aiming at roughly ' + MOD_ENEMY.kill +\n ' swings to kill it and ' + MOD_ENEMY.die + ' before it kills you, with a ' +\n 'guaranteed margin between the two so a fight is never a coin flip. Variety ' +\n 'comes from the area\\'s level band and each creature\\'s own stat weighting, ' +\n 'not from difficulty scaling.

' +\n '

The reason for that: within the first stage alone you go from 1 STR to ' +\n 'around 265, so anything anchored to a level is either trivial at the end of ' +\n 'a stage or impossible at the start of one.

';\n return h;\n});\n\n/* --- 8. items -------------------------------------------------------------\n The one place where \"list everything\" needs help: 321 of the game's 371\n plain items are the same inventory type, so grouping by type alone leaves a\n single group of 321. Anything past MOD_WIKI_ALPHA_AT gets an alphabetical\n sub-split as well — the oldest index pattern there is, and derived rather\n than invented, which a hand-made taxonomy would not be.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_ALPHA_AT = 60;\n\n/* The game's own inventory tabs (isort): ALL / WPN / EQP / USE / OTHER. Using\n its categories rather than new ones means a group here is a tab there. */\nvar MOD_WIKI_STYPE = { 2: 'Weapons', 3: 'Equipment', 4: 'Usable', 5: 'Other' };\n\nfunction MOD_wikiItemRow(it) {\n var val = it.v !== undefined ? it.v : (it.val !== undefined ? it.val : null);\n return '
' +\n '

' + MOD_WIKI.safe(it.name) +\n (it.rar > 1 ? 'rarity ' + it.rar + '' : '') +\n (val !== null ? '' + MOD_wikiNum(val) + ' coin' : '') +\n (it.amount > 0 ? '×' + it.amount + '' : '') +\n '

' + MOD_wikiDesc(it.desc) + '
';\n}\n\n/* The letter a name files under. Several items are quoted — the master's\n manuals are literally named \"Sword Saint Manual\" — and filing those under a\n quotation mark is no use to anyone looking for S. */\nfunction MOD_wikiLetter(name) {\n var m = String(name || '').replace(/^[^0-9A-Za-z]+/, '');\n return (m.charAt(0) || '?').toUpperCase();\n}\n\nfunction MOD_wikiByLetter(a, b) {\n var x = MOD_wikiLetter(a.name) + String(a.name || '').toLowerCase();\n var y = MOD_wikiLetter(b.name) + String(b.name || '').toLowerCase();\n return x < y ? -1 : (x > y ? 1 : 0);\n}\n\n/* Flat below the threshold, alphabetically sub-grouped above it. Ranges are cut\n from the entries actually present, so they stay even as content is added. */\nfunction MOD_wikiAlpha(entries, render, level) {\n if (entries.length <= MOD_WIKI_ALPHA_AT) return entries.map(render).join('');\n var per = Math.ceil(entries.length / Math.ceil(entries.length / 40));\n var out = '';\n for (var i = 0; i < entries.length; i += per) {\n var chunk = entries.slice(i, i + per);\n var first = MOD_wikiLetter(chunk[0].name);\n var last = MOD_wikiLetter(chunk[chunk.length - 1].name);\n out += MOD_wikiGroup(first === last ? first : first + '–' + last,\n chunk.length, chunk.map(render).join(''), level || 4);\n }\n return out;\n}\n\nMOD_wikiPage('items', 'Items', function () {\n var h = '

Items

' +\n '

Everything the game defines, whether or not you have seen ' +\n 'it. Grouped the way your inventory groups it. Values are the base sell ' +\n 'price where the game gives one.

';\n\n /* item[] first, split by the inventory tab it appears under. */\n var buckets = {};\n for (var k in item) {\n var it = item[k];\n if (!it || !it.name || it.name === 'dummy') continue;\n var lab = MOD_WIKI_STYPE[it.stype] || 'Uncategorised';\n (buckets[lab] = buckets[lab] || []).push(it);\n }\n ['Usable', 'Other', 'Weapons', 'Equipment', 'Uncategorised'].forEach(function (lab) {\n var list = buckets[lab];\n if (!list || !list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(lab, list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n\n /* and the equipment objects, which live in their own globals */\n [['Weapons', wpn], ['Armour', eqp], ['Shields', sld], ['Accessories', acc]]\n .forEach(function (g) {\n var list = [];\n for (var kk in g[1]) {\n if (g[1][kk] && g[1][kk].name && g[1][kk].name !== 'dummy') list.push(g[1][kk]);\n }\n if (!list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(g[0], list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n return h;\n});\n\n/* --- 9. actions ----------------------------------------------------------- */\nMOD_wikiPage('actions', 'Actions', function () {\n var unlock = {};\n MOD_ACTION_UNLOCKS.forEach(function (u) {\n if (u.act) unlock[u.act.id] = (skl[u.skill] ? skl[u.skill].name : u.skill) +\n ' level ' + u.lv;\n });\n if (typeof act.mod_qi !== 'undefined') {\n unlock[act.mod_qi.id] = 'clearing the dojo\\'s Easiest, Easy and Normal fights';\n }\n\n var h = '

Actions

' +\n '

What you can be doing. One at a time, and only where the ' +\n 'action makes sense — unless \"Unrestricted actions\" is ticked in settings, ' +\n 'which lets several run at once anywhere. Both limits are deliberate; the ' +\n 'switch is there because they are not to everyone\\'s taste.

';\n\n var keys = [];\n for (var k in act) if (act[k] && act[k].name && act[k].name !== 'dummy') keys.push(k);\n keys.sort(function (a, b) { return act[a].name < act[b].name ? -1 : 1; });\n\n keys.forEach(function (k) {\n var a = act[k];\n var how = unlock[a.id];\n h += '
' +\n '

' + MOD_WIKI.safe(a.name) +\n (a.have === true ? 'unlocked'\n : 'locked') +\n (how ? 'from ' + MOD_WIKI.safe(how) + '' : '') +\n '

' + MOD_wikiDesc(a.desc) + '
';\n });\n return h;\n});\n\n/* --- 10. the dojo --------------------------------------------------------- */\nMOD_wikiPage('dojo', 'The dojo', function () {\n var h = '

The dojo

' +\n '

The instructor promises a reward every five levels. The base ' +\n 'game stops delivering at 30; the mod carries the same ladder to 110, on the ' +\n 'same terms.

' +\n '

Only the lowest rung you have not claimed is offered, which is how the ' +\n 'first six behave. Rewards are sized against the character experience curve ' +\n '(4×level3 + level2) — a level at 110 costs 5.3 ' +\n 'million, which is why the base game\\'s best pill is worth 0.3% of one there.

' +\n '' +\n '';\n\n MOD_DOJO_RUNGS.forEach(function (r) {\n var pill = item[r.pill];\n var extra = [];\n if (r.realmPill) extra.push('breakthrough pill for realm ' + r.realmPill);\n if (r.manual) extra.push('a choice of master\\'s manual');\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
LevelRewardCoinAlso
' + r.lv + '' + (pill ? MOD_WIKI.safe(pill.name) + ' ×' + r.n\n : '—') + '' + MOD_wikiNum(r.coin) + '' + (extra.join(', ') || '—') + '' + (global.flags[r.flag] ? 'claimed' : '') +\n '
' +\n\n '

Spirit pills

' +\n '

Character experience in a bottle. The four the mod adds continue the ' +\n 'game\\'s own series up to the new curve.

' +\n '' +\n '';\n MOD_PILLS.forEach(function (p) {\n h += '';\n });\n h += '
PillExperience
' + MOD_WIKI.safe(p[2]) + '' +\n p[3].toLocaleString() + '
' +\n\n '

Master\\'s manuals

' +\n '

A third grade of skillbook, offered at levels 50, 75 and 100 as a choice ' +\n 'the same way the first one was. Each is worth +' +\n Math.round(MOD_MANUAL_RATE * 100) + '% experience in its skill, permanently.

' +\n '' +\n '';\n MOD_MANUALS.forEach(function (m) {\n h += '';\n });\n h += '
ManualSkill
' + MOD_WIKI.safe(m[2]) + '' +\n MOD_WIKI.safe(skl[m[0]] ? (skl[m[0]].bname || skl[m[0]].name) : m[0]) +\n '
';\n return h;\n});\n\n/* --- 11. mechanics -------------------------------------------------------- */\nMOD_wikiPage('mechanics', 'Mechanics', function () {\n return '

Mechanics

' +\n\n '

Damage is subtractive, in both directions

' +\n '

A hit does the attacker\\'s strength minus the defender\\'s strength, plus ' +\n 'the weapon, plus one, before criticals. A defender\\'s STR is their ' +\n 'armour — there is no separate armour stat doing that job.

' +\n '

Two consequences worth carrying around:

' +\n '
    ' +\n '
  • Damage does not taper, it stops. Once a defender\\'s STR passes ' +\n 'yours, your hits do not get small; they land for nothing. Progress against ' +\n 'something too strong is not slow, it is zero.
  • ' +\n '
  • A stat ratio tells you nothing. Comparing your HP to an enemy\\'s ' +\n 'strength ignores the subtraction entirely and reads several times off in ' +\n 'either direction.
  • ' +\n '
' +\n '

This is also the reason enemies are never scaled by raising ' +\n 'their strength: it would raise their offence and their armour together, and ' +\n 'the second effect arrives first.

' +\n\n '

Criticals carry the late game

' +\n '

Critical rate reaches about a third at the top of the ladder and a ' +\n 'critical is roughly eight times a normal swing, so around three quarters of ' +\n 'all damage done at that point is critical damage. Anything that raises ' +\n 'critical rate or critical damage is worth more than it looks; Sharp Eye and ' +\n 'War are the two that do.

' +\n\n '

Hit chance

' +\n '

Landing a hit is a contest of agility. It is a ratio rather than a ' +\n 'subtraction, so agility behaves more gently than strength does — falling ' +\n 'behind on it costs you accuracy, not everything.

' +\n\n '

Skill sections and parents

' +\n '

Each of the ' + Object.keys(MOD_SECTIONS).length + ' sections has a parent ' +\n 'skill. A parent earns ' + Math.round(MOD_PARENT.feed * 100) + '% of everything ' +\n 'its children earn and grants no stats of its own. In return, a child below ' +\n 'the parent\\'s level trains ' + Math.round(MOD_PARENT.perLevel * 100) +\n '% faster per level of lag, up to ' + MOD_PARENT.maxGap + ' levels behind. ' +\n 'Bringing one section up makes every skill in it cheaper to catch up.

' +\n\n '

Saving

' +\n '

Three slots, switched from the bottom bar. The game builds its world once ' +\n 'at startup and cannot unload a save, so switching slots or starting a new ' +\n 'game reloads the page. Exporting a save before running anything unusual is ' +\n 'always the safe move.

';\n});\n\n/* --- the document ---------------------------------------------------------\n One string, no fetches, no dependencies: this has to work from file:// as\n readily as from a server.\n ------------------------------------------------------------------------- */\n\nfunction MOD_wikiCss() {\n return [\n ':root{color-scheme:dark}',\n '*{box-sizing:border-box}',\n /* Menlo and Consolas before the generic: with MS Gothic missing (any Mac)\n \"monospace\" is Courier, which Safari sets wider than Chrome does, and\n table cells wrapped on one and not the other. */\n 'body{margin:0;background:#060a1c;color:#dfe6f5;font:14px/1.6 \"MS Gothic\",' +\n '\"MS Mincho\",Menlo,Consolas,\"DejaVu Sans Mono\",monospace;display:flex;min-height:100vh}',\n 'a{color:#7cb0ff}',\n 'nav{width:210px;flex:0 0 210px;background:#081040;border-right:1px solid #46a;' +\n 'position:sticky;top:0;height:100vh;overflow:auto;padding:14px 0}',\n 'nav h2{font-size:13px;color:#9fb4d8;margin:0 0 10px 14px;letter-spacing:1px}',\n 'nav a{display:block;padding:6px 14px;text-decoration:none;color:#cfe0ff;' +\n 'border-left:3px solid transparent}',\n 'nav a:hover{background:#154080}',\n 'nav a.on{background:#123070;border-left-color:#7cb0ff;color:#fff}',\n 'nav .srch{margin:0 10px 12px;width:calc(100% - 20px);background:#050a20;' +\n 'color:#dfe6f5;border:1px solid #46a;padding:5px;font:inherit}',\n 'nav .xall{display:block;margin:12px 10px 0;color:#8296b8;font-size:12px;' +\n 'cursor:pointer;-webkit-user-select:none;user-select:none}',\n 'nav .xall input{vertical-align:-1px;margin-right:4px}',\n 'details.wk-g{border:1px solid #17224a;background:#0a1130;margin:8px 0}',\n 'details.wk-g>summary{cursor:pointer;padding:7px 12px;list-style:none;' +\n 'background:#0d1740}',\n 'details.wk-g>summary:hover{background:#154080}',\n 'details.wk-g>summary::-webkit-details-marker{display:none}',\n // a caret drawn rather than borrowed, so it is the same in every browser\n 'details.wk-g>summary::before{content:\"\\\\25B8\";color:#7cb0ff;' +\n 'display:inline-block;width:1em;transition:transform .12s}',\n 'details.wk-g[open]>summary::before{transform:rotate(90deg)}',\n 'details.wk-g>summary h2,details.wk-g>summary h3{display:inline;margin:0;' +\n 'font-size:15px;color:#fff}',\n '.wk-gi{padding:2px 12px 10px}',\n 'details.wk-g details.wk-g>summary h4{display:inline;margin:0;font-size:13px;' +\n 'color:#cfe0ff;font-weight:normal}',\n 'details.wk-g details.wk-g>summary{padding:5px 10px;background:#0b1435}',\n 'main{flex:1;min-width:0;padding:22px 30px 80px;max-width:1000px}',\n 'h1{font-size:26px;margin:0 0 6px;color:#fff;border-bottom:1px solid #46a;' +\n 'padding-bottom:8px}',\n 'h2{font-size:19px;color:#bcd4ff;margin:30px 0 8px}',\n 'h3{font-size:15px;color:#fff;margin:0 0 4px}',\n 'p.lede{color:#a8bcdc;margin:0 0 18px}',\n '.dim{color:#8296b8;font-weight:normal}',\n '.flav{font-size:12px;font-style:italic}',\n '.note{background:#0b1638;border-left:3px solid #46a;padding:8px 12px;' +\n 'color:#a8bcdc;margin:14px 0}',\n 'code,pre{background:#0b1638;border:1px solid #24356e;padding:1px 5px;' +\n 'color:#9fd0ff}',\n 'pre{padding:10px;overflow-x:auto}',\n 'dl dt{color:#fff;margin-top:14px}',\n 'dl dd{margin:2px 0 0 0;color:#a8bcdc}',\n 'ul{color:#a8bcdc}',\n 'table.wk-tbl{border-collapse:collapse;width:100%;margin:10px 0;' +\n 'display:block;overflow-x:auto}',\n 'table.wk-tbl th{text-align:left;color:#9fb4d8;font-weight:normal;' +\n 'border-bottom:1px solid #46a;padding:5px 9px;white-space:nowrap}',\n 'table.wk-tbl td{border-bottom:1px solid #17224a;padding:5px 9px;' +\n 'vertical-align:top}',\n 'table.wk-tbl tr.done td{color:#cfe0ff}',\n 'table.wk-tbl tr.here{background:#123070}',\n '.num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}',\n '.wk-e{border:1px solid #17224a;background:#081040;padding:9px 12px;' +\n 'margin:7px 0;border-radius:2px}',\n 'tr.wk-e{border:0;background:none}',\n '.wk-e.done{border-color:#2a4a8c}',\n '.wk-e h3{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}',\n '.lv{margin-left:auto;font-size:12px;color:#8296b8;font-weight:normal}',\n '.lv.capped{color:#ffd24a}',\n '.rank{font-size:12px;color:#8296b8;font-weight:normal}',\n '.desc{color:#a8bcdc;font-size:13px}',\n '.proj{color:#ffd24a;font-size:12px;margin-top:3px}',\n '.tag{font-size:11px;border:1px solid #46a;color:#9fb4d8;padding:0 5px;' +\n 'border-radius:2px;font-weight:normal}',\n '.tag.mod{border-color:#7a4ea8;color:#c07bff}',\n '.tag.done{border-color:#2e7d4f;color:#6fdc9a}',\n '.tag.here{border-color:#ffd24a;color:#ffd24a}',\n '.tag.warn{border-color:#a8562e;color:#ff9a5c}',\n 'table.wk-perks{border-collapse:collapse;margin:6px 0 0;width:100%}',\n 'table.wk-perks td{padding:2px 8px 2px 0;font-size:12px;color:#8296b8;' +\n 'border-bottom:1px solid #101a3c}',\n 'table.wk-perks tr.done td{color:#cfe0ff}',\n 'table.wk-perks td.tick{color:#6fdc9a;width:1em;text-align:right}',\n // fixed columns so the level bands line up between one area and the next\n 'table.wk-pop{table-layout:fixed}',\n 'table.wk-pop td:first-child{width:60%}',\n 'table.wk-pop td:nth-child(2){width:20%}',\n '.wk-title .tname{font-weight:bold}',\n '.wk-now{background:#0b1638;border:1px solid #24356e;padding:10px 14px;' +\n 'margin:0 0 18px;display:inline-block;min-width:320px}',\n '.wk-now h3{color:#9fb4d8;font-weight:normal;font-size:13px;' +\n 'letter-spacing:1px;margin-bottom:6px}',\n '.wk-now td{padding:2px 16px 2px 0}',\n '.wk-now td:last-child{color:#fff;text-align:right}',\n '.pg{display:none}.pg.on{display:block}',\n 'footer{color:#5a6c8c;font-size:12px;border-top:1px solid #17224a;' +\n 'margin-top:40px;padding-top:12px}',\n '@media(max-width:760px){body{display:block}nav{width:auto;height:auto;' +\n 'position:static;flex:none}main{padding:16px}}'\n ].join('\\n');\n}\n\n/* Filtering, page switching and the hash. Small enough to inline, and inlining\n it is the point — the document has to stand on its own. */\nfunction MOD_wikiJs() {\n return [\n 'var pages=[].slice.call(document.querySelectorAll(\".pg\"));',\n 'var links=[].slice.call(document.querySelectorAll(\"nav a[data-p]\"));',\n 'function show(id){',\n ' var found=false;',\n ' pages.forEach(function(p){var on=p.id===\"pg-\"+id;if(on)found=true;',\n ' p.className=\"pg\"+(on?\" on\":\"\")});',\n ' if(!found&&pages.length){pages[0].className=\"pg on\";id=pages[0].id.slice(3)}',\n ' links.forEach(function(a){a.className=a.dataset.p===id?\"on\":\"\"});',\n ' window.scrollTo(0,0);return id;}',\n 'links.forEach(function(a){a.addEventListener(\"click\",function(e){',\n ' e.preventDefault();location.hash=a.dataset.p;show(a.dataset.p)})});',\n 'window.addEventListener(\"hashchange\",function(){show(location.hash.slice(1))});',\n 'show(location.hash.slice(1)||\"start\");',\n // Search filters entries, hides groups left with none, and OPENS the groups\n // that still have some — a match hidden inside a collapsed group reads as\n // no match at all, which is worse than not having search.\n 'var box=document.getElementById(\"srch\");',\n 'var xall=document.getElementById(\"xall\");',\n 'var groups=[].slice.call(document.querySelectorAll(\"details.wk-g\"));',\n 'function filter(){',\n ' var q=box.value.trim().toLowerCase();',\n ' pages.forEach(function(p){',\n ' [].slice.call(p.querySelectorAll(\".wk-e\")).forEach(function(e){',\n ' e.style.display=(!q||e.textContent.toLowerCase().indexOf(q)>=0)?\"\":\"none\"})});',\n ' groups.forEach(function(g){',\n ' var any=[].slice.call(g.querySelectorAll(\".wk-e\")).some(function(e){',\n ' return e.style.display!==\"none\"});',\n ' g.style.display=any?\"\":\"none\";',\n ' g.open=xall.checked||(!!q&&any)}); }',\n 'box.addEventListener(\"input\",filter);',\n 'box.addEventListener(\"keydown\",function(e){if(e.key===\"Escape\"){box.value=\"\";filter()}});',\n // Everything is collapsed by default because three of these pages run past\n // 40,000px open. This is the way back to one long document, which is what\n // the browser's own find needs.\n 'xall.addEventListener(\"change\",filter);'\n ].join('\\n');\n}\n\nfunction MOD_wikiBuild() {\n var nav = '', body = '';\n MOD_WIKI_PAGES.forEach(function (pg) {\n nav += '' +\n MOD_WIKI.safe(pg.label) + '';\n var inner;\n /* One page throwing must not take the wiki down with it — the page says so\n and the rest still renders. */\n try { inner = pg.build(); }\n catch (e) {\n inner = '

' + MOD_WIKI.safe(pg.label) + '

This page ' +\n 'could not be built: ' + MOD_WIKI.safe(e.message) + '

';\n console.warn('[mod] wiki page \"' + pg.id + '\" failed: ' + e.message);\n }\n body += '
' + inner + '
';\n });\n\n var stamp = new Date().toLocaleString();\n return '' +\n '' +\n '' + MOD_WIKI.safe(MOD_WIKI.title) + '' +\n '' +\n '' +\n '
' + body +\n '
Generated from the running game at ' + MOD_WIKI.safe(stamp) +\n ' — mod ' + MOD_WIKI.safe(MOD.version) + '. Everything here is read from the ' +\n 'live data, including your own save, so it cannot fall out of step with the ' +\n 'game. The game is truezhangwei\\'s; the mod is not endorsed by the author.' +\n '
' +\n ' inside content closing the block early, so that is the\n one thing scrubbed. */\n safe: function (h) {\n return String(h === undefined || h === null ? '' : h).replace(/<\\/script/gi, '<\\\\/script');\n }\n};\n\n/* Descriptions carry real HTML (coloured spans, the game's separator div), so\n they are passed through rather than escaped — but the separator becomes a\n rule, since a wiki row is not a tooltip. */\nfunction MOD_wikiDesc(d) {\n var s;\n try { s = (typeof d === 'function') ? d() : d; } catch (e) { s = ''; }\n if (!s) return '';\n s = String(s).split('
 
').join('
');\n return MOD_WIKI.safe(s);\n}\n\nfunction MOD_wikiNum(n) {\n if (!isFinite(n)) return '—';\n return Math.round(n).toLocaleString();\n}\n\n/* An effect line at a level the player has not reached yet. The effect\n functions read the skill they are handed, so the level is borrowed and put\n back; anything that throws simply gets no projection. */\nfunction MOD_wikiEffectAt(key, sk, lvl) {\n var eff = MOD_EFFECTS[key];\n if (!eff) return '';\n var was = sk.lvl, out = '';\n try { sk.lvl = lvl; out = eff(sk) || ''; }\n catch (e) { out = ''; }\n finally { sk.lvl = was; }\n return MOD_WIKI.safe(out);\n}\n\n/* --- page builders --------------------------------------------------------\n Each returns the inner HTML of one page. `wk-e` marks a searchable entry and\n `wk-g` a group that hides itself when all its entries are filtered out.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_PAGES = [];\n\nfunction MOD_wikiPage(id, label, build) {\n MOD_WIKI_PAGES.push({ id: id, label: label, build: build });\n}\n\n/* A collapsible group.\n\n Not decoration: with every skill, item and title listed, three of the eleven\n pages ran past 40,000 pixels — a hundred screens of scrolling, which is the\n same as not having the content at all. Collapsed, each of those pages opens\n as an index you can read in one screen and expand where you want.\n\n
rather than a click handler, so it still works with JavaScript off,\n the browser's own find can be made to reach inside it, and the summary is\n keyboard-reachable for free. `level` is the heading level for the summary,\n since a group is sometimes a section of a page and sometimes a subsection. */\nfunction MOD_wikiGroup(heading, count, inner, level) {\n var hn = 'h' + (level || 2);\n return '
<' + hn + '>' + heading +\n (count !== null && count !== undefined\n ? ' (' + count + ')' : '') +\n '
' + inner + '
';\n}\n\n/* --- 1. orientation ------------------------------------------------------- */\nMOD_wikiPage('start', 'Start here', function () {\n var cap = MOD_levelCap();\n var realm = MOD_realm();\n var h = '

proto23

' +\n '

An idle RPG by truezhangwei, plus a mod. You train skills by ' +\n 'doing things, the things you can do open up as the story does, and none of ' +\n 'it is in a hurry.

' +\n\n '

Where you are right now

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Character level' + MOD_wikiNum(you.lvl) + '
Skill level cap' + cap + ' — ' +\n MOD_WIKI.safe(MOD_CAP.name) + '
Cultivation realm' + MOD_WIKI.safe(realm.name) +\n ' (' + realm.n + ' of ' + (MOD_REALMS.length - 1) + ')
Titles held' + MOD_titleCount() + ' — renown ' +\n (skl.rnwn ? skl.rnwn.lvl : 0) + '
Coin' + MOD_wikiNum(you.wealth || 0) + '
' +\n\n '

The four things worth knowing early

' +\n '
' +\n '
Your skill levels are capped by the story, not by grinding.
' +\n '
The cap starts at 10 and ends at 110, and it moves when you reach a new ' +\n 'place — not when you earn enough experience. If a skill has stopped ' +\n 'levelling, you are not doing it wrong; you need to go somewhere. See ' +\n 'Progression.
' +\n\n '
Armour is subtraction, so levels matter more than they look.
' +\n '
A hit does the attacker\\'s STR minus the defender\\'s STR. That means a ' +\n 'small gap in strength is a large gap in damage, and a big enough one takes ' +\n 'your damage to nothing at all rather than merely reducing it. See ' +\n 'Mechanics.
' +\n\n '
Most skills have a parent that pulls them along.
' +\n '
Every section has a parent skill that earns a share of everything its ' +\n 'children do, and any child lagging behind the parent trains faster — up to ' +\n 'twice as fast at ' + MOD_PARENT.maxGap + ' levels behind. A neglected skill is ' +\n 'cheap to bring up later, which is the only reason having this many is ' +\n 'workable.
' +\n\n '
Cultivation is a second ladder, and it is not automatic.
' +\n '
Circulating qi levels a skill; the skill does not raise your realm. ' +\n 'Reaching the level opens a bottleneck, and breaking through it costs a pill ' +\n 'and can fail. See Cultivation.
' +\n '
' +\n\n '

What is the mod and what is the game

' +\n '

The game is index.html and it is entirely truezhangwei\\'s ' +\n 'work. Everything in mod.js is a modification of it and is not ' +\n 'endorsed by the author. Pages here mark added content with ' +\n 'mod where the distinction matters. The full ' +\n 'list of changes is in the changelog, on the bottom bar.

';\n return h;\n});\n\n/* --- 2. progression ------------------------------------------------------- */\nMOD_wikiPage('progress', 'Progression', function () {\n var cap = MOD_levelCap();\n var h = '

Progression

' +\n '

Skill levels are capped by how far the story has gone. Ten ' +\n 'rungs, 10 through 110. The cap applies to every skill at once; Renown is ' +\n 'the single exception, since it is counted rather than trained.

' +\n '' +\n '';\n\n MOD_TIERS.forEach(function (t) {\n var got = null;\n try { got = MOD_tierFlag(t); } catch (e) {}\n var here = (t.cap === cap);\n var need = t.needText ? t.needText\n : (t.flags.length ? t.flags.join(' or ') : 'the start of the game');\n h += '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
CapStageWhat opens it
' + t.cap + '' + MOD_WIKI.safe(t.name) + '' + MOD_WIKI.safe(need) + '' + (here ? 'you are here'\n : got ? 'reached' : '') + '
' +\n\n '

The experience curve

' +\n '

The game\\'s own skill curve is super-exponential — level 109 to 110 ' +\n 'alone costs ' + MOD_wikiNum(MOD_vanillaExpnext(109)) + ' experience, so the top of the ladder ' +\n 'is unreachable by a factor of about ten trillion. But the author only ever ' +\n 'designed skills up to level 15, and below that his curve is fine.

' +\n (MOD_XP.vanillaTo > 0\n ? '

So the mod keeps it. Below level ' + MOD_XP.vanillaTo + ' a skill costs ' +\n 'exactly what the original charges; from there it rises by ×' +\n MOD_XP.ratio.toFixed(4) + ' a level, a ratio solved so the climb to 110 takes ' +\n 'the same time it always did.'\n : '

You have chosen the Mod before 4.2 pacing: a plain geometric curve, ' +\n '×' + MOD_XP.ratio.toFixed(3) + ' a level from level 0, much faster than the ' +\n 'original early on. The Original pacing in settings puts back the ' +\n 'author\\'s own cost below level 10.') +\n ' Character level uses the game\\'s own curve, untouched.

' +\n '' +\n '';\n [1, 5, 9, 10, 15, 25, 50, 75, 90, 109].forEach(function (l) {\n h += '';\n });\n h += '
LevelExperience for the nextOriginal game
' + l +\n '' + MOD_wikiNum(MOD_expnextFor(l)) + '' +\n MOD_wikiNum(MOD_vanillaExpnext(l)) + '
';\n return h;\n});\n\n/* --- 3. skills ------------------------------------------------------------ */\nMOD_wikiPage('skills', 'Skills', function () {\n var cap = MOD_levelCap();\n var bySection = {};\n var keys = [];\n for (var k in skl) {\n var s = skl[k];\n if (!s || typeof s !== 'object' || !s.name) continue;\n keys.push(k);\n }\n keys.forEach(function (k) {\n var s = skl[k];\n var sec = MOD_sectionOf(s);\n (bySection[sec] = bySection[sec] || []).push(k);\n });\n\n var h = '

Skills

' +\n '

' + keys.length + ' skills in ' +\n Object.keys(MOD_SECTIONS).length + ' sections. Every one has perks at ' +\n MOD_LADDER.join(', ') + ', and every one that has milestones grants titles at ' +\n MOD_TITLE_RUNGS.join(', ') + '. Levels and ticks below are yours.

';\n\n Object.keys(MOD_SECTIONS).forEach(function (sec) {\n var list = bySection[sec];\n if (!list || !list.length) return;\n var parentKey = MOD_PARENT_KEY[sec];\n var g = '';\n if (parentKey && skl[parentKey]) {\n g += '

Parent skill: ' +\n MOD_WIKI.safe(skl[parentKey].bname || skl[parentKey].name) +\n ' — earns ' + Math.round(MOD_PARENT.feed * 100) +\n '% of what its children earn, and speeds up any child that lags behind it.

';\n }\n list.sort(function (a, b) { return skl[a].name < skl[b].name ? -1 : 1; });\n list.forEach(function (k) {\n var s = skl[k];\n var lvl = Number(s.lvl) || 0;\n g += '
' +\n '

' + MOD_WIKI.safe(s.bname || s.name) +\n (s.bname ? ' (' + MOD_WIKI.safe(s.name) + ')' : '') +\n '= cap ? ' capped' : '') + '\">lv ' + lvl +\n (lvl >= cap ? ' — at the cap' : '') + '

' +\n '
' + MOD_wikiDesc(s.desc) + '
';\n\n var at110 = MOD_wikiEffectAt(k, s, 110);\n if (at110) g += '
At level 110: ' + at110 + '
';\n\n if (s.mlstn && s.mlstn.length) {\n g += '';\n s.mlstn.forEach(function (m) {\n g += '' +\n '' +\n '' +\n '';\n });\n g += '
' + m.lv + '' + MOD_WIKI.safe(m.p || '—') + '' + (m.g === true ? '✓' : '') + '
';\n }\n g += '
';\n });\n h += MOD_wikiGroup(MOD_WIKI.safe(MOD_SECTIONS[sec]), list.length, g, 2);\n });\n return h;\n});\n\n/* --- 4. cultivation ------------------------------------------------------- */\nMOD_wikiPage('cultivation', 'Cultivation', function () {\n var now = MOD_realm();\n var elig = 0;\n try { elig = MOD_realmEligible().n; } catch (e) {}\n var qic = skl.qic ? skl.qic.lvl : 0;\n\n var h = '

Cultivation

' +\n '

Ten realms above Mortal. Circulating qi levels the Qi ' +\n 'Circulation skill; the skill does not advance the realm. Reaching the level ' +\n 'opens a bottleneck, and breaking through it costs a pill and can fail — the ' +\n 'pill is spent either way.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Realm' + MOD_WIKI.safe(MOD_realmTitle()) + '
Spiritual Root' + MOD_WIKI.safe(MOD_root().name) + '
Qi Circulationlevel ' + qic + '
Insight' + MOD_insight() + '
Bottleneck open' +\n (elig > now.n ? 'yes — ' + MOD_WIKI.safe(MOD_REALMS[now.n + 1].name) +\n ' is within reach' : 'no') + '
Qi Deviation' + (MOD_deviated()\n ? MOD_deviationLeft() + ' in-game minutes left' : 'no') + '
' +\n '

Realm thresholds are the same levels that earn a title rank, so realm N ' +\n 'is the level at which a rank N title arrives. Every realm multiplies all ' +\n 'stats and health, and the multipliers do not stack with each other — the ' +\n 'realm you are in is the one that applies.

' +\n '' +\n '';\n\n MOD_REALMS.forEach(function (r) {\n var pill = r.n >= 1 ? item['mod_bp' + r.n] : null;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmNameQi Circ.StatsHealthPill
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + r.qic + '×' + r.mult + '×' + r.hp + '' + (pill ? MOD_WIKI.safe(pill.name) : '—') +\n '
' +\n\n '

Where the pills come from

' +\n '

Every breakthrough pill has a source in normal play. None of them are ' +\n 'craftable, and none are cheap — a bottleneck you can buy in bulk is not a ' +\n 'bottleneck.

' +\n '' +\n '' +\n '' +\n '' +\n '
RealmsSource
1The dojo instructor, when qi ' +\n 'circulation is first taught
2–5The Herbalist in the village ' +\n '(vendor.pha1), restocked
6–10The dojo\\'s Level Advancement ' +\n 'ladder, and the Pill Tower
' +\n '

Realm 1 is deliberately not sold at the marketplace: ' +\n 'the marketplace is behind a 40%-a-visit chance encounter, and realm 1 opens ' +\n 'long before that is reliable.

' +\n\n '

Breaking through

' +\n '

A bottleneck wants three things, which is what the genre says it wants: ' +\n 'a pill, insight, and enough consolidation to make the ' +\n 'attempt worth making. The pill and the insight are both spent whether the ' +\n 'attempt works or not.

' +\n '

The odds improve the further past the threshold you are, plus your root, ' +\n 'plus time sat in Closed Door Training, and cap short of certain.

' +\n '' +\n '';\n [0, 2, 4, 6, 8].forEach(function (o) {\n var base = Math.min(0.55 + o * 0.05 + MOD_root().odds, 0.95);\n h += '';\n });\n h += '
Levels past the thresholdChanceAfter full seclusion
+' + o + '' +\n Math.round(base * 100) + '%' +\n Math.round(Math.min(base + MOD_CULT.secludeOdds, 0.95) * 100) + '%
' +\n\n '

Insight

' +\n '

Insight is the half of the price that cannot be bought. It comes from ' +\n 'three places, and they are the three the genre names: meditating, fights you ' +\n 'nearly lost, and going somewhere you have not been.

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
SourceRate
Circulate Qi, at a wall' +\n (MOD_CULT.meditateChance * 100).toFixed(1) + '% a second
Closed Door Training' +\n (MOD_CULT.secludeChance * 100).toFixed(1) + '% a second
A kill made under ' +\n Math.round(MOD_CULT.brinkFrac * 100) + '% health — ' +\n (MOD_FIGHTS.mode === 'original'\n ? 'the author\\'s areas rarely come close once you reach them; the three past ' +\n 'the golem arena and the rank duels do, within a few fights, as does an area ' +\n 'you walk into early'\n : 'every fight costs a share of your health, so a few in a row without resting') +\n '' +\n Math.round(MOD_CULT.brinkChance * 100) + '% a kill
Entering an area for the first timealways
' +\n '

The insight a realm asks for is sized to the climb to it: about ' +\n Math.round(MOD_CULT.wallShare * 100) + '% of the Circulate Qi time the climb took, ' +\n 'spent at the wall. Seclusion earns it three times as fast, and full ' +\n 'consolidation takes as long as that on average.

' +\n '' +\n '' +\n '';\n var wkHours = function (sec) {\n return sec >= 7200 ? (sec / 3600).toFixed(sec >= 36000 ? 0 : 1) + ' h' : Math.round(sec / 60) + ' min';\n };\n MOD_REALMS.forEach(function (r) {\n if (!r.n) return;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmClimb (Circulate Qi, 1x)Insight to enterFull seclusionQi Deviation (in-game)
' + MOD_WIKI.safe(r.name) + '' + wkHours(MOD_realmClimbSec(r.n)) + '' + MOD_insightNeed(r.n) + '' + wkHours(MOD_secludeFull(r.n)) + '' + (MOD_devMinutes(r.n) / 1440).toFixed(1) + ' days
' +\n\n '

The nine layers

' +\n '

Each realm is nine layers deep, derived from where Qi Circulation sits ' +\n 'between this realm\\'s threshold and the next one\\'s — so breaking through ' +\n 'puts you at the first layer of the new realm on its own. Layers are worth ' +\n '×' + MOD_CULT.layerMult + ' stats and ×' + MOD_CULT.layerBody +\n ' body each above the first, and the top realm has no rung above it to ' +\n 'measure against, so it stays at its first layer.

' +\n '' +\n '';\n for (var li = 1; li <= MOD_CULT.layers; li++) {\n h += '';\n }\n h += '
LayerCalled
' +\n li + '' + MOD_LAYER_ORD[li] + ' layer, ' +\n MOD_WIKI.safe(MOD_layerStage(li)) + '
' +\n '

At the threshold of the next realm and before breaking ' +\n 'through, you are a half step to it — the genre\\'s own name for the ' +\n 'state this mod calls a bottleneck.

' +\n\n '

Closed Door Training

' +\n '

Seclusion does two things. It consolidates — a full sitting ' +\n '(the table above) is worth +' + Math.round(MOD_CULT.secludeOdds * 100) +\n '% on the attempt — and it shelters: a breakthrough attempted while ' +\n 'you are still sitting in it cannot end in Qi Deviation, only in a lost ' +\n 'pill. It will not start unless you are actually at a wall.

' +\n\n '

Qi Deviation

' +\n '

Failing a breakthrough out in the open leaves your cultivation base ' +\n 'unstable for as long as the seclusion you skipped (the table above, never ' +\n 'under ' + MOD_CULT.devMinutes + ' in-game minutes): stats at ×' +\n MOD_CULT.devStat + ', body at ×' + MOD_CULT.devBody + ', and no second ' +\n 'attempt until it passes. It clears on its own, or at once with a ' +\n 'Qi Settling Pill. It never kills — the death system stays out of it.

' +\n\n '

Heavenly Tribulation

' +\n '

From realm ' + MOD_CULT.tribFrom + ' up, a successful breakthrough is ' +\n 'answered. The cost is a fraction of your maximum health, so it is a gate on ' +\n 'walking in healthy rather than a damage race; the realm bonus you are ' +\n 'standing on is what pays it. Fall to it and the realm does not open.

' +\n '' +\n '';\n MOD_REALMS.forEach(function (r) {\n if (!r.n || r.n < MOD_CULT.tribFrom) return;\n h += '' +\n '' +\n '';\n });\n h += '
RealmBoltsOf your max HP
' + MOD_WIKI.safe(r.name) + '' + MOD_tribulationBolts(r.n) + '' + Math.round(MOD_tribulationCost(r.n) * 100) + '%
' +\n\n '

Spiritual Root

' +\n '

Rolled once, when the dojo finishes teaching you to fight. It is never a ' +\n 'dead end: the worst grade costs cultivation speed, not access, and a ' +\n 'Root Cleansing Pill moves you up a grade.

' +\n '' +\n '';\n var rootTotal = 0;\n MOD_ROOTS.forEach(function (r) { rootTotal += r.weight; });\n MOD_ROOTS.forEach(function (r) {\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
GradeRootChanceCultivationBreakthrough
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + Math.round(r.weight / rootTotal * 100) + '%×' + r.xp + '' + (r.odds >= 0 ? '+' : '') +\n Math.round(r.odds * 100) + '%
';\n return h;\n});\n\n/* --- 5. elemental mastery ------------------------------------------------- */\nMOD_wikiPage('mastery', 'Elemental mastery', function () {\n var h = '

Elemental mastery mod

' +\n '

Six masteries, each with a technique that fires on its own ' +\n 'during a fight. Combat is automatic and there is no ability picker, so a ' +\n 'technique is a chance on each of your swings to become something else — one ' +\n 'that scales with INT and the element rather than with your weapon.

' +\n '

Nothing fires while you are still Mortal: the channels have to be open ' +\n 'first. Chance is ' + (MOD_TECH.perLevel * 100).toFixed(1) + '% per mastery ' +\n 'level plus ' + (MOD_TECH.perRealm * 100).toFixed(1) + '% per realm, capped at ' +\n Math.round(MOD_TECH.cap * 100) + '%; a technique lands for ×' +\n MOD_TECH.power + ' of a normal strike.

' +\n '' +\n '';\n\n var realm = 0;\n try { realm = MOD_realm().n; } catch (e) {}\n MOD_MASTERY.forEach(function (m) {\n var lvl = Number(m.skill.lvl) || 0;\n var ch = realm < 1 ? 0\n : Math.min(lvl * MOD_TECH.perLevel + realm * MOD_TECH.perRealm, MOD_TECH.cap);\n h += '' +\n '' +\n '' +\n '';\n });\n h += '
MasteryTechniqueLevelChance now
' +\n MOD_WIKI.safe(m.skill.name) + '' + MOD_WIKI.safe(m.name) + '' + lvl + '' + (realm < 1 ? 'Mortal'\n : (ch * 100).toFixed(1) + '%') + '
';\n return h;\n});\n\n/* --- 6. titles ------------------------------------------------------------ */\nMOD_wikiPage('titles', 'Titles', function () {\n var perm = MOD_permanentRank();\n var held = MOD_titleCount();\n var total = 0; for (var kk in ttl) if (ttl[kk] && ttl[kk].name) total++;\n\n var h = '

Titles

' +\n '

A title\\'s rank runs 1 to 10 and is derived from the level ' +\n 'that earns it, so a rank is a statement about where in the game you are. ' +\n 'Only the title you are wearing applies — until renown makes the lower ranks ' +\n 'permanent.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '
Titles held' + held + ' of ' + total + '
Renown level' + (skl.rnwn ? skl.rnwn.lvl : 0) +\n ' of ' + MOD_RENOWN_MAX + '
Ranks that apply without wearing' +\n (perm >= 1 ? '1–' + perm : 'none yet') + '
' +\n\n '

Ranks

' +\n '';\n MOD_RANK_AT.forEach(function (lv, i) {\n var st = MOD_rankStyle(i + 1);\n h += '' +\n '';\n });\n h += '
RankEarned from levelColour
' + (i + 1) + '' + lv + '' + '●●●' + '
' +\n\n '

Renown

' +\n '

Renown is not trained. It counts the titles you hold, and each level ' +\n 'multiplies every stat by ' + (1 + MOD_RENOWN_RATE) + ' — compounding, so ' +\n 'level ' + MOD_RENOWN_MAX + ' is ×' +\n (Math.round(MOD_renownMult(MOD_RENOWN_MAX) * 1000) / 1000) + '. At renown N, ' +\n 'every title of rank N or below applies permanently.

' +\n '' +\n '';\n MOD_RENOWN_STEPS.forEach(function (n, i) {\n h += '= n ? 'done' : '') + '\">' +\n '';\n });\n h += '
RenownTitles neededAll statsPassive ranks
' + (i + 1) +\n '' + n + '×' +\n (Math.round(MOD_renownMult(i + 1) * 1000) / 1000) + '1–' + (i + 1) + '
' +\n\n '

Every title

' +\n '

Grouped by the skill that grants it. What is left is either ' +\n 'a ladder of its own — creatures killed, items gathered, the ' +\n 'cultivation realms — or a one-off from the story.

';\n\n var groups = {}, fams = {}, loose = [];\n for (var k in ttl) {\n var t = ttl[k];\n /* Unlike the picker, the wiki KEEPS the author's broken entries and says\n so — same call as area.clg, which is listed and tagged \"nothing spawns\"\n rather than quietly dropped. A reader looking for why a title never\n appears is better served by the row than by its absence. */\n if (!t || t.name === undefined || t.name === null) continue;\n var sk = MOD_TITLE_SKILL[k];\n if (sk && skl[sk]) { (groups[sk] = groups[sk] || []).push(k); continue; }\n var stem = MOD_titleStem(k);\n if (stem) { (fams[stem] = fams[stem] || []).push(k); continue; }\n loose.push(k);\n }\n /* Same rule as the picker: a family of one is not a family. */\n Object.keys(fams).forEach(function (stem) {\n if (fams[stem].length < 2) { loose.push(fams[stem][0]); delete fams[stem]; }\n });\n var order = Object.keys(groups).sort(function (a, b) {\n return skl[a].name < skl[b].name ? -1 : 1;\n });\n\n var row = function (k) {\n var t = ttl[k];\n var st = MOD_rankStyle(t.rar);\n var gen = null;\n for (var i = 0; i < MOD_SKILL_TITLES.length; i++) {\n if (MOD_SKILL_TITLES[i].key === k) { gen = MOD_SKILL_TITLES[i]; break; }\n }\n /* The author left a handful unfinished: ttl.ddcd carries the literal string\n \"null\" for both name and description, and shpt2/shpt3/mone3 are blank.\n None is granted by anything, so none can be earned. */\n var broken = !t.name || t.name === 'null';\n return '
' +\n '' +\n MOD_WIKI.safe(t.name || '(unnamed)') + '' +\n (broken ? 'unfinished' : '') +\n 'rank ' + (t.rar || 1) + '' +\n (gen ? 'lv ' + gen.rung + ' · +' +\n Math.round(gen.xp * 100) + '% ' + MOD_WIKI.safe(skl[gen.skill].name) +\n ' exp' : '') +\n (t.have === true ? 'held' : '') +\n '
' + MOD_wikiDesc(t.desc) + '
';\n };\n\n var byRank = function (a, b) { return (ttl[a].rar || 1) - (ttl[b].rar || 1); };\n order.forEach(function (sk) {\n groups[sk].sort(byRank);\n h += MOD_wikiGroup(MOD_WIKI.safe(skl[sk].bname || skl[sk].name),\n groups[sk].length, groups[sk].map(row).join(''), 3);\n });\n /* The ladders that belong to no skill, by the same derivation the picker\n uses — so the two cannot drift apart. Sorted by the label, since that is\n what the reader scans. */\n var famList = Object.keys(fams)\n .map(function (stem) {\n fams[stem].sort(byRank);\n return { stem: stem,\n label: MOD_titleFamilyLabel(stem, fams[stem].map(function (k) { return ttl[k]; })) };\n })\n .sort(function (a, b) { return a.label < b.label ? -1 : a.label > b.label ? 1 : 0; });\n if (famList.length) {\n /* Nested one level down rather than sitting beside the skills. Eleven more\n top-level summaries cost ~530px on a page that is already the tallest in\n the wiki, and these are a different KIND of thing from a skill anyway. */\n var famInner = '', famTotal = 0;\n famList.forEach(function (f) {\n famTotal += fams[f.stem].length;\n famInner += MOD_wikiGroup(MOD_WIKI.safe(f.label), fams[f.stem].length,\n fams[f.stem].map(row).join(''), 4);\n });\n h += MOD_wikiGroup('Ladders of their own', famTotal, famInner, 3);\n }\n if (loose.length) {\n loose.sort(byRank);\n /* The story titles are the one group with no skill to divide them, and\n there are enough of them to need dividing. Rank rather than the alphabet:\n it is the axis they are already sorted on, and the one a reader cares\n about — a rank is a statement about where in the game a title comes from. */\n var inner;\n if (loose.length <= MOD_WIKI_ALPHA_AT) {\n inner = loose.map(row).join('');\n } else {\n var byRankNo = {};\n loose.forEach(function (k) {\n var r = Math.min(Math.max(Number(ttl[k].rar) || 1, 1), 10);\n (byRankNo[r] = byRankNo[r] || []).push(k);\n });\n inner = '';\n Object.keys(byRankNo).sort(function (a, b) { return a - b; }).forEach(function (r) {\n var st = MOD_rankStyle(r);\n inner += MOD_wikiGroup('Rank ' + r + '', byRankNo[r].length,\n byRankNo[r].map(row).join(''), 4);\n });\n }\n h += MOD_wikiGroup('From the story', loose.length, inner, 3);\n }\n return h;\n});\n\n/* --- 7. areas ------------------------------------------------------------- */\nMOD_wikiPage('areas', 'Areas & enemies', function () {\n var h = '

Areas & enemies

' +\n '

Every area, what lives in it and at what levels, and what it ' +\n 'drops. Enemy strength is not read off these levels — see the note below the ' +\n 'table.

';\n\n /* Two of the game's 21 areas are not places and listing them misleads:\n nwh \"Somewhere\" — where global.current_z parks whenever you are NOT in\n a fight, populated by creature.default (\"Nothing\").\n tst \"Test\" — reached only from chss.tst, which has id -1 and is in no\n sector, so nothing in the game can travel there.\n The test for \"is this a place\" is whether you can get to it, and neither\n can be. Everything else is listed whether or not you have been. */\n var MOD_WIKI_NOT_PLACES = { nwh: 1, tst: 1 };\n\n var keys = [];\n for (var k in area) {\n if (area[k] && area[k].pop && !MOD_WIKI_NOT_PLACES[k]) keys.push(k);\n }\n /* Ordered by the middle of the level band, which is the closest thing the\n game has to a difficulty order. */\n keys.sort(function (a, b) {\n var mid = function (z) {\n if (!z.pop.length) return 0;\n var s = 0; z.pop.forEach(function (e) { s += (e.lvlmin + e.lvlmax) / 2; });\n return s / z.pop.length;\n };\n return mid(area[a]) - mid(area[b]);\n });\n\n keys.forEach(function (k) {\n var z = area[k];\n /* An area whose baked bands are not finite can never spawn anything — see\n the note rendered under its table. Derived rather than listed by name, so\n it stays true if the game changes. */\n var broken = !z.popc || z.popc.length !== z.pop.length ||\n z.popc.some(function (band) { return !band || !isFinite(band[1] - band[0]); });\n h += '

' + MOD_WIKI.safe(z.name) +\n ' ' + k + '' +\n (broken ? 'nothing spawns' : '') +\n '' + (z.size === -1 ? 'endless' : z.size + ' to clear') +\n '

';\n if (z.pop.length) {\n /* The spawn share is NOT pop[i].c. z_bake normalises those weights into\n area.popc — a list of [lo,hi] bands that mon_gen rolls a uniform\n random against — and the weights do not have to sum to 1, so they are\n off wherever they don't. The Southern forest's .35/.45/.25 are really\n 33/43/24%. Read the bands the game actually rolls against. */\n var bands = z.popc || [];\n h += '';\n z.pop.forEach(function (e, i) {\n var c = e.crt || {};\n var band = bands[i];\n var share = band ? band[1] - band[0] : NaN;\n h += '' +\n '' +\n '';\n });\n h += '
' + MOD_WIKI.safe(c.name || '?') + 'lv ' + e.lvlmin +\n (e.lvlmax !== e.lvlmin ? '–' + e.lvlmax : '') + '' + (isFinite(share)\n ? Math.round(share * 100) + '% of spawns'\n : '—') + '
';\n if (broken) {\n h += '
Nothing spawns here. The area\\'s entries carry ' +\n 'no spawn weight, so the table the game rolls against comes out as ' +\n 'NaN and no creature can ever match it. A bug in the base game, left ' +\n 'as it is — nothing in the game travels to this area either.
';\n }\n }\n if (z.drop && z.drop.length) {\n var d = z.drop.map(function (x) {\n return MOD_WIKI.safe((x.item && x.item.name) || '?') +\n ' ' + ((x.c || 0) * 100).toFixed(2) + '%';\n }).join(', ');\n h += '
Drops: ' + d + '
';\n }\n h += '
';\n });\n\n var fightsOrig = MOD_FIGHTS.mode === 'original';\n h += '

How hard an enemy is

' +\n '

It depends on the Fights setting, which is ' +\n (fightsOrig ? 'Original' : 'Scaled') + ' now. On Original (the default) ' +\n 'every area the author wrote is exactly as he wrote it: his creatures, his ' +\n 'level ranges, and you out-grow them as you do in his game. On Scaled every ' +\n 'area is sized to you. The three areas past the golem arena and the rank ' +\n 'duels are sized to you either way.

' +\n '

Sizing to you works like this. Enemies are not fitted to a level. Each spawn is solved against your ' +\n 'actual power at the moment it appears, aiming at roughly ' + MOD_ENEMY.kill +\n ' swings to kill it and ' + MOD_ENEMY.die + ' before it kills you, with a ' +\n 'guaranteed margin between the two so a fight is never a coin flip. Variety ' +\n 'comes from the area\\'s level band and each creature\\'s own stat weighting, ' +\n 'not from difficulty scaling.

' +\n '

The reason for that: within the first stage alone you go from 1 STR to ' +\n 'around 265, so anything anchored to a level is either trivial at the end of ' +\n 'a stage or impossible at the start of one.

';\n return h;\n});\n\n/* --- 8. items -------------------------------------------------------------\n The one place where \"list everything\" needs help: 321 of the game's 371\n plain items are the same inventory type, so grouping by type alone leaves a\n single group of 321. Anything past MOD_WIKI_ALPHA_AT gets an alphabetical\n sub-split as well — the oldest index pattern there is, and derived rather\n than invented, which a hand-made taxonomy would not be.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_ALPHA_AT = 60;\n\n/* The game's own inventory tabs (isort): ALL / WPN / EQP / USE / OTHER. Using\n its categories rather than new ones means a group here is a tab there. */\nvar MOD_WIKI_STYPE = { 2: 'Weapons', 3: 'Equipment', 4: 'Usable', 5: 'Other' };\n\nfunction MOD_wikiItemRow(it) {\n var val = it.v !== undefined ? it.v : (it.val !== undefined ? it.val : null);\n return '
' +\n '

' + MOD_WIKI.safe(it.name) +\n (it.rar > 1 ? 'rarity ' + it.rar + '' : '') +\n (val !== null ? '' + MOD_wikiNum(val) + ' coin' : '') +\n (it.amount > 0 ? '×' + it.amount + '' : '') +\n '

' + MOD_wikiDesc(it.desc) + '
';\n}\n\n/* The letter a name files under. Several items are quoted — the master's\n manuals are literally named \"Sword Saint Manual\" — and filing those under a\n quotation mark is no use to anyone looking for S. */\nfunction MOD_wikiLetter(name) {\n var m = String(name || '').replace(/^[^0-9A-Za-z]+/, '');\n return (m.charAt(0) || '?').toUpperCase();\n}\n\nfunction MOD_wikiByLetter(a, b) {\n var x = MOD_wikiLetter(a.name) + String(a.name || '').toLowerCase();\n var y = MOD_wikiLetter(b.name) + String(b.name || '').toLowerCase();\n return x < y ? -1 : (x > y ? 1 : 0);\n}\n\n/* Flat below the threshold, alphabetically sub-grouped above it. Ranges are cut\n from the entries actually present, so they stay even as content is added. */\nfunction MOD_wikiAlpha(entries, render, level) {\n if (entries.length <= MOD_WIKI_ALPHA_AT) return entries.map(render).join('');\n var per = Math.ceil(entries.length / Math.ceil(entries.length / 40));\n var out = '';\n for (var i = 0; i < entries.length; i += per) {\n var chunk = entries.slice(i, i + per);\n var first = MOD_wikiLetter(chunk[0].name);\n var last = MOD_wikiLetter(chunk[chunk.length - 1].name);\n out += MOD_wikiGroup(first === last ? first : first + '–' + last,\n chunk.length, chunk.map(render).join(''), level || 4);\n }\n return out;\n}\n\nMOD_wikiPage('items', 'Items', function () {\n var h = '

Items

' +\n '

Everything the game defines, whether or not you have seen ' +\n 'it. Grouped the way your inventory groups it. Values are the base sell ' +\n 'price where the game gives one.

';\n\n /* item[] first, split by the inventory tab it appears under. */\n var buckets = {};\n for (var k in item) {\n var it = item[k];\n if (!it || !it.name || it.name === 'dummy') continue;\n var lab = MOD_WIKI_STYPE[it.stype] || 'Uncategorised';\n (buckets[lab] = buckets[lab] || []).push(it);\n }\n ['Usable', 'Other', 'Weapons', 'Equipment', 'Uncategorised'].forEach(function (lab) {\n var list = buckets[lab];\n if (!list || !list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(lab, list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n\n /* and the equipment objects, which live in their own globals */\n [['Weapons', wpn], ['Armour', eqp], ['Shields', sld], ['Accessories', acc]]\n .forEach(function (g) {\n var list = [];\n for (var kk in g[1]) {\n if (g[1][kk] && g[1][kk].name && g[1][kk].name !== 'dummy') list.push(g[1][kk]);\n }\n if (!list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(g[0], list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n return h;\n});\n\n/* --- 9. actions ----------------------------------------------------------- */\nMOD_wikiPage('actions', 'Actions', function () {\n var unlock = {};\n MOD_ACTION_UNLOCKS.forEach(function (u) {\n if (u.act) unlock[u.act.id] = (skl[u.skill] ? skl[u.skill].name : u.skill) +\n ' level ' + u.lv;\n });\n if (typeof act.mod_qi !== 'undefined') {\n unlock[act.mod_qi.id] = 'clearing the dojo\\'s Easiest, Easy and Normal fights';\n }\n\n var h = '

Actions

' +\n '

What you can be doing. One at a time, and only where the ' +\n 'action makes sense — unless \"Unrestricted actions\" is ticked in settings, ' +\n 'which lets several run at once anywhere. Both limits are deliberate; the ' +\n 'switch is there because they are not to everyone\\'s taste.

';\n\n var keys = [];\n for (var k in act) if (act[k] && act[k].name && act[k].name !== 'dummy') keys.push(k);\n keys.sort(function (a, b) { return act[a].name < act[b].name ? -1 : 1; });\n\n keys.forEach(function (k) {\n var a = act[k];\n var how = unlock[a.id];\n h += '
' +\n '

' + MOD_WIKI.safe(a.name) +\n (a.have === true ? 'unlocked'\n : 'locked') +\n (how ? 'from ' + MOD_WIKI.safe(how) + '' : '') +\n '

' + MOD_wikiDesc(a.desc) + '
';\n });\n return h;\n});\n\n/* --- 10. the dojo --------------------------------------------------------- */\nMOD_wikiPage('dojo', 'The dojo', function () {\n var h = '

The dojo

' +\n '

The instructor promises a reward every five levels. The base ' +\n 'game stops delivering at 30; the mod carries the same ladder to 110, on the ' +\n 'same terms.

' +\n '

Only the lowest rung you have not claimed is offered, which is how the ' +\n 'first six behave. Rewards are sized against the character experience curve ' +\n '(4×level3 + level2) — a level at 110 costs 5.3 ' +\n 'million, which is why the base game\\'s best pill is worth 0.3% of one there.

' +\n '' +\n '';\n\n MOD_DOJO_RUNGS.forEach(function (r) {\n var pill = item[r.pill];\n var extra = [];\n if (r.realmPill) extra.push('breakthrough pill for realm ' + r.realmPill);\n if (r.manual) extra.push('a choice of master\\'s manual');\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
LevelRewardCoinAlso
' + r.lv + '' + (pill ? MOD_WIKI.safe(pill.name) + ' ×' + r.n\n : '—') + '' + MOD_wikiNum(r.coin) + '' + (extra.join(', ') || '—') + '' + (global.flags[r.flag] ? 'claimed' : '') +\n '
' +\n\n '

Spirit pills

' +\n '

Character experience in a bottle. The four the mod adds continue the ' +\n 'game\\'s own series up to the new curve.

' +\n '' +\n '';\n MOD_PILLS.forEach(function (p) {\n h += '';\n });\n h += '
PillExperience
' + MOD_WIKI.safe(p[2]) + '' +\n p[3].toLocaleString() + '
' +\n\n '

Master\\'s manuals

' +\n '

A third grade of skillbook, offered at levels 50, 75 and 100 as a choice ' +\n 'the same way the first one was. Each is worth +' +\n Math.round(MOD_MANUAL_RATE * 100) + '% experience in its skill, permanently.

' +\n '' +\n '';\n MOD_MANUALS.forEach(function (m) {\n h += '';\n });\n h += '
ManualSkill
' + MOD_WIKI.safe(m[2]) + '' +\n MOD_WIKI.safe(skl[m[0]] ? (skl[m[0]].bname || skl[m[0]].name) : m[0]) +\n '
';\n return h;\n});\n\n/* --- 11. mechanics -------------------------------------------------------- */\nMOD_wikiPage('mechanics', 'Mechanics', function () {\n return '

Mechanics

' +\n\n '

Damage is subtractive, in both directions

' +\n '

A hit does the attacker\\'s strength minus the defender\\'s strength, plus ' +\n 'the weapon, plus one, before criticals. A defender\\'s STR is their ' +\n 'armour — there is no separate armour stat doing that job.

' +\n '

Two consequences worth carrying around:

' +\n '
    ' +\n '
  • Damage does not taper, it stops. Once a defender\\'s STR passes ' +\n 'yours, your hits do not get small; they land for nothing. Progress against ' +\n 'something too strong is not slow, it is zero.
  • ' +\n '
  • A stat ratio tells you nothing. Comparing your HP to an enemy\\'s ' +\n 'strength ignores the subtraction entirely and reads several times off in ' +\n 'either direction.
  • ' +\n '
' +\n '

This is also the reason enemies are never scaled by raising ' +\n 'their strength: it would raise their offence and their armour together, and ' +\n 'the second effect arrives first.

' +\n\n '

Criticals carry the late game

' +\n '

Critical rate reaches about a third at the top of the ladder and a ' +\n 'critical is roughly eight times a normal swing, so around three quarters of ' +\n 'all damage done at that point is critical damage. Anything that raises ' +\n 'critical rate or critical damage is worth more than it looks; Sharp Eye and ' +\n 'War are the two that do.

' +\n\n '

Hit chance

' +\n '

Landing a hit is a contest of agility. It is a ratio rather than a ' +\n 'subtraction, so agility behaves more gently than strength does — falling ' +\n 'behind on it costs you accuracy, not everything.

' +\n\n '

Skill sections and parents

' +\n '

Each of the ' + Object.keys(MOD_SECTIONS).length + ' sections has a parent ' +\n 'skill. A parent earns ' + Math.round(MOD_PARENT.feed * 100) + '% of everything ' +\n 'its children earn and grants no stats of its own. In return, a child below ' +\n 'the parent\\'s level trains ' + Math.round(MOD_PARENT.perLevel * 100) +\n '% faster per level of lag, up to ' + MOD_PARENT.maxGap + ' levels behind. ' +\n 'Bringing one section up makes every skill in it cheaper to catch up.

' +\n\n '

Saving

' +\n '

Three slots, switched from the bottom bar. The game builds its world once ' +\n 'at startup and cannot unload a save, so switching slots or starting a new ' +\n 'game reloads the page. Exporting a save before running anything unusual is ' +\n 'always the safe move.

';\n});\n\n/* --- the document ---------------------------------------------------------\n One string, no fetches, no dependencies: this has to work from file:// as\n readily as from a server.\n ------------------------------------------------------------------------- */\n\nfunction MOD_wikiCss() {\n return [\n ':root{color-scheme:dark}',\n '*{box-sizing:border-box}',\n /* Menlo and Consolas before the generic: with MS Gothic missing (any Mac)\n \"monospace\" is Courier, which Safari sets wider than Chrome does, and\n table cells wrapped on one and not the other. */\n 'body{margin:0;background:#060a1c;color:#dfe6f5;font:14px/1.6 \"MS Gothic\",' +\n '\"MS Mincho\",Menlo,Consolas,\"DejaVu Sans Mono\",monospace;display:flex;min-height:100vh}',\n 'a{color:#7cb0ff}',\n 'nav{width:210px;flex:0 0 210px;background:#081040;border-right:1px solid #46a;' +\n 'position:sticky;top:0;height:100vh;overflow:auto;padding:14px 0}',\n 'nav h2{font-size:13px;color:#9fb4d8;margin:0 0 10px 14px;letter-spacing:1px}',\n 'nav a{display:block;padding:6px 14px;text-decoration:none;color:#cfe0ff;' +\n 'border-left:3px solid transparent}',\n 'nav a:hover{background:#154080}',\n 'nav a.on{background:#123070;border-left-color:#7cb0ff;color:#fff}',\n 'nav .srch{margin:0 10px 12px;width:calc(100% - 20px);background:#050a20;' +\n 'color:#dfe6f5;border:1px solid #46a;padding:5px;font:inherit}',\n 'nav .xall{display:block;margin:12px 10px 0;color:#8296b8;font-size:12px;' +\n 'cursor:pointer;-webkit-user-select:none;user-select:none}',\n 'nav .xall input{vertical-align:-1px;margin-right:4px}',\n 'details.wk-g{border:1px solid #17224a;background:#0a1130;margin:8px 0}',\n 'details.wk-g>summary{cursor:pointer;padding:7px 12px;list-style:none;' +\n 'background:#0d1740}',\n 'details.wk-g>summary:hover{background:#154080}',\n 'details.wk-g>summary::-webkit-details-marker{display:none}',\n // a caret drawn rather than borrowed, so it is the same in every browser\n 'details.wk-g>summary::before{content:\"\\\\25B8\";color:#7cb0ff;' +\n 'display:inline-block;width:1em;transition:transform .12s}',\n 'details.wk-g[open]>summary::before{transform:rotate(90deg)}',\n 'details.wk-g>summary h2,details.wk-g>summary h3{display:inline;margin:0;' +\n 'font-size:15px;color:#fff}',\n '.wk-gi{padding:2px 12px 10px}',\n 'details.wk-g details.wk-g>summary h4{display:inline;margin:0;font-size:13px;' +\n 'color:#cfe0ff;font-weight:normal}',\n 'details.wk-g details.wk-g>summary{padding:5px 10px;background:#0b1435}',\n 'main{flex:1;min-width:0;padding:22px 30px 80px;max-width:1000px}',\n 'h1{font-size:26px;margin:0 0 6px;color:#fff;border-bottom:1px solid #46a;' +\n 'padding-bottom:8px}',\n 'h2{font-size:19px;color:#bcd4ff;margin:30px 0 8px}',\n 'h3{font-size:15px;color:#fff;margin:0 0 4px}',\n 'p.lede{color:#a8bcdc;margin:0 0 18px}',\n '.dim{color:#8296b8;font-weight:normal}',\n '.flav{font-size:12px;font-style:italic}',\n '.note{background:#0b1638;border-left:3px solid #46a;padding:8px 12px;' +\n 'color:#a8bcdc;margin:14px 0}',\n 'code,pre{background:#0b1638;border:1px solid #24356e;padding:1px 5px;' +\n 'color:#9fd0ff}',\n 'pre{padding:10px;overflow-x:auto}',\n 'dl dt{color:#fff;margin-top:14px}',\n 'dl dd{margin:2px 0 0 0;color:#a8bcdc}',\n 'ul{color:#a8bcdc}',\n 'table.wk-tbl{border-collapse:collapse;width:100%;margin:10px 0;' +\n 'display:block;overflow-x:auto}',\n 'table.wk-tbl th{text-align:left;color:#9fb4d8;font-weight:normal;' +\n 'border-bottom:1px solid #46a;padding:5px 9px;white-space:nowrap}',\n 'table.wk-tbl td{border-bottom:1px solid #17224a;padding:5px 9px;' +\n 'vertical-align:top}',\n 'table.wk-tbl tr.done td{color:#cfe0ff}',\n 'table.wk-tbl tr.here{background:#123070}',\n '.num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}',\n '.wk-e{border:1px solid #17224a;background:#081040;padding:9px 12px;' +\n 'margin:7px 0;border-radius:2px}',\n 'tr.wk-e{border:0;background:none}',\n '.wk-e.done{border-color:#2a4a8c}',\n '.wk-e h3{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}',\n '.lv{margin-left:auto;font-size:12px;color:#8296b8;font-weight:normal}',\n '.lv.capped{color:#ffd24a}',\n '.rank{font-size:12px;color:#8296b8;font-weight:normal}',\n '.desc{color:#a8bcdc;font-size:13px}',\n '.proj{color:#ffd24a;font-size:12px;margin-top:3px}',\n '.tag{font-size:11px;border:1px solid #46a;color:#9fb4d8;padding:0 5px;' +\n 'border-radius:2px;font-weight:normal}',\n '.tag.mod{border-color:#7a4ea8;color:#c07bff}',\n '.tag.done{border-color:#2e7d4f;color:#6fdc9a}',\n '.tag.here{border-color:#ffd24a;color:#ffd24a}',\n '.tag.warn{border-color:#a8562e;color:#ff9a5c}',\n 'table.wk-perks{border-collapse:collapse;margin:6px 0 0;width:100%}',\n 'table.wk-perks td{padding:2px 8px 2px 0;font-size:12px;color:#8296b8;' +\n 'border-bottom:1px solid #101a3c}',\n 'table.wk-perks tr.done td{color:#cfe0ff}',\n 'table.wk-perks td.tick{color:#6fdc9a;width:1em;text-align:right}',\n // fixed columns so the level bands line up between one area and the next\n 'table.wk-pop{table-layout:fixed}',\n 'table.wk-pop td:first-child{width:60%}',\n 'table.wk-pop td:nth-child(2){width:20%}',\n '.wk-title .tname{font-weight:bold}',\n '.wk-now{background:#0b1638;border:1px solid #24356e;padding:10px 14px;' +\n 'margin:0 0 18px;display:inline-block;min-width:320px}',\n '.wk-now h3{color:#9fb4d8;font-weight:normal;font-size:13px;' +\n 'letter-spacing:1px;margin-bottom:6px}',\n '.wk-now td{padding:2px 16px 2px 0}',\n '.wk-now td:last-child{color:#fff;text-align:right}',\n '.pg{display:none}.pg.on{display:block}',\n 'footer{color:#5a6c8c;font-size:12px;border-top:1px solid #17224a;' +\n 'margin-top:40px;padding-top:12px}',\n '@media(max-width:760px){body{display:block}nav{width:auto;height:auto;' +\n 'position:static;flex:none}main{padding:16px}}'\n ].join('\\n');\n}\n\n/* Filtering, page switching and the hash. Small enough to inline, and inlining\n it is the point — the document has to stand on its own. */\nfunction MOD_wikiJs() {\n return [\n 'var pages=[].slice.call(document.querySelectorAll(\".pg\"));',\n 'var links=[].slice.call(document.querySelectorAll(\"nav a[data-p]\"));',\n 'function show(id){',\n ' var found=false;',\n ' pages.forEach(function(p){var on=p.id===\"pg-\"+id;if(on)found=true;',\n ' p.className=\"pg\"+(on?\" on\":\"\")});',\n ' if(!found&&pages.length){pages[0].className=\"pg on\";id=pages[0].id.slice(3)}',\n ' links.forEach(function(a){a.className=a.dataset.p===id?\"on\":\"\"});',\n ' window.scrollTo(0,0);return id;}',\n 'links.forEach(function(a){a.addEventListener(\"click\",function(e){',\n ' e.preventDefault();location.hash=a.dataset.p;show(a.dataset.p)})});',\n 'window.addEventListener(\"hashchange\",function(){show(location.hash.slice(1))});',\n 'show(location.hash.slice(1)||\"start\");',\n // Search filters entries, hides groups left with none, and OPENS the groups\n // that still have some — a match hidden inside a collapsed group reads as\n // no match at all, which is worse than not having search.\n 'var box=document.getElementById(\"srch\");',\n 'var xall=document.getElementById(\"xall\");',\n 'var groups=[].slice.call(document.querySelectorAll(\"details.wk-g\"));',\n 'function filter(){',\n ' var q=box.value.trim().toLowerCase();',\n ' pages.forEach(function(p){',\n ' [].slice.call(p.querySelectorAll(\".wk-e\")).forEach(function(e){',\n ' e.style.display=(!q||e.textContent.toLowerCase().indexOf(q)>=0)?\"\":\"none\"})});',\n ' groups.forEach(function(g){',\n ' var any=[].slice.call(g.querySelectorAll(\".wk-e\")).some(function(e){',\n ' return e.style.display!==\"none\"});',\n ' g.style.display=any?\"\":\"none\";',\n ' g.open=xall.checked||(!!q&&any)}); }',\n 'box.addEventListener(\"input\",filter);',\n 'box.addEventListener(\"keydown\",function(e){if(e.key===\"Escape\"){box.value=\"\";filter()}});',\n // Everything is collapsed by default because three of these pages run past\n // 40,000px open. This is the way back to one long document, which is what\n // the browser's own find needs.\n 'xall.addEventListener(\"change\",filter);'\n ].join('\\n');\n}\n\nfunction MOD_wikiBuild() {\n var nav = '', body = '';\n MOD_WIKI_PAGES.forEach(function (pg) {\n nav += '' +\n MOD_WIKI.safe(pg.label) + '';\n var inner;\n /* One page throwing must not take the wiki down with it — the page says so\n and the rest still renders. */\n try { inner = pg.build(); }\n catch (e) {\n inner = '

' + MOD_WIKI.safe(pg.label) + '

This page ' +\n 'could not be built: ' + MOD_WIKI.safe(e.message) + '

';\n console.warn('[mod] wiki page \"' + pg.id + '\" failed: ' + e.message);\n }\n body += '
' + inner + '
';\n });\n\n var stamp = new Date().toLocaleString();\n return '' +\n '' +\n '' + MOD_WIKI.safe(MOD_WIKI.title) + '' +\n '' +\n '' +\n '
' + body +\n '
Generated from the running game at ' + MOD_WIKI.safe(stamp) +\n ' — mod ' + MOD_WIKI.safe(MOD.version) + '. Everything here is read from the ' +\n 'live data, including your own save, so it cannot fall out of step with the ' +\n 'game. The game is truezhangwei\\'s; the mod is not endorsed by the author.' +\n '
' +\n ' inside content closing the block early, so that is the\n one thing scrubbed. */\n safe: function (h) {\n return String(h === undefined || h === null ? '' : h).replace(/<\\/script/gi, '<\\\\/script');\n }\n};\n\n/* Descriptions carry real HTML (coloured spans, the game's separator div), so\n they are passed through rather than escaped — but the separator becomes a\n rule, since a wiki row is not a tooltip. */\nfunction MOD_wikiDesc(d) {\n var s;\n try { s = (typeof d === 'function') ? d() : d; } catch (e) { s = ''; }\n if (!s) return '';\n s = String(s).split('
 
').join('
');\n return MOD_WIKI.safe(s);\n}\n\nfunction MOD_wikiNum(n) {\n if (!isFinite(n)) return '—';\n return Math.round(n).toLocaleString();\n}\n\n/* An effect line at a level the player has not reached yet. The effect\n functions read the skill they are handed, so the level is borrowed and put\n back; anything that throws simply gets no projection. */\nfunction MOD_wikiEffectAt(key, sk, lvl) {\n var eff = MOD_EFFECTS[key];\n if (!eff) return '';\n var was = sk.lvl, out = '';\n try { sk.lvl = lvl; out = eff(sk) || ''; }\n catch (e) { out = ''; }\n finally { sk.lvl = was; }\n return MOD_WIKI.safe(out);\n}\n\n/* --- page builders --------------------------------------------------------\n Each returns the inner HTML of one page. `wk-e` marks a searchable entry and\n `wk-g` a group that hides itself when all its entries are filtered out.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_PAGES = [];\n\nfunction MOD_wikiPage(id, label, build) {\n MOD_WIKI_PAGES.push({ id: id, label: label, build: build });\n}\n\n/* A collapsible group.\n\n Not decoration: with every skill, item and title listed, three of the eleven\n pages ran past 40,000 pixels — a hundred screens of scrolling, which is the\n same as not having the content at all. Collapsed, each of those pages opens\n as an index you can read in one screen and expand where you want.\n\n
rather than a click handler, so it still works with JavaScript off,\n the browser's own find can be made to reach inside it, and the summary is\n keyboard-reachable for free. `level` is the heading level for the summary,\n since a group is sometimes a section of a page and sometimes a subsection. */\nfunction MOD_wikiGroup(heading, count, inner, level) {\n var hn = 'h' + (level || 2);\n return '
<' + hn + '>' + heading +\n (count !== null && count !== undefined\n ? ' (' + count + ')' : '') +\n '
' + inner + '
';\n}\n\n/* --- 1. orientation ------------------------------------------------------- */\nMOD_wikiPage('start', 'Start here', function () {\n var cap = MOD_levelCap();\n var realm = MOD_realm();\n var h = '

proto23

' +\n '

An idle RPG by truezhangwei, plus a mod. You train skills by ' +\n 'doing things, the things you can do open up as the story does, and none of ' +\n 'it is in a hurry.

' +\n\n '

Where you are right now

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Character level' + MOD_wikiNum(you.lvl) + '
Skill level cap' + cap + ' — ' +\n MOD_WIKI.safe(MOD_CAP.name) + '
Cultivation realm' + MOD_WIKI.safe(realm.name) +\n ' (' + realm.n + ' of ' + (MOD_REALMS.length - 1) + ')
Titles held' + MOD_titleCount() + ' — renown ' +\n (skl.rnwn ? skl.rnwn.lvl : 0) + '
Coin' + MOD_wikiNum(you.wealth || 0) + '
' +\n\n '

The four things worth knowing early

' +\n '
' +\n '
Your skill levels are capped by the story, not by grinding.
' +\n '
The cap starts at 10 and ends at 110, and it moves when you reach a new ' +\n 'place — not when you earn enough experience. If a skill has stopped ' +\n 'levelling, you are not doing it wrong; you need to go somewhere. See ' +\n 'Progression.
' +\n\n '
Armour is subtraction, so levels matter more than they look.
' +\n '
A hit does the attacker\\'s STR minus the defender\\'s STR. That means a ' +\n 'small gap in strength is a large gap in damage, and a big enough one takes ' +\n 'your damage to nothing at all rather than merely reducing it. See ' +\n 'Mechanics.
' +\n\n '
Most skills have a parent that pulls them along.
' +\n '
Every section has a parent skill that earns a share of everything its ' +\n 'children do, and any child lagging behind the parent trains faster — up to ' +\n 'twice as fast at ' + MOD_PARENT.maxGap + ' levels behind. A neglected skill is ' +\n 'cheap to bring up later, which is the only reason having this many is ' +\n 'workable.
' +\n\n '
Cultivation is a second ladder, and it is not automatic.
' +\n '
Circulating qi levels a skill; the skill does not raise your realm. ' +\n 'Reaching the level opens a bottleneck, and breaking through it costs a pill ' +\n 'and can fail. See Cultivation.
' +\n '
' +\n\n '

What is the mod and what is the game

' +\n '

The game is index.html and it is entirely truezhangwei\\'s ' +\n 'work. Everything in mod.js is a modification of it and is not ' +\n 'endorsed by the author. Pages here mark added content with ' +\n 'mod where the distinction matters. The full ' +\n 'list of changes is in the changelog, on the bottom bar.

';\n return h;\n});\n\n/* --- 2. progression ------------------------------------------------------- */\nMOD_wikiPage('progress', 'Progression', function () {\n var cap = MOD_levelCap();\n var h = '

Progression

' +\n '

Skill levels are capped by how far the story has gone. Ten ' +\n 'rungs, 10 through 110. The cap applies to every skill at once; Renown is ' +\n 'the single exception, since it is counted rather than trained.

' +\n '' +\n '';\n\n MOD_TIERS.forEach(function (t) {\n var got = null;\n try { got = MOD_tierFlag(t); } catch (e) {}\n var here = (t.cap === cap);\n var need = t.needText ? t.needText\n : (t.flags.length ? t.flags.join(' or ') : 'the start of the game');\n h += '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
CapStageWhat opens it
' + t.cap + '' + MOD_WIKI.safe(t.name) + '' + MOD_WIKI.safe(need) + '' + (here ? 'you are here'\n : got ? 'reached' : '') + '
' +\n\n '

The experience curve

' +\n '

The game\\'s own skill curve is super-exponential — level 109 to 110 ' +\n 'alone costs ' + MOD_wikiNum(MOD_vanillaExpnext(109)) + ' experience, so the top of the ladder ' +\n 'is unreachable by a factor of about ten trillion. But the author only ever ' +\n 'designed skills up to level 15, and below that his curve is fine.

' +\n (MOD_XP.vanillaTo > 0\n ? '

So the mod keeps it. Below level ' + MOD_XP.vanillaTo + ' a skill costs ' +\n 'exactly what the original charges; from there it rises by ×' +\n MOD_XP.ratio.toFixed(4) + ' a level, a ratio solved so the climb to 110 takes ' +\n 'the same time it always did.'\n : '

You have chosen the Mod before 4.2 pacing: a plain geometric curve, ' +\n '×' + MOD_XP.ratio.toFixed(3) + ' a level from level 0, much faster than the ' +\n 'original early on. The Original pacing in settings puts back the ' +\n 'author\\'s own cost below level 10.') +\n ' Character level uses the game\\'s own curve, untouched.

' +\n '' +\n '';\n [1, 5, 9, 10, 15, 25, 50, 75, 90, 109].forEach(function (l) {\n h += '';\n });\n h += '
LevelExperience for the nextOriginal game
' + l +\n '' + MOD_wikiNum(MOD_expnextFor(l)) + '' +\n MOD_wikiNum(MOD_vanillaExpnext(l)) + '
';\n return h;\n});\n\n/* --- 3. skills ------------------------------------------------------------ */\nMOD_wikiPage('skills', 'Skills', function () {\n var cap = MOD_levelCap();\n var bySection = {};\n var keys = [];\n for (var k in skl) {\n var s = skl[k];\n if (!s || typeof s !== 'object' || !s.name) continue;\n keys.push(k);\n }\n keys.forEach(function (k) {\n var s = skl[k];\n var sec = MOD_sectionOf(s);\n (bySection[sec] = bySection[sec] || []).push(k);\n });\n\n var h = '

Skills

' +\n '

' + keys.length + ' skills in ' +\n Object.keys(MOD_SECTIONS).length + ' sections. Every one has perks at ' +\n MOD_LADDER.join(', ') + ', and every one that has milestones grants titles at ' +\n MOD_TITLE_RUNGS.join(', ') + '. Levels and ticks below are yours.

';\n\n Object.keys(MOD_SECTIONS).forEach(function (sec) {\n var list = bySection[sec];\n if (!list || !list.length) return;\n var parentKey = MOD_PARENT_KEY[sec];\n var g = '';\n if (parentKey && skl[parentKey]) {\n g += '

Parent skill: ' +\n MOD_WIKI.safe(skl[parentKey].bname || skl[parentKey].name) +\n ' — earns ' + Math.round(MOD_PARENT.feed * 100) +\n '% of what its children earn, and speeds up any child that lags behind it.

';\n }\n list.sort(function (a, b) { return skl[a].name < skl[b].name ? -1 : 1; });\n list.forEach(function (k) {\n var s = skl[k];\n var lvl = Number(s.lvl) || 0;\n g += '
' +\n '

' + MOD_WIKI.safe(s.bname || s.name) +\n (s.bname ? ' (' + MOD_WIKI.safe(s.name) + ')' : '') +\n '= cap ? ' capped' : '') + '\">lv ' + lvl +\n (lvl >= cap ? ' — at the cap' : '') + '

' +\n '
' + MOD_wikiDesc(s.desc) + '
';\n\n var at110 = MOD_wikiEffectAt(k, s, 110);\n if (at110) g += '
At level 110: ' + at110 + '
';\n\n if (s.mlstn && s.mlstn.length) {\n g += '';\n s.mlstn.forEach(function (m) {\n g += '' +\n '' +\n '' +\n '';\n });\n g += '
' + m.lv + '' + MOD_WIKI.safe(m.p || '—') + '' + (m.g === true ? '✓' : '') + '
';\n }\n g += '
';\n });\n h += MOD_wikiGroup(MOD_WIKI.safe(MOD_SECTIONS[sec]), list.length, g, 2);\n });\n return h;\n});\n\n/* --- 4. cultivation ------------------------------------------------------- */\nMOD_wikiPage('cultivation', 'Cultivation', function () {\n var now = MOD_realm();\n var elig = 0;\n try { elig = MOD_realmEligible().n; } catch (e) {}\n var qic = skl.qic ? skl.qic.lvl : 0;\n\n var h = '

Cultivation

' +\n '

Ten realms above Mortal. Circulating qi levels the Qi ' +\n 'Circulation skill; the skill does not advance the realm. Reaching the level ' +\n 'opens a bottleneck, and breaking through it costs a pill and can fail — the ' +\n 'pill is spent either way.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Realm' + MOD_WIKI.safe(MOD_realmTitle()) + '
Spiritual Root' + MOD_WIKI.safe(MOD_root().name) + '
Qi Circulationlevel ' + qic + '
Insight' + MOD_insight() + '
Bottleneck open' +\n (elig > now.n ? 'yes — ' + MOD_WIKI.safe(MOD_REALMS[now.n + 1].name) +\n ' is within reach' : 'no') + '
Qi Deviation' + (MOD_deviated()\n ? MOD_deviationLeft() + ' in-game minutes left' : 'no') + '
' +\n '

Realm thresholds are the same levels that earn a title rank, so realm N ' +\n 'is the level at which a rank N title arrives. Every realm multiplies all ' +\n 'stats and health, and the multipliers do not stack with each other — the ' +\n 'realm you are in is the one that applies.

' +\n '' +\n '';\n\n MOD_REALMS.forEach(function (r) {\n var pill = r.n >= 1 ? item['mod_bp' + r.n] : null;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmNameQi Circ.StatsHealthPill
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + r.qic + '×' + r.mult + '×' + r.hp + '' + (pill ? MOD_WIKI.safe(pill.name) : '—') +\n '
' +\n\n '

Where the pills come from

' +\n '

Every breakthrough pill has a source in normal play. None of them are ' +\n 'craftable, and none are cheap — a bottleneck you can buy in bulk is not a ' +\n 'bottleneck.

' +\n '' +\n '' +\n '' +\n '' +\n '
RealmsSource
1The dojo instructor, when qi ' +\n 'circulation is first taught
2–5The Herbalist in the village ' +\n '(vendor.pha1), restocked
6–10The dojo\\'s Level Advancement ' +\n 'ladder, and the Pill Tower
' +\n '

Realm 1 is deliberately not sold at the marketplace: ' +\n 'the marketplace is behind a 40%-a-visit chance encounter, and realm 1 opens ' +\n 'long before that is reliable.

' +\n\n '

Breaking through

' +\n '

A bottleneck wants three things, which is what the genre says it wants: ' +\n 'a pill, insight, and enough consolidation to make the ' +\n 'attempt worth making. The pill and the insight are both spent whether the ' +\n 'attempt works or not.

' +\n '

The odds improve the further past the threshold you are, plus your root, ' +\n 'plus time sat in Closed Door Training, and cap short of certain.

' +\n '' +\n '';\n [0, 2, 4, 6, 8].forEach(function (o) {\n var base = Math.min(0.55 + o * 0.05 + MOD_root().odds, 0.95);\n h += '';\n });\n h += '
Levels past the thresholdChanceAfter full seclusion
+' + o + '' +\n Math.round(base * 100) + '%' +\n Math.round(Math.min(base + MOD_CULT.secludeOdds, 0.95) * 100) + '%
' +\n\n '

Insight

' +\n '

Insight is the half of the price that cannot be bought. It comes from ' +\n 'three places, and they are the three the genre names: meditating, fights you ' +\n 'nearly lost, and going somewhere you have not been.

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
SourceRate
Circulate Qi, at a wall' +\n (MOD_CULT.meditateChance * 100).toFixed(1) + '% a second
Closed Door Training' +\n (MOD_CULT.secludeChance * 100).toFixed(1) + '% a second
A kill made under ' +\n Math.round(MOD_CULT.brinkFrac * 100) + '% health — ' +\n (MOD_FIGHTS.mode === 'original'\n ? 'the author\\'s areas rarely come close once you reach them; the three past ' +\n 'the golem arena and the rank duels do, within a few fights, as does an area ' +\n 'you walk into early'\n : 'every fight costs a share of your health, so a few in a row without resting') +\n '' +\n Math.round(MOD_CULT.brinkChance * 100) + '% a kill
Entering an area for the first timealways
' +\n '

The insight a realm asks for is sized to the climb to it: about ' +\n Math.round(MOD_CULT.wallShare * 100) + '% of the Circulate Qi time the climb took, ' +\n 'spent at the wall. Seclusion earns it three times as fast, and full ' +\n 'consolidation takes as long as that on average.

' +\n '' +\n '' +\n '';\n var wkHours = function (sec) {\n return sec >= 7200 ? (sec / 3600).toFixed(sec >= 36000 ? 0 : 1) + ' h' : Math.round(sec / 60) + ' min';\n };\n MOD_REALMS.forEach(function (r) {\n if (!r.n) return;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmClimb (Circulate Qi, 1x)Insight to enterFull seclusionQi Deviation (in-game)
' + MOD_WIKI.safe(r.name) + '' + wkHours(MOD_realmClimbSec(r.n)) + '' + MOD_insightNeed(r.n) + '' + wkHours(MOD_secludeFull(r.n)) + '' + (MOD_devMinutes(r.n) / 1440).toFixed(1) + ' days
' +\n\n '

The nine layers

' +\n '

Each realm is nine layers deep, derived from where Qi Circulation sits ' +\n 'between this realm\\'s threshold and the next one\\'s — so breaking through ' +\n 'puts you at the first layer of the new realm on its own. Layers are worth ' +\n '×' + MOD_CULT.layerMult + ' stats and ×' + MOD_CULT.layerBody +\n ' body each above the first, and the top realm has no rung above it to ' +\n 'measure against, so it stays at its first layer.

' +\n '' +\n '';\n for (var li = 1; li <= MOD_CULT.layers; li++) {\n h += '';\n }\n h += '
LayerCalled
' +\n li + '' + MOD_LAYER_ORD[li] + ' layer, ' +\n MOD_WIKI.safe(MOD_layerStage(li)) + '
' +\n '

At the threshold of the next realm and before breaking ' +\n 'through, you are a half step to it — the genre\\'s own name for the ' +\n 'state this mod calls a bottleneck.

' +\n\n '

Closed Door Training

' +\n '

Seclusion does two things. It consolidates — a full sitting ' +\n '(the table above) is worth +' + Math.round(MOD_CULT.secludeOdds * 100) +\n '% on the attempt — and it shelters: a breakthrough attempted while ' +\n 'you are still sitting in it cannot end in Qi Deviation, only in a lost ' +\n 'pill. It will not start unless you are actually at a wall.

' +\n\n '

Qi Deviation

' +\n '

Failing a breakthrough out in the open leaves your cultivation base ' +\n 'unstable for as long as the seclusion you skipped (the table above, never ' +\n 'under ' + MOD_CULT.devMinutes + ' in-game minutes): stats at ×' +\n MOD_CULT.devStat + ', body at ×' + MOD_CULT.devBody + ', and no second ' +\n 'attempt until it passes. It clears on its own, or at once with a ' +\n 'Qi Settling Pill. It never kills — the death system stays out of it.

' +\n\n '

Heavenly Tribulation

' +\n '

From realm ' + MOD_CULT.tribFrom + ' up, a successful breakthrough is ' +\n 'answered. The cost is a fraction of your maximum health, so it is a gate on ' +\n 'walking in healthy rather than a damage race; the realm bonus you are ' +\n 'standing on is what pays it. Fall to it and the realm does not open.

' +\n '' +\n '';\n MOD_REALMS.forEach(function (r) {\n if (!r.n || r.n < MOD_CULT.tribFrom) return;\n h += '' +\n '' +\n '';\n });\n h += '
RealmBoltsOf your max HP
' + MOD_WIKI.safe(r.name) + '' + MOD_tribulationBolts(r.n) + '' + Math.round(MOD_tribulationCost(r.n) * 100) + '%
' +\n\n '

Spiritual Root

' +\n '

Rolled once, when the dojo finishes teaching you to fight. It is never a ' +\n 'dead end: the worst grade costs cultivation speed, not access, and a ' +\n 'Root Cleansing Pill moves you up a grade.

' +\n '' +\n '';\n var rootTotal = 0;\n MOD_ROOTS.forEach(function (r) { rootTotal += r.weight; });\n MOD_ROOTS.forEach(function (r) {\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
GradeRootChanceCultivationBreakthrough
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + Math.round(r.weight / rootTotal * 100) + '%×' + r.xp + '' + (r.odds >= 0 ? '+' : '') +\n Math.round(r.odds * 100) + '%
';\n return h;\n});\n\n/* --- 5. elemental mastery ------------------------------------------------- */\nMOD_wikiPage('mastery', 'Elemental mastery', function () {\n var h = '

Elemental mastery mod

' +\n '

Six masteries, each with a technique that fires on its own ' +\n 'during a fight. Combat is automatic and there is no ability picker, so a ' +\n 'technique is a chance on each of your swings to become something else — one ' +\n 'that scales with INT and the element rather than with your weapon.

' +\n '

Nothing fires while you are still Mortal: the channels have to be open ' +\n 'first. Chance is ' + (MOD_TECH.perLevel * 100).toFixed(1) + '% per mastery ' +\n 'level plus ' + (MOD_TECH.perRealm * 100).toFixed(1) + '% per realm, capped at ' +\n Math.round(MOD_TECH.cap * 100) + '%; a technique lands for ×' +\n MOD_TECH.power + ' of a normal strike.

' +\n '' +\n '';\n\n var realm = 0;\n try { realm = MOD_realm().n; } catch (e) {}\n MOD_MASTERY.forEach(function (m) {\n var lvl = Number(m.skill.lvl) || 0;\n var ch = realm < 1 ? 0\n : Math.min(lvl * MOD_TECH.perLevel + realm * MOD_TECH.perRealm, MOD_TECH.cap);\n h += '' +\n '' +\n '' +\n '';\n });\n h += '
MasteryTechniqueLevelChance now
' +\n MOD_WIKI.safe(m.skill.name) + '' + MOD_WIKI.safe(m.name) + '' + lvl + '' + (realm < 1 ? 'Mortal'\n : (ch * 100).toFixed(1) + '%') + '
';\n return h;\n});\n\n/* --- 6. titles ------------------------------------------------------------ */\nMOD_wikiPage('titles', 'Titles', function () {\n var perm = MOD_permanentRank();\n var held = MOD_titleCount();\n var total = 0; for (var kk in ttl) if (ttl[kk] && ttl[kk].name) total++;\n\n var h = '

Titles

' +\n '

A title\\'s rank runs 1 to 10 and is derived from the level ' +\n 'that earns it, so a rank is a statement about where in the game you are. ' +\n 'Only the title you are wearing applies — until renown makes the lower ranks ' +\n 'permanent.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '
Titles held' + held + ' of ' + total + '
Renown level' + (skl.rnwn ? skl.rnwn.lvl : 0) +\n ' of ' + MOD_RENOWN_MAX + '
Ranks that apply without wearing' +\n (perm >= 1 ? '1–' + perm : 'none yet') + '
' +\n\n '

Ranks

' +\n '';\n MOD_RANK_AT.forEach(function (lv, i) {\n var st = MOD_rankStyle(i + 1);\n h += '' +\n '';\n });\n h += '
RankEarned from levelColour
' + (i + 1) + '' + lv + '' + '●●●' + '
' +\n\n '

Renown

' +\n '

Renown is not trained. It counts the titles you hold, and each level ' +\n 'multiplies every stat by ' + (1 + MOD_RENOWN_RATE) + ' — compounding, so ' +\n 'level ' + MOD_RENOWN_MAX + ' is ×' +\n (Math.round(MOD_renownMult(MOD_RENOWN_MAX) * 1000) / 1000) + '. At renown N, ' +\n 'every title of rank N or below applies permanently.

' +\n '' +\n '';\n MOD_RENOWN_STEPS.forEach(function (n, i) {\n h += '= n ? 'done' : '') + '\">' +\n '';\n });\n h += '
RenownTitles neededAll statsPassive ranks
' + (i + 1) +\n '' + n + '×' +\n (Math.round(MOD_renownMult(i + 1) * 1000) / 1000) + '1–' + (i + 1) + '
' +\n\n '

Every title

' +\n '

Grouped by the skill that grants it. What is left is either ' +\n 'a ladder of its own — creatures killed, items gathered, the ' +\n 'cultivation realms — or a one-off from the story.

';\n\n var groups = {}, fams = {}, loose = [];\n for (var k in ttl) {\n var t = ttl[k];\n /* Unlike the picker, the wiki KEEPS the author's broken entries and says\n so — same call as area.clg, which is listed and tagged \"nothing spawns\"\n rather than quietly dropped. A reader looking for why a title never\n appears is better served by the row than by its absence. */\n if (!t || t.name === undefined || t.name === null) continue;\n var sk = MOD_TITLE_SKILL[k];\n if (sk && skl[sk]) { (groups[sk] = groups[sk] || []).push(k); continue; }\n var stem = MOD_titleStem(k);\n if (stem) { (fams[stem] = fams[stem] || []).push(k); continue; }\n loose.push(k);\n }\n /* Same rule as the picker: a family of one is not a family. */\n Object.keys(fams).forEach(function (stem) {\n if (fams[stem].length < 2) { loose.push(fams[stem][0]); delete fams[stem]; }\n });\n var order = Object.keys(groups).sort(function (a, b) {\n return skl[a].name < skl[b].name ? -1 : 1;\n });\n\n var row = function (k) {\n var t = ttl[k];\n var st = MOD_rankStyle(t.rar);\n var gen = null;\n for (var i = 0; i < MOD_SKILL_TITLES.length; i++) {\n if (MOD_SKILL_TITLES[i].key === k) { gen = MOD_SKILL_TITLES[i]; break; }\n }\n /* The author left a handful unfinished: ttl.ddcd carries the literal string\n \"null\" for both name and description, and shpt2/shpt3/mone3 are blank.\n None is granted by anything, so none can be earned. */\n var broken = !t.name || t.name === 'null';\n return '
' +\n '' +\n MOD_WIKI.safe(t.name || '(unnamed)') + '' +\n (broken ? 'unfinished' : '') +\n 'rank ' + (t.rar || 1) + '' +\n (gen ? 'lv ' + gen.rung + ' · +' +\n Math.round(gen.xp * 100) + '% ' + MOD_WIKI.safe(skl[gen.skill].name) +\n ' exp' : '') +\n (t.have === true ? 'held' : '') +\n '
' + MOD_wikiDesc(t.desc) + '
';\n };\n\n var byRank = function (a, b) { return (ttl[a].rar || 1) - (ttl[b].rar || 1); };\n order.forEach(function (sk) {\n groups[sk].sort(byRank);\n h += MOD_wikiGroup(MOD_WIKI.safe(skl[sk].bname || skl[sk].name),\n groups[sk].length, groups[sk].map(row).join(''), 3);\n });\n /* The ladders that belong to no skill, by the same derivation the picker\n uses — so the two cannot drift apart. Sorted by the label, since that is\n what the reader scans. */\n var famList = Object.keys(fams)\n .map(function (stem) {\n fams[stem].sort(byRank);\n return { stem: stem,\n label: MOD_titleFamilyLabel(stem, fams[stem].map(function (k) { return ttl[k]; })) };\n })\n .sort(function (a, b) { return a.label < b.label ? -1 : a.label > b.label ? 1 : 0; });\n if (famList.length) {\n /* Nested one level down rather than sitting beside the skills. Eleven more\n top-level summaries cost ~530px on a page that is already the tallest in\n the wiki, and these are a different KIND of thing from a skill anyway. */\n var famInner = '', famTotal = 0;\n famList.forEach(function (f) {\n famTotal += fams[f.stem].length;\n famInner += MOD_wikiGroup(MOD_WIKI.safe(f.label), fams[f.stem].length,\n fams[f.stem].map(row).join(''), 4);\n });\n h += MOD_wikiGroup('Ladders of their own', famTotal, famInner, 3);\n }\n if (loose.length) {\n loose.sort(byRank);\n /* The story titles are the one group with no skill to divide them, and\n there are enough of them to need dividing. Rank rather than the alphabet:\n it is the axis they are already sorted on, and the one a reader cares\n about — a rank is a statement about where in the game a title comes from. */\n var inner;\n if (loose.length <= MOD_WIKI_ALPHA_AT) {\n inner = loose.map(row).join('');\n } else {\n var byRankNo = {};\n loose.forEach(function (k) {\n var r = Math.min(Math.max(Number(ttl[k].rar) || 1, 1), 10);\n (byRankNo[r] = byRankNo[r] || []).push(k);\n });\n inner = '';\n Object.keys(byRankNo).sort(function (a, b) { return a - b; }).forEach(function (r) {\n var st = MOD_rankStyle(r);\n inner += MOD_wikiGroup('Rank ' + r + '', byRankNo[r].length,\n byRankNo[r].map(row).join(''), 4);\n });\n }\n h += MOD_wikiGroup('From the story', loose.length, inner, 3);\n }\n return h;\n});\n\n/* --- 7. areas ------------------------------------------------------------- */\nMOD_wikiPage('areas', 'Areas & enemies', function () {\n var h = '

Areas & enemies

' +\n '

Every area, what lives in it and at what levels, and what it ' +\n 'drops. Enemy strength is not read off these levels — see the note below the ' +\n 'table.

';\n\n /* Two of the game's 21 areas are not places and listing them misleads:\n nwh \"Somewhere\" — where global.current_z parks whenever you are NOT in\n a fight, populated by creature.default (\"Nothing\").\n tst \"Test\" — reached only from chss.tst, which has id -1 and is in no\n sector, so nothing in the game can travel there.\n The test for \"is this a place\" is whether you can get to it, and neither\n can be. Everything else is listed whether or not you have been. */\n var MOD_WIKI_NOT_PLACES = { nwh: 1, tst: 1 };\n\n var keys = [];\n for (var k in area) {\n if (area[k] && area[k].pop && !MOD_WIKI_NOT_PLACES[k]) keys.push(k);\n }\n /* Ordered by the middle of the level band, which is the closest thing the\n game has to a difficulty order. */\n keys.sort(function (a, b) {\n var mid = function (z) {\n if (!z.pop.length) return 0;\n var s = 0; z.pop.forEach(function (e) { s += (e.lvlmin + e.lvlmax) / 2; });\n return s / z.pop.length;\n };\n return mid(area[a]) - mid(area[b]);\n });\n\n keys.forEach(function (k) {\n var z = area[k];\n /* An area whose baked bands are not finite can never spawn anything — see\n the note rendered under its table. Derived rather than listed by name, so\n it stays true if the game changes. */\n var broken = !z.popc || z.popc.length !== z.pop.length ||\n z.popc.some(function (band) { return !band || !isFinite(band[1] - band[0]); });\n h += '

' + MOD_WIKI.safe(z.name) +\n ' ' + k + '' +\n (broken ? 'nothing spawns' : '') +\n '' + (z.size === -1 ? 'endless' : z.size + ' to clear') +\n '

';\n if (z.pop.length) {\n /* The spawn share is NOT pop[i].c. z_bake normalises those weights into\n area.popc — a list of [lo,hi] bands that mon_gen rolls a uniform\n random against — and the weights do not have to sum to 1, so they are\n off wherever they don't. The Southern forest's .35/.45/.25 are really\n 33/43/24%. Read the bands the game actually rolls against. */\n var bands = z.popc || [];\n h += '';\n z.pop.forEach(function (e, i) {\n var c = e.crt || {};\n var band = bands[i];\n var share = band ? band[1] - band[0] : NaN;\n h += '' +\n '' +\n '';\n });\n h += '
' + MOD_WIKI.safe(c.name || '?') + 'lv ' + e.lvlmin +\n (e.lvlmax !== e.lvlmin ? '–' + e.lvlmax : '') + '' + (isFinite(share)\n ? Math.round(share * 100) + '% of spawns'\n : '—') + '
';\n if (broken) {\n h += '
Nothing spawns here. The area\\'s entries carry ' +\n 'no spawn weight, so the table the game rolls against comes out as ' +\n 'NaN and no creature can ever match it. A bug in the base game, left ' +\n 'as it is — nothing in the game travels to this area either.
';\n }\n }\n if (z.drop && z.drop.length) {\n var d = z.drop.map(function (x) {\n return MOD_WIKI.safe((x.item && x.item.name) || '?') +\n ' ' + ((x.c || 0) * 100).toFixed(2) + '%';\n }).join(', ');\n h += '
Drops: ' + d + '
';\n }\n h += '
';\n });\n\n var fightsOrig = MOD_FIGHTS.mode === 'original';\n h += '

How hard an enemy is

' +\n '

It depends on the Fights setting, which is ' +\n (fightsOrig ? 'Original' : 'Scaled') + ' now. On Original (the default) ' +\n 'every area the author wrote is exactly as he wrote it: his creatures, his ' +\n 'level ranges, and you out-grow them as you do in his game. On Scaled every ' +\n 'area is sized to you. The three areas past the golem arena and the rank ' +\n 'duels are sized to you either way.

' +\n '

Sizing to you works like this. Enemies are not fitted to a level. Each spawn is solved against your ' +\n 'actual power at the moment it appears, aiming at roughly ' + MOD_ENEMY.kill +\n ' swings to kill it and ' + MOD_ENEMY.die + ' before it kills you, with a ' +\n 'guaranteed margin between the two so a fight is never a coin flip. Variety ' +\n 'comes from the area\\'s level band and each creature\\'s own stat weighting, ' +\n 'not from difficulty scaling.

' +\n '

The reason for that: within the first stage alone you go from 1 STR to ' +\n 'around 265, so anything anchored to a level is either trivial at the end of ' +\n 'a stage or impossible at the start of one.

';\n return h;\n});\n\n/* --- 8. items -------------------------------------------------------------\n The one place where \"list everything\" needs help: 321 of the game's 371\n plain items are the same inventory type, so grouping by type alone leaves a\n single group of 321. Anything past MOD_WIKI_ALPHA_AT gets an alphabetical\n sub-split as well — the oldest index pattern there is, and derived rather\n than invented, which a hand-made taxonomy would not be.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_ALPHA_AT = 60;\n\n/* The game's own inventory tabs (isort): ALL / WPN / EQP / USE / OTHER. Using\n its categories rather than new ones means a group here is a tab there. */\nvar MOD_WIKI_STYPE = { 2: 'Weapons', 3: 'Equipment', 4: 'Usable', 5: 'Other' };\n\nfunction MOD_wikiItemRow(it) {\n var val = it.v !== undefined ? it.v : (it.val !== undefined ? it.val : null);\n return '
' +\n '

' + MOD_WIKI.safe(it.name) +\n (it.rar > 1 ? 'rarity ' + it.rar + '' : '') +\n (val !== null ? '' + MOD_wikiNum(val) + ' coin' : '') +\n (it.amount > 0 ? '×' + it.amount + '' : '') +\n '

' + MOD_wikiDesc(it.desc) + '
';\n}\n\n/* The letter a name files under. Several items are quoted — the master's\n manuals are literally named \"Sword Saint Manual\" — and filing those under a\n quotation mark is no use to anyone looking for S. */\nfunction MOD_wikiLetter(name) {\n var m = String(name || '').replace(/^[^0-9A-Za-z]+/, '');\n return (m.charAt(0) || '?').toUpperCase();\n}\n\nfunction MOD_wikiByLetter(a, b) {\n var x = MOD_wikiLetter(a.name) + String(a.name || '').toLowerCase();\n var y = MOD_wikiLetter(b.name) + String(b.name || '').toLowerCase();\n return x < y ? -1 : (x > y ? 1 : 0);\n}\n\n/* Flat below the threshold, alphabetically sub-grouped above it. Ranges are cut\n from the entries actually present, so they stay even as content is added. */\nfunction MOD_wikiAlpha(entries, render, level) {\n if (entries.length <= MOD_WIKI_ALPHA_AT) return entries.map(render).join('');\n var per = Math.ceil(entries.length / Math.ceil(entries.length / 40));\n var out = '';\n for (var i = 0; i < entries.length; i += per) {\n var chunk = entries.slice(i, i + per);\n var first = MOD_wikiLetter(chunk[0].name);\n var last = MOD_wikiLetter(chunk[chunk.length - 1].name);\n out += MOD_wikiGroup(first === last ? first : first + '–' + last,\n chunk.length, chunk.map(render).join(''), level || 4);\n }\n return out;\n}\n\nMOD_wikiPage('items', 'Items', function () {\n var h = '

Items

' +\n '

Everything the game defines, whether or not you have seen ' +\n 'it. Grouped the way your inventory groups it. Values are the base sell ' +\n 'price where the game gives one.

';\n\n /* item[] first, split by the inventory tab it appears under. */\n var buckets = {};\n for (var k in item) {\n var it = item[k];\n if (!it || !it.name || it.name === 'dummy') continue;\n var lab = MOD_WIKI_STYPE[it.stype] || 'Uncategorised';\n (buckets[lab] = buckets[lab] || []).push(it);\n }\n ['Usable', 'Other', 'Weapons', 'Equipment', 'Uncategorised'].forEach(function (lab) {\n var list = buckets[lab];\n if (!list || !list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(lab, list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n\n /* and the equipment objects, which live in their own globals */\n [['Weapons', wpn], ['Armour', eqp], ['Shields', sld], ['Accessories', acc]]\n .forEach(function (g) {\n var list = [];\n for (var kk in g[1]) {\n if (g[1][kk] && g[1][kk].name && g[1][kk].name !== 'dummy') list.push(g[1][kk]);\n }\n if (!list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(g[0], list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n return h;\n});\n\n/* --- 9. actions ----------------------------------------------------------- */\nMOD_wikiPage('actions', 'Actions', function () {\n var unlock = {};\n MOD_ACTION_UNLOCKS.forEach(function (u) {\n if (u.act) unlock[u.act.id] = (skl[u.skill] ? skl[u.skill].name : u.skill) +\n ' level ' + u.lv;\n });\n if (typeof act.mod_qi !== 'undefined') {\n unlock[act.mod_qi.id] = 'clearing the dojo\\'s Easiest, Easy and Normal fights';\n }\n\n var h = '

Actions

' +\n '

What you can be doing. One at a time, and only where the ' +\n 'action makes sense — unless \"Unrestricted actions\" is ticked in settings, ' +\n 'which lets several run at once anywhere. Both limits are deliberate; the ' +\n 'switch is there because they are not to everyone\\'s taste.

';\n\n var keys = [];\n for (var k in act) if (act[k] && act[k].name && act[k].name !== 'dummy') keys.push(k);\n keys.sort(function (a, b) { return act[a].name < act[b].name ? -1 : 1; });\n\n keys.forEach(function (k) {\n var a = act[k];\n var how = unlock[a.id];\n h += '
' +\n '

' + MOD_WIKI.safe(a.name) +\n (a.have === true ? 'unlocked'\n : 'locked') +\n (how ? 'from ' + MOD_WIKI.safe(how) + '' : '') +\n '

' + MOD_wikiDesc(a.desc) + '
';\n });\n return h;\n});\n\n/* --- 10. the dojo --------------------------------------------------------- */\nMOD_wikiPage('dojo', 'The dojo', function () {\n var h = '

The dojo

' +\n '

The instructor promises a reward every five levels. The base ' +\n 'game stops delivering at 30; the mod carries the same ladder to 110, on the ' +\n 'same terms.

' +\n '

Only the lowest rung you have not claimed is offered, which is how the ' +\n 'first six behave. Rewards are sized against the character experience curve ' +\n '(4×level3 + level2) — a level at 110 costs 5.3 ' +\n 'million, which is why the base game\\'s best pill is worth 0.3% of one there.

' +\n '' +\n '';\n\n MOD_DOJO_RUNGS.forEach(function (r) {\n var pill = item[r.pill];\n var extra = [];\n if (r.realmPill) extra.push('breakthrough pill for realm ' + r.realmPill);\n if (r.manual) extra.push('a choice of master\\'s manual');\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
LevelRewardCoinAlso
' + r.lv + '' + (pill ? MOD_WIKI.safe(pill.name) + ' ×' + r.n\n : '—') + '' + MOD_wikiNum(r.coin) + '' + (extra.join(', ') || '—') + '' + (global.flags[r.flag] ? 'claimed' : '') +\n '
' +\n\n '

Spirit pills

' +\n '

Character experience in a bottle. The four the mod adds continue the ' +\n 'game\\'s own series up to the new curve.

' +\n '' +\n '';\n MOD_PILLS.forEach(function (p) {\n h += '';\n });\n h += '
PillExperience
' + MOD_WIKI.safe(p[2]) + '' +\n p[3].toLocaleString() + '
' +\n\n '

Master\\'s manuals

' +\n '

A third grade of skillbook, offered at levels 50, 75 and 100 as a choice ' +\n 'the same way the first one was. Each is worth +' +\n Math.round(MOD_MANUAL_RATE * 100) + '% experience in its skill, permanently.

' +\n '' +\n '';\n MOD_MANUALS.forEach(function (m) {\n h += '';\n });\n h += '
ManualSkill
' + MOD_WIKI.safe(m[2]) + '' +\n MOD_WIKI.safe(skl[m[0]] ? (skl[m[0]].bname || skl[m[0]].name) : m[0]) +\n '
';\n return h;\n});\n\n/* --- 11. mechanics -------------------------------------------------------- */\nMOD_wikiPage('mechanics', 'Mechanics', function () {\n return '

Mechanics

' +\n\n '

Damage is subtractive, in both directions

' +\n '

A hit does the attacker\\'s strength minus the defender\\'s strength, plus ' +\n 'the weapon, plus one, before criticals. A defender\\'s STR is their ' +\n 'armour — there is no separate armour stat doing that job.

' +\n '

Two consequences worth carrying around:

' +\n '
    ' +\n '
  • Damage does not taper, it stops. Once a defender\\'s STR passes ' +\n 'yours, your hits do not get small; they land for nothing. Progress against ' +\n 'something too strong is not slow, it is zero.
  • ' +\n '
  • A stat ratio tells you nothing. Comparing your HP to an enemy\\'s ' +\n 'strength ignores the subtraction entirely and reads several times off in ' +\n 'either direction.
  • ' +\n '
' +\n '

This is also the reason enemies are never scaled by raising ' +\n 'their strength: it would raise their offence and their armour together, and ' +\n 'the second effect arrives first.

' +\n\n '

Criticals carry the late game

' +\n '

Critical rate reaches about a third at the top of the ladder and a ' +\n 'critical is roughly eight times a normal swing, so around three quarters of ' +\n 'all damage done at that point is critical damage. Anything that raises ' +\n 'critical rate or critical damage is worth more than it looks; Sharp Eye and ' +\n 'War are the two that do.

' +\n\n '

Hit chance

' +\n '

Landing a hit is a contest of agility. It is a ratio rather than a ' +\n 'subtraction, so agility behaves more gently than strength does — falling ' +\n 'behind on it costs you accuracy, not everything.

' +\n\n '

Skill sections and parents

' +\n '

Each of the ' + Object.keys(MOD_SECTIONS).length + ' sections has a parent ' +\n 'skill. A parent earns ' + Math.round(MOD_PARENT.feed * 100) + '% of everything ' +\n 'its children earn and grants no stats of its own. In return, a child below ' +\n 'the parent\\'s level trains ' + Math.round(MOD_PARENT.perLevel * 100) +\n '% faster per level of lag, up to ' + MOD_PARENT.maxGap + ' levels behind. ' +\n 'Bringing one section up makes every skill in it cheaper to catch up.

' +\n\n '

Saving

' +\n '

Three slots, switched from the bottom bar. The game builds its world once ' +\n 'at startup and cannot unload a save, so switching slots or starting a new ' +\n 'game reloads the page. Exporting a save before running anything unusual is ' +\n 'always the safe move.

';\n});\n\n/* --- the document ---------------------------------------------------------\n One string, no fetches, no dependencies: this has to work from file:// as\n readily as from a server.\n ------------------------------------------------------------------------- */\n\nfunction MOD_wikiCss() {\n return [\n ':root{color-scheme:dark}',\n '*{box-sizing:border-box}',\n /* Menlo and Consolas before the generic: with MS Gothic missing (any Mac)\n \"monospace\" is Courier, which Safari sets wider than Chrome does, and\n table cells wrapped on one and not the other. */\n 'body{margin:0;background:#060a1c;color:#dfe6f5;font:14px/1.6 \"MS Gothic\",' +\n '\"MS Mincho\",Menlo,Consolas,\"DejaVu Sans Mono\",monospace;display:flex;min-height:100vh}',\n 'a{color:#7cb0ff}',\n 'nav{width:210px;flex:0 0 210px;background:#081040;border-right:1px solid #46a;' +\n 'position:sticky;top:0;height:100vh;overflow:auto;padding:14px 0}',\n 'nav h2{font-size:13px;color:#9fb4d8;margin:0 0 10px 14px;letter-spacing:1px}',\n 'nav a{display:block;padding:6px 14px;text-decoration:none;color:#cfe0ff;' +\n 'border-left:3px solid transparent}',\n 'nav a:hover{background:#154080}',\n 'nav a.on{background:#123070;border-left-color:#7cb0ff;color:#fff}',\n 'nav .srch{margin:0 10px 12px;width:calc(100% - 20px);background:#050a20;' +\n 'color:#dfe6f5;border:1px solid #46a;padding:5px;font:inherit}',\n 'nav .xall{display:block;margin:12px 10px 0;color:#8296b8;font-size:12px;' +\n 'cursor:pointer;-webkit-user-select:none;user-select:none}',\n 'nav .xall input{vertical-align:-1px;margin-right:4px}',\n 'details.wk-g{border:1px solid #17224a;background:#0a1130;margin:8px 0}',\n 'details.wk-g>summary{cursor:pointer;padding:7px 12px;list-style:none;' +\n 'background:#0d1740}',\n 'details.wk-g>summary:hover{background:#154080}',\n 'details.wk-g>summary::-webkit-details-marker{display:none}',\n // a caret drawn rather than borrowed, so it is the same in every browser\n 'details.wk-g>summary::before{content:\"\\\\25B8\";color:#7cb0ff;' +\n 'display:inline-block;width:1em;transition:transform .12s}',\n 'details.wk-g[open]>summary::before{transform:rotate(90deg)}',\n 'details.wk-g>summary h2,details.wk-g>summary h3{display:inline;margin:0;' +\n 'font-size:15px;color:#fff}',\n '.wk-gi{padding:2px 12px 10px}',\n 'details.wk-g details.wk-g>summary h4{display:inline;margin:0;font-size:13px;' +\n 'color:#cfe0ff;font-weight:normal}',\n 'details.wk-g details.wk-g>summary{padding:5px 10px;background:#0b1435}',\n 'main{flex:1;min-width:0;padding:22px 30px 80px;max-width:1000px}',\n 'h1{font-size:26px;margin:0 0 6px;color:#fff;border-bottom:1px solid #46a;' +\n 'padding-bottom:8px}',\n 'h2{font-size:19px;color:#bcd4ff;margin:30px 0 8px}',\n 'h3{font-size:15px;color:#fff;margin:0 0 4px}',\n 'p.lede{color:#a8bcdc;margin:0 0 18px}',\n '.dim{color:#8296b8;font-weight:normal}',\n '.flav{font-size:12px;font-style:italic}',\n '.note{background:#0b1638;border-left:3px solid #46a;padding:8px 12px;' +\n 'color:#a8bcdc;margin:14px 0}',\n 'code,pre{background:#0b1638;border:1px solid #24356e;padding:1px 5px;' +\n 'color:#9fd0ff}',\n 'pre{padding:10px;overflow-x:auto}',\n 'dl dt{color:#fff;margin-top:14px}',\n 'dl dd{margin:2px 0 0 0;color:#a8bcdc}',\n 'ul{color:#a8bcdc}',\n 'table.wk-tbl{border-collapse:collapse;width:100%;margin:10px 0;' +\n 'display:block;overflow-x:auto}',\n 'table.wk-tbl th{text-align:left;color:#9fb4d8;font-weight:normal;' +\n 'border-bottom:1px solid #46a;padding:5px 9px;white-space:nowrap}',\n 'table.wk-tbl td{border-bottom:1px solid #17224a;padding:5px 9px;' +\n 'vertical-align:top}',\n 'table.wk-tbl tr.done td{color:#cfe0ff}',\n 'table.wk-tbl tr.here{background:#123070}',\n '.num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}',\n '.wk-e{border:1px solid #17224a;background:#081040;padding:9px 12px;' +\n 'margin:7px 0;border-radius:2px}',\n 'tr.wk-e{border:0;background:none}',\n '.wk-e.done{border-color:#2a4a8c}',\n '.wk-e h3{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}',\n '.lv{margin-left:auto;font-size:12px;color:#8296b8;font-weight:normal}',\n '.lv.capped{color:#ffd24a}',\n '.rank{font-size:12px;color:#8296b8;font-weight:normal}',\n '.desc{color:#a8bcdc;font-size:13px}',\n '.proj{color:#ffd24a;font-size:12px;margin-top:3px}',\n '.tag{font-size:11px;border:1px solid #46a;color:#9fb4d8;padding:0 5px;' +\n 'border-radius:2px;font-weight:normal}',\n '.tag.mod{border-color:#7a4ea8;color:#c07bff}',\n '.tag.done{border-color:#2e7d4f;color:#6fdc9a}',\n '.tag.here{border-color:#ffd24a;color:#ffd24a}',\n '.tag.warn{border-color:#a8562e;color:#ff9a5c}',\n 'table.wk-perks{border-collapse:collapse;margin:6px 0 0;width:100%}',\n 'table.wk-perks td{padding:2px 8px 2px 0;font-size:12px;color:#8296b8;' +\n 'border-bottom:1px solid #101a3c}',\n 'table.wk-perks tr.done td{color:#cfe0ff}',\n 'table.wk-perks td.tick{color:#6fdc9a;width:1em;text-align:right}',\n // fixed columns so the level bands line up between one area and the next\n 'table.wk-pop{table-layout:fixed}',\n 'table.wk-pop td:first-child{width:60%}',\n 'table.wk-pop td:nth-child(2){width:20%}',\n '.wk-title .tname{font-weight:bold}',\n '.wk-now{background:#0b1638;border:1px solid #24356e;padding:10px 14px;' +\n 'margin:0 0 18px;display:inline-block;min-width:320px}',\n '.wk-now h3{color:#9fb4d8;font-weight:normal;font-size:13px;' +\n 'letter-spacing:1px;margin-bottom:6px}',\n '.wk-now td{padding:2px 16px 2px 0}',\n '.wk-now td:last-child{color:#fff;text-align:right}',\n '.pg{display:none}.pg.on{display:block}',\n 'footer{color:#5a6c8c;font-size:12px;border-top:1px solid #17224a;' +\n 'margin-top:40px;padding-top:12px}',\n '@media(max-width:760px){body{display:block}nav{width:auto;height:auto;' +\n 'position:static;flex:none}main{padding:16px}}'\n ].join('\\n');\n}\n\n/* Filtering, page switching and the hash. Small enough to inline, and inlining\n it is the point — the document has to stand on its own. */\nfunction MOD_wikiJs() {\n return [\n 'var pages=[].slice.call(document.querySelectorAll(\".pg\"));',\n 'var links=[].slice.call(document.querySelectorAll(\"nav a[data-p]\"));',\n 'function show(id){',\n ' var found=false;',\n ' pages.forEach(function(p){var on=p.id===\"pg-\"+id;if(on)found=true;',\n ' p.className=\"pg\"+(on?\" on\":\"\")});',\n ' if(!found&&pages.length){pages[0].className=\"pg on\";id=pages[0].id.slice(3)}',\n ' links.forEach(function(a){a.className=a.dataset.p===id?\"on\":\"\"});',\n ' window.scrollTo(0,0);return id;}',\n 'links.forEach(function(a){a.addEventListener(\"click\",function(e){',\n ' e.preventDefault();location.hash=a.dataset.p;show(a.dataset.p)})});',\n 'window.addEventListener(\"hashchange\",function(){show(location.hash.slice(1))});',\n 'show(location.hash.slice(1)||\"start\");',\n // Search filters entries, hides groups left with none, and OPENS the groups\n // that still have some — a match hidden inside a collapsed group reads as\n // no match at all, which is worse than not having search.\n 'var box=document.getElementById(\"srch\");',\n 'var xall=document.getElementById(\"xall\");',\n 'var groups=[].slice.call(document.querySelectorAll(\"details.wk-g\"));',\n 'function filter(){',\n ' var q=box.value.trim().toLowerCase();',\n ' pages.forEach(function(p){',\n ' [].slice.call(p.querySelectorAll(\".wk-e\")).forEach(function(e){',\n ' e.style.display=(!q||e.textContent.toLowerCase().indexOf(q)>=0)?\"\":\"none\"})});',\n ' groups.forEach(function(g){',\n ' var any=[].slice.call(g.querySelectorAll(\".wk-e\")).some(function(e){',\n ' return e.style.display!==\"none\"});',\n ' g.style.display=any?\"\":\"none\";',\n ' g.open=xall.checked||(!!q&&any)}); }',\n 'box.addEventListener(\"input\",filter);',\n 'box.addEventListener(\"keydown\",function(e){if(e.key===\"Escape\"){box.value=\"\";filter()}});',\n // Everything is collapsed by default because three of these pages run past\n // 40,000px open. This is the way back to one long document, which is what\n // the browser's own find needs.\n 'xall.addEventListener(\"change\",filter);'\n ].join('\\n');\n}\n\nfunction MOD_wikiBuild() {\n var nav = '', body = '';\n MOD_WIKI_PAGES.forEach(function (pg) {\n nav += '' +\n MOD_WIKI.safe(pg.label) + '';\n var inner;\n /* One page throwing must not take the wiki down with it — the page says so\n and the rest still renders. */\n try { inner = pg.build(); }\n catch (e) {\n inner = '

' + MOD_WIKI.safe(pg.label) + '

This page ' +\n 'could not be built: ' + MOD_WIKI.safe(e.message) + '

';\n console.warn('[mod] wiki page \"' + pg.id + '\" failed: ' + e.message);\n }\n body += '
' + inner + '
';\n });\n\n var stamp = new Date().toLocaleString();\n return '' +\n '' +\n '' + MOD_WIKI.safe(MOD_WIKI.title) + '' +\n '' +\n '' +\n '
' + body +\n '
Generated from the running game at ' + MOD_WIKI.safe(stamp) +\n ' — mod ' + MOD_WIKI.safe(MOD.version) + '. Everything here is read from the ' +\n 'live data, including your own save, so it cannot fall out of step with the ' +\n 'game. The game is truezhangwei\\'s; the mod is not endorsed by the author.' +\n '
' +\n ' inside content closing the block early, so that is the\n one thing scrubbed. */\n safe: function (h) {\n return String(h === undefined || h === null ? '' : h).replace(/<\\/script/gi, '<\\\\/script');\n }\n};\n\n/* Descriptions carry real HTML (coloured spans, the game's separator div), so\n they are passed through rather than escaped — but the separator becomes a\n rule, since a wiki row is not a tooltip. */\nfunction MOD_wikiDesc(d) {\n var s;\n try { s = (typeof d === 'function') ? d() : d; } catch (e) { s = ''; }\n if (!s) return '';\n s = String(s).split('
 
').join('
');\n return MOD_WIKI.safe(s);\n}\n\nfunction MOD_wikiNum(n) {\n if (!isFinite(n)) return '—';\n return Math.round(n).toLocaleString();\n}\n\n/* An effect line at a level the player has not reached yet. The effect\n functions read the skill they are handed, so the level is borrowed and put\n back; anything that throws simply gets no projection. */\nfunction MOD_wikiEffectAt(key, sk, lvl) {\n var eff = MOD_EFFECTS[key];\n if (!eff) return '';\n var was = sk.lvl, out = '';\n try { sk.lvl = lvl; out = eff(sk) || ''; }\n catch (e) { out = ''; }\n finally { sk.lvl = was; }\n return MOD_WIKI.safe(out);\n}\n\n/* --- page builders --------------------------------------------------------\n Each returns the inner HTML of one page. `wk-e` marks a searchable entry and\n `wk-g` a group that hides itself when all its entries are filtered out.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_PAGES = [];\n\nfunction MOD_wikiPage(id, label, build) {\n MOD_WIKI_PAGES.push({ id: id, label: label, build: build });\n}\n\n/* A collapsible group.\n\n Not decoration: with every skill, item and title listed, three of the eleven\n pages ran past 40,000 pixels — a hundred screens of scrolling, which is the\n same as not having the content at all. Collapsed, each of those pages opens\n as an index you can read in one screen and expand where you want.\n\n
rather than a click handler, so it still works with JavaScript off,\n the browser's own find can be made to reach inside it, and the summary is\n keyboard-reachable for free. `level` is the heading level for the summary,\n since a group is sometimes a section of a page and sometimes a subsection. */\nfunction MOD_wikiGroup(heading, count, inner, level) {\n var hn = 'h' + (level || 2);\n return '
<' + hn + '>' + heading +\n (count !== null && count !== undefined\n ? ' (' + count + ')' : '') +\n '
' + inner + '
';\n}\n\n/* --- 1. orientation ------------------------------------------------------- */\nMOD_wikiPage('start', 'Start here', function () {\n var cap = MOD_levelCap();\n var realm = MOD_realm();\n var h = '

proto23

' +\n '

An idle RPG by truezhangwei, plus a mod. You train skills by ' +\n 'doing things, the things you can do open up as the story does, and none of ' +\n 'it is in a hurry.

' +\n\n '

Where you are right now

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Character level' + MOD_wikiNum(you.lvl) + '
Skill level cap' + cap + ' — ' +\n MOD_WIKI.safe(MOD_CAP.name) + '
Cultivation realm' + MOD_WIKI.safe(realm.name) +\n ' (' + realm.n + ' of ' + (MOD_REALMS.length - 1) + ')
Titles held' + MOD_titleCount() + ' — renown ' +\n (skl.rnwn ? skl.rnwn.lvl : 0) + '
Coin' + MOD_wikiNum(you.wealth || 0) + '
' +\n\n '

The four things worth knowing early

' +\n '
' +\n '
Your skill levels are capped by the story, not by grinding.
' +\n '
The cap starts at 10 and ends at 110, and it moves when you reach a new ' +\n 'place — not when you earn enough experience. If a skill has stopped ' +\n 'levelling, you are not doing it wrong; you need to go somewhere. See ' +\n 'Progression.
' +\n\n '
Armour is subtraction, so levels matter more than they look.
' +\n '
A hit does the attacker\\'s STR minus the defender\\'s STR. That means a ' +\n 'small gap in strength is a large gap in damage, and a big enough one takes ' +\n 'your damage to nothing at all rather than merely reducing it. See ' +\n 'Mechanics.
' +\n\n '
Most skills have a parent that pulls them along.
' +\n '
Every section has a parent skill that earns a share of everything its ' +\n 'children do, and any child lagging behind the parent trains faster — up to ' +\n 'twice as fast at ' + MOD_PARENT.maxGap + ' levels behind. A neglected skill is ' +\n 'cheap to bring up later, which is the only reason having this many is ' +\n 'workable.
' +\n\n '
Cultivation is a second ladder, and it is not automatic.
' +\n '
Circulating qi levels a skill; the skill does not raise your realm. ' +\n 'Reaching the level opens a bottleneck, and breaking through it costs a pill ' +\n 'and can fail. See Cultivation.
' +\n '
' +\n\n '

What is the mod and what is the game

' +\n '

The game is index.html and it is entirely truezhangwei\\'s ' +\n 'work. Everything in mod.js is a modification of it and is not ' +\n 'endorsed by the author. Pages here mark added content with ' +\n 'mod where the distinction matters. The full ' +\n 'list of changes is in the changelog, on the bottom bar.

';\n return h;\n});\n\n/* --- 2. progression ------------------------------------------------------- */\nMOD_wikiPage('progress', 'Progression', function () {\n var cap = MOD_levelCap();\n var h = '

Progression

' +\n '

Skill levels are capped by how far the story has gone. Ten ' +\n 'rungs, 10 through 110. The cap applies to every skill at once; Renown is ' +\n 'the single exception, since it is counted rather than trained.

' +\n '' +\n '';\n\n MOD_TIERS.forEach(function (t) {\n var got = null;\n try { got = MOD_tierFlag(t); } catch (e) {}\n var here = (t.cap === cap);\n var need = t.needText ? t.needText\n : (t.flags.length ? t.flags.join(' or ') : 'the start of the game');\n h += '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
CapStageWhat opens it
' + t.cap + '' + MOD_WIKI.safe(t.name) + '' + MOD_WIKI.safe(need) + '' + (here ? 'you are here'\n : got ? 'reached' : '') + '
' +\n\n '

The experience curve

' +\n '

The game\\'s own skill curve is super-exponential — level 109 to 110 ' +\n 'alone costs ' + MOD_wikiNum(MOD_vanillaExpnext(109)) + ' experience, so the top of the ladder ' +\n 'is unreachable by a factor of about ten trillion. But the author only ever ' +\n 'designed skills up to level 15, and below that his curve is fine.

' +\n (MOD_XP.vanillaTo > 0\n ? '

So the mod keeps it. Below level ' + MOD_XP.vanillaTo + ' a skill costs ' +\n 'exactly what the original charges; from there it rises by ×' +\n MOD_XP.ratio.toFixed(4) + ' a level, a ratio solved so the climb to 110 takes ' +\n 'the same time it always did.'\n : '

You have chosen the Mod before 4.2 pacing: a plain geometric curve, ' +\n '×' + MOD_XP.ratio.toFixed(3) + ' a level from level 0, much faster than the ' +\n 'original early on. The Original pacing in settings puts back the ' +\n 'author\\'s own cost below level 10.') +\n ' Character level uses the game\\'s own curve, untouched.

' +\n '' +\n '';\n [1, 5, 9, 10, 15, 25, 50, 75, 90, 109].forEach(function (l) {\n h += '';\n });\n h += '
LevelExperience for the nextOriginal game
' + l +\n '' + MOD_wikiNum(MOD_expnextFor(l)) + '' +\n MOD_wikiNum(MOD_vanillaExpnext(l)) + '
';\n return h;\n});\n\n/* --- 3. skills ------------------------------------------------------------ */\nMOD_wikiPage('skills', 'Skills', function () {\n var cap = MOD_levelCap();\n var bySection = {};\n var keys = [];\n for (var k in skl) {\n var s = skl[k];\n if (!s || typeof s !== 'object' || !s.name) continue;\n keys.push(k);\n }\n keys.forEach(function (k) {\n var s = skl[k];\n var sec = MOD_sectionOf(s);\n (bySection[sec] = bySection[sec] || []).push(k);\n });\n\n var h = '

Skills

' +\n '

' + keys.length + ' skills in ' +\n Object.keys(MOD_SECTIONS).length + ' sections. Every one has perks at ' +\n MOD_LADDER.join(', ') + ', and every one that has milestones grants titles at ' +\n MOD_TITLE_RUNGS.join(', ') + '. Levels and ticks below are yours.

';\n\n Object.keys(MOD_SECTIONS).forEach(function (sec) {\n var list = bySection[sec];\n if (!list || !list.length) return;\n var parentKey = MOD_PARENT_KEY[sec];\n var g = '';\n if (parentKey && skl[parentKey]) {\n g += '

Parent skill: ' +\n MOD_WIKI.safe(skl[parentKey].bname || skl[parentKey].name) +\n ' — earns ' + Math.round(MOD_PARENT.feed * 100) +\n '% of what its children earn, and speeds up any child that lags behind it.

';\n }\n list.sort(function (a, b) { return skl[a].name < skl[b].name ? -1 : 1; });\n list.forEach(function (k) {\n var s = skl[k];\n var lvl = Number(s.lvl) || 0;\n g += '
' +\n '

' + MOD_WIKI.safe(s.bname || s.name) +\n (s.bname ? ' (' + MOD_WIKI.safe(s.name) + ')' : '') +\n '= cap ? ' capped' : '') + '\">lv ' + lvl +\n (lvl >= cap ? ' — at the cap' : '') + '

' +\n '
' + MOD_wikiDesc(s.desc) + '
';\n\n var at110 = MOD_wikiEffectAt(k, s, 110);\n if (at110) g += '
At level 110: ' + at110 + '
';\n\n if (s.mlstn && s.mlstn.length) {\n g += '';\n s.mlstn.forEach(function (m) {\n g += '' +\n '' +\n '' +\n '';\n });\n g += '
' + m.lv + '' + MOD_WIKI.safe(m.p || '—') + '' + (m.g === true ? '✓' : '') + '
';\n }\n g += '
';\n });\n h += MOD_wikiGroup(MOD_WIKI.safe(MOD_SECTIONS[sec]), list.length, g, 2);\n });\n return h;\n});\n\n/* --- 4. cultivation ------------------------------------------------------- */\nMOD_wikiPage('cultivation', 'Cultivation', function () {\n var now = MOD_realm();\n var elig = 0;\n try { elig = MOD_realmEligible().n; } catch (e) {}\n var qic = skl.qic ? skl.qic.lvl : 0;\n\n var h = '

Cultivation

' +\n '

Ten realms above Mortal. Circulating qi levels the Qi ' +\n 'Circulation skill; the skill does not advance the realm. Reaching the level ' +\n 'opens a bottleneck, and breaking through it costs a pill and can fail — the ' +\n 'pill is spent either way.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Realm' + MOD_WIKI.safe(MOD_realmTitle()) + '
Spiritual Root' + MOD_WIKI.safe(MOD_root().name) + '
Qi Circulationlevel ' + qic + '
Insight' + MOD_insight() + '
Bottleneck open' +\n (elig > now.n ? 'yes — ' + MOD_WIKI.safe(MOD_REALMS[now.n + 1].name) +\n ' is within reach' : 'no') + '
Qi Deviation' + (MOD_deviated()\n ? MOD_deviationLeft() + ' in-game minutes left' : 'no') + '
' +\n '

Realm thresholds are the same levels that earn a title rank, so realm N ' +\n 'is the level at which a rank N title arrives. Every realm multiplies all ' +\n 'stats and health, and the multipliers do not stack with each other — the ' +\n 'realm you are in is the one that applies.

' +\n '' +\n '';\n\n MOD_REALMS.forEach(function (r) {\n var pill = r.n >= 1 ? item['mod_bp' + r.n] : null;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmNameQi Circ.StatsHealthPill
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + r.qic + '×' + r.mult + '×' + r.hp + '' + (pill ? MOD_WIKI.safe(pill.name) : '—') +\n '
' +\n\n '

Where the pills come from

' +\n '

Every breakthrough pill has a source in normal play. None of them are ' +\n 'craftable, and none are cheap — a bottleneck you can buy in bulk is not a ' +\n 'bottleneck.

' +\n '' +\n '' +\n '' +\n '' +\n '
RealmsSource
1The dojo instructor, when qi ' +\n 'circulation is first taught
2–5The Herbalist in the village ' +\n '(vendor.pha1), restocked
6–10The dojo\\'s Level Advancement ' +\n 'ladder, and the Pill Tower
' +\n '

Realm 1 is deliberately not sold at the marketplace: ' +\n 'the marketplace is behind a 40%-a-visit chance encounter, and realm 1 opens ' +\n 'long before that is reliable.

' +\n\n '

Breaking through

' +\n '

A bottleneck wants three things, which is what the genre says it wants: ' +\n 'a pill, insight, and enough consolidation to make the ' +\n 'attempt worth making. The pill and the insight are both spent whether the ' +\n 'attempt works or not.

' +\n '

The odds improve the further past the threshold you are, plus your root, ' +\n 'plus time sat in Closed Door Training, and cap short of certain.

' +\n '' +\n '';\n [0, 2, 4, 6, 8].forEach(function (o) {\n var base = Math.min(0.55 + o * 0.05 + MOD_root().odds, 0.95);\n h += '';\n });\n h += '
Levels past the thresholdChanceAfter full seclusion
+' + o + '' +\n Math.round(base * 100) + '%' +\n Math.round(Math.min(base + MOD_CULT.secludeOdds, 0.95) * 100) + '%
' +\n\n '

Insight

' +\n '

Insight is the half of the price that cannot be bought. It comes from ' +\n 'three places, and they are the three the genre names: meditating, fights you ' +\n 'nearly lost, and going somewhere you have not been.

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
SourceRate
Circulate Qi, at a wall' +\n (MOD_CULT.meditateChance * 100).toFixed(1) + '% a second
Closed Door Training' +\n (MOD_CULT.secludeChance * 100).toFixed(1) + '% a second
A kill made under ' +\n Math.round(MOD_CULT.brinkFrac * 100) + '% health — ' +\n (MOD_FIGHTS.mode === 'original'\n ? 'the author\\'s areas rarely come close once you reach them; the three past ' +\n 'the golem arena and the rank duels do, within a few fights, as does an area ' +\n 'you walk into early'\n : 'every fight costs a share of your health, so a few in a row without resting') +\n '' +\n Math.round(MOD_CULT.brinkChance * 100) + '% a kill
Entering an area for the first timealways
' +\n '

The insight a realm asks for is sized to the climb to it: about ' +\n Math.round(MOD_CULT.wallShare * 100) + '% of the Circulate Qi time the climb took, ' +\n 'spent at the wall. Seclusion earns it three times as fast, and full ' +\n 'consolidation takes as long as that on average.

' +\n '' +\n '' +\n '';\n var wkHours = function (sec) {\n return sec >= 7200 ? (sec / 3600).toFixed(sec >= 36000 ? 0 : 1) + ' h' : Math.round(sec / 60) + ' min';\n };\n MOD_REALMS.forEach(function (r) {\n if (!r.n) return;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmClimb (Circulate Qi, 1x)Insight to enterFull seclusionQi Deviation (in-game)
' + MOD_WIKI.safe(r.name) + '' + wkHours(MOD_realmClimbSec(r.n)) + '' + MOD_insightNeed(r.n) + '' + wkHours(MOD_secludeFull(r.n)) + '' + (MOD_devMinutes(r.n) / 1440).toFixed(1) + ' days
' +\n\n '

The nine layers

' +\n '

Each realm is nine layers deep, derived from where Qi Circulation sits ' +\n 'between this realm\\'s threshold and the next one\\'s — so breaking through ' +\n 'puts you at the first layer of the new realm on its own. Layers are worth ' +\n '×' + MOD_CULT.layerMult + ' stats and ×' + MOD_CULT.layerBody +\n ' body each above the first, and the top realm has no rung above it to ' +\n 'measure against, so it stays at its first layer.

' +\n '' +\n '';\n for (var li = 1; li <= MOD_CULT.layers; li++) {\n h += '';\n }\n h += '
LayerCalled
' +\n li + '' + MOD_LAYER_ORD[li] + ' layer, ' +\n MOD_WIKI.safe(MOD_layerStage(li)) + '
' +\n '

At the threshold of the next realm and before breaking ' +\n 'through, you are a half step to it — the genre\\'s own name for the ' +\n 'state this mod calls a bottleneck.

' +\n\n '

Closed Door Training

' +\n '

Seclusion does two things. It consolidates — a full sitting ' +\n '(the table above) is worth +' + Math.round(MOD_CULT.secludeOdds * 100) +\n '% on the attempt — and it shelters: a breakthrough attempted while ' +\n 'you are still sitting in it cannot end in Qi Deviation, only in a lost ' +\n 'pill. It will not start unless you are actually at a wall.

' +\n\n '

Qi Deviation

' +\n '

Failing a breakthrough out in the open leaves your cultivation base ' +\n 'unstable for as long as the seclusion you skipped (the table above, never ' +\n 'under ' + MOD_CULT.devMinutes + ' in-game minutes): stats at ×' +\n MOD_CULT.devStat + ', body at ×' + MOD_CULT.devBody + ', and no second ' +\n 'attempt until it passes. It clears on its own, or at once with a ' +\n 'Qi Settling Pill. It never kills — the death system stays out of it.

' +\n\n '

Heavenly Tribulation

' +\n '

From realm ' + MOD_CULT.tribFrom + ' up, a successful breakthrough is ' +\n 'answered. The cost is a fraction of your maximum health, so it is a gate on ' +\n 'walking in healthy rather than a damage race; the realm bonus you are ' +\n 'standing on is what pays it. Fall to it and the realm does not open.

' +\n '' +\n '';\n MOD_REALMS.forEach(function (r) {\n if (!r.n || r.n < MOD_CULT.tribFrom) return;\n h += '' +\n '' +\n '';\n });\n h += '
RealmBoltsOf your max HP
' + MOD_WIKI.safe(r.name) + '' + MOD_tribulationBolts(r.n) + '' + Math.round(MOD_tribulationCost(r.n) * 100) + '%
' +\n\n '

Spiritual Root

' +\n '

Rolled once, when the dojo finishes teaching you to fight. It is never a ' +\n 'dead end: the worst grade costs cultivation speed, not access, and a ' +\n 'Root Cleansing Pill moves you up a grade.

' +\n '' +\n '';\n var rootTotal = 0;\n MOD_ROOTS.forEach(function (r) { rootTotal += r.weight; });\n MOD_ROOTS.forEach(function (r) {\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
GradeRootChanceCultivationBreakthrough
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + Math.round(r.weight / rootTotal * 100) + '%×' + r.xp + '' + (r.odds >= 0 ? '+' : '') +\n Math.round(r.odds * 100) + '%
';\n return h;\n});\n\n/* --- 5. elemental mastery ------------------------------------------------- */\nMOD_wikiPage('mastery', 'Elemental mastery', function () {\n var h = '

Elemental mastery mod

' +\n '

Six masteries, each with a technique that fires on its own ' +\n 'during a fight. Combat is automatic and there is no ability picker, so a ' +\n 'technique is a chance on each of your swings to become something else — one ' +\n 'that scales with INT and the element rather than with your weapon.

' +\n '

Nothing fires while you are still Mortal: the channels have to be open ' +\n 'first. Chance is ' + (MOD_TECH.perLevel * 100).toFixed(1) + '% per mastery ' +\n 'level plus ' + (MOD_TECH.perRealm * 100).toFixed(1) + '% per realm, capped at ' +\n Math.round(MOD_TECH.cap * 100) + '%; a technique lands for ×' +\n MOD_TECH.power + ' of a normal strike.

' +\n '' +\n '';\n\n var realm = 0;\n try { realm = MOD_realm().n; } catch (e) {}\n MOD_MASTERY.forEach(function (m) {\n var lvl = Number(m.skill.lvl) || 0;\n var ch = realm < 1 ? 0\n : Math.min(lvl * MOD_TECH.perLevel + realm * MOD_TECH.perRealm, MOD_TECH.cap);\n h += '' +\n '' +\n '' +\n '';\n });\n h += '
MasteryTechniqueLevelChance now
' +\n MOD_WIKI.safe(m.skill.name) + '' + MOD_WIKI.safe(m.name) + '' + lvl + '' + (realm < 1 ? 'Mortal'\n : (ch * 100).toFixed(1) + '%') + '
';\n return h;\n});\n\n/* --- 6. titles ------------------------------------------------------------ */\nMOD_wikiPage('titles', 'Titles', function () {\n var perm = MOD_permanentRank();\n var held = MOD_titleCount();\n var total = 0; for (var kk in ttl) if (ttl[kk] && ttl[kk].name) total++;\n\n var h = '

Titles

' +\n '

A title\\'s rank runs 1 to 10 and is derived from the level ' +\n 'that earns it, so a rank is a statement about where in the game you are. ' +\n 'Only the title you are wearing applies — until renown makes the lower ranks ' +\n 'permanent.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '
Titles held' + held + ' of ' + total + '
Renown level' + (skl.rnwn ? skl.rnwn.lvl : 0) +\n ' of ' + MOD_RENOWN_MAX + '
Ranks that apply without wearing' +\n (perm >= 1 ? '1–' + perm : 'none yet') + '
' +\n\n '

Ranks

' +\n '';\n MOD_RANK_AT.forEach(function (lv, i) {\n var st = MOD_rankStyle(i + 1);\n h += '' +\n '';\n });\n h += '
RankEarned from levelColour
' + (i + 1) + '' + lv + '' + '●●●' + '
' +\n\n '

Renown

' +\n '

Renown is not trained. It counts the titles you hold, and each level ' +\n 'multiplies every stat by ' + (1 + MOD_RENOWN_RATE) + ' — compounding, so ' +\n 'level ' + MOD_RENOWN_MAX + ' is ×' +\n (Math.round(MOD_renownMult(MOD_RENOWN_MAX) * 1000) / 1000) + '. At renown N, ' +\n 'every title of rank N or below applies permanently.

' +\n '' +\n '';\n MOD_RENOWN_STEPS.forEach(function (n, i) {\n h += '= n ? 'done' : '') + '\">' +\n '';\n });\n h += '
RenownTitles neededAll statsPassive ranks
' + (i + 1) +\n '' + n + '×' +\n (Math.round(MOD_renownMult(i + 1) * 1000) / 1000) + '1–' + (i + 1) + '
' +\n\n '

Every title

' +\n '

Grouped by the skill that grants it. What is left is either ' +\n 'a ladder of its own — creatures killed, items gathered, the ' +\n 'cultivation realms — or a one-off from the story.

';\n\n var groups = {}, fams = {}, loose = [];\n for (var k in ttl) {\n var t = ttl[k];\n /* Unlike the picker, the wiki KEEPS the author's broken entries and says\n so — same call as area.clg, which is listed and tagged \"nothing spawns\"\n rather than quietly dropped. A reader looking for why a title never\n appears is better served by the row than by its absence. */\n if (!t || t.name === undefined || t.name === null) continue;\n var sk = MOD_TITLE_SKILL[k];\n if (sk && skl[sk]) { (groups[sk] = groups[sk] || []).push(k); continue; }\n var stem = MOD_titleStem(k);\n if (stem) { (fams[stem] = fams[stem] || []).push(k); continue; }\n loose.push(k);\n }\n /* Same rule as the picker: a family of one is not a family. */\n Object.keys(fams).forEach(function (stem) {\n if (fams[stem].length < 2) { loose.push(fams[stem][0]); delete fams[stem]; }\n });\n var order = Object.keys(groups).sort(function (a, b) {\n return skl[a].name < skl[b].name ? -1 : 1;\n });\n\n var row = function (k) {\n var t = ttl[k];\n var st = MOD_rankStyle(t.rar);\n var gen = null;\n for (var i = 0; i < MOD_SKILL_TITLES.length; i++) {\n if (MOD_SKILL_TITLES[i].key === k) { gen = MOD_SKILL_TITLES[i]; break; }\n }\n /* The author left a handful unfinished: ttl.ddcd carries the literal string\n \"null\" for both name and description, and shpt2/shpt3/mone3 are blank.\n None is granted by anything, so none can be earned. */\n var broken = !t.name || t.name === 'null';\n return '
' +\n '' +\n MOD_WIKI.safe(t.name || '(unnamed)') + '' +\n (broken ? 'unfinished' : '') +\n 'rank ' + (t.rar || 1) + '' +\n (gen ? 'lv ' + gen.rung + ' · +' +\n Math.round(gen.xp * 100) + '% ' + MOD_WIKI.safe(skl[gen.skill].name) +\n ' exp' : '') +\n (t.have === true ? 'held' : '') +\n '
' + MOD_wikiDesc(t.desc) + '
';\n };\n\n var byRank = function (a, b) { return (ttl[a].rar || 1) - (ttl[b].rar || 1); };\n order.forEach(function (sk) {\n groups[sk].sort(byRank);\n h += MOD_wikiGroup(MOD_WIKI.safe(skl[sk].bname || skl[sk].name),\n groups[sk].length, groups[sk].map(row).join(''), 3);\n });\n /* The ladders that belong to no skill, by the same derivation the picker\n uses — so the two cannot drift apart. Sorted by the label, since that is\n what the reader scans. */\n var famList = Object.keys(fams)\n .map(function (stem) {\n fams[stem].sort(byRank);\n return { stem: stem,\n label: MOD_titleFamilyLabel(stem, fams[stem].map(function (k) { return ttl[k]; })) };\n })\n .sort(function (a, b) { return a.label < b.label ? -1 : a.label > b.label ? 1 : 0; });\n if (famList.length) {\n /* Nested one level down rather than sitting beside the skills. Eleven more\n top-level summaries cost ~530px on a page that is already the tallest in\n the wiki, and these are a different KIND of thing from a skill anyway. */\n var famInner = '', famTotal = 0;\n famList.forEach(function (f) {\n famTotal += fams[f.stem].length;\n famInner += MOD_wikiGroup(MOD_WIKI.safe(f.label), fams[f.stem].length,\n fams[f.stem].map(row).join(''), 4);\n });\n h += MOD_wikiGroup('Ladders of their own', famTotal, famInner, 3);\n }\n if (loose.length) {\n loose.sort(byRank);\n /* The story titles are the one group with no skill to divide them, and\n there are enough of them to need dividing. Rank rather than the alphabet:\n it is the axis they are already sorted on, and the one a reader cares\n about — a rank is a statement about where in the game a title comes from. */\n var inner;\n if (loose.length <= MOD_WIKI_ALPHA_AT) {\n inner = loose.map(row).join('');\n } else {\n var byRankNo = {};\n loose.forEach(function (k) {\n var r = Math.min(Math.max(Number(ttl[k].rar) || 1, 1), 10);\n (byRankNo[r] = byRankNo[r] || []).push(k);\n });\n inner = '';\n Object.keys(byRankNo).sort(function (a, b) { return a - b; }).forEach(function (r) {\n var st = MOD_rankStyle(r);\n inner += MOD_wikiGroup('Rank ' + r + '', byRankNo[r].length,\n byRankNo[r].map(row).join(''), 4);\n });\n }\n h += MOD_wikiGroup('From the story', loose.length, inner, 3);\n }\n return h;\n});\n\n/* --- 7. areas ------------------------------------------------------------- */\nMOD_wikiPage('areas', 'Areas & enemies', function () {\n var h = '

Areas & enemies

' +\n '

Every area, what lives in it and at what levels, and what it ' +\n 'drops. Enemy strength is not read off these levels — see the note below the ' +\n 'table.

';\n\n /* Two of the game's 21 areas are not places and listing them misleads:\n nwh \"Somewhere\" — where global.current_z parks whenever you are NOT in\n a fight, populated by creature.default (\"Nothing\").\n tst \"Test\" — reached only from chss.tst, which has id -1 and is in no\n sector, so nothing in the game can travel there.\n The test for \"is this a place\" is whether you can get to it, and neither\n can be. Everything else is listed whether or not you have been. */\n var MOD_WIKI_NOT_PLACES = { nwh: 1, tst: 1 };\n\n var keys = [];\n for (var k in area) {\n if (area[k] && area[k].pop && !MOD_WIKI_NOT_PLACES[k]) keys.push(k);\n }\n /* Ordered by the middle of the level band, which is the closest thing the\n game has to a difficulty order. */\n keys.sort(function (a, b) {\n var mid = function (z) {\n if (!z.pop.length) return 0;\n var s = 0; z.pop.forEach(function (e) { s += (e.lvlmin + e.lvlmax) / 2; });\n return s / z.pop.length;\n };\n return mid(area[a]) - mid(area[b]);\n });\n\n keys.forEach(function (k) {\n var z = area[k];\n /* An area whose baked bands are not finite can never spawn anything — see\n the note rendered under its table. Derived rather than listed by name, so\n it stays true if the game changes. */\n var broken = !z.popc || z.popc.length !== z.pop.length ||\n z.popc.some(function (band) { return !band || !isFinite(band[1] - band[0]); });\n h += '

' + MOD_WIKI.safe(z.name) +\n ' ' + k + '' +\n (broken ? 'nothing spawns' : '') +\n '' + (z.size === -1 ? 'endless' : z.size + ' to clear') +\n '

';\n if (z.pop.length) {\n /* The spawn share is NOT pop[i].c. z_bake normalises those weights into\n area.popc — a list of [lo,hi] bands that mon_gen rolls a uniform\n random against — and the weights do not have to sum to 1, so they are\n off wherever they don't. The Southern forest's .35/.45/.25 are really\n 33/43/24%. Read the bands the game actually rolls against. */\n var bands = z.popc || [];\n h += '';\n z.pop.forEach(function (e, i) {\n var c = e.crt || {};\n var band = bands[i];\n var share = band ? band[1] - band[0] : NaN;\n h += '' +\n '' +\n '';\n });\n h += '
' + MOD_WIKI.safe(c.name || '?') + 'lv ' + e.lvlmin +\n (e.lvlmax !== e.lvlmin ? '–' + e.lvlmax : '') + '' + (isFinite(share)\n ? Math.round(share * 100) + '% of spawns'\n : '—') + '
';\n if (broken) {\n h += '
Nothing spawns here. The area\\'s entries carry ' +\n 'no spawn weight, so the table the game rolls against comes out as ' +\n 'NaN and no creature can ever match it. A bug in the base game, left ' +\n 'as it is — nothing in the game travels to this area either.
';\n }\n }\n if (z.drop && z.drop.length) {\n var d = z.drop.map(function (x) {\n return MOD_WIKI.safe((x.item && x.item.name) || '?') +\n ' ' + ((x.c || 0) * 100).toFixed(2) + '%';\n }).join(', ');\n h += '
Drops: ' + d + '
';\n }\n h += '
';\n });\n\n var fightsOrig = MOD_FIGHTS.mode === 'original';\n h += '

How hard an enemy is

' +\n '

It depends on the Fights setting, which is ' +\n (fightsOrig ? 'Original' : 'Scaled') + ' now. On Original (the default) ' +\n 'every area the author wrote is exactly as he wrote it: his creatures, his ' +\n 'level ranges, and you out-grow them as you do in his game. On Scaled every ' +\n 'area is sized to you. The three areas past the golem arena and the rank ' +\n 'duels are sized to you either way.

' +\n '

Sizing to you works like this. Enemies are not fitted to a level. Each spawn is solved against your ' +\n 'actual power at the moment it appears, aiming at roughly ' + MOD_ENEMY.kill +\n ' swings to kill it and ' + MOD_ENEMY.die + ' before it kills you, with a ' +\n 'guaranteed margin between the two so a fight is never a coin flip. Variety ' +\n 'comes from the area\\'s level band and each creature\\'s own stat weighting, ' +\n 'not from difficulty scaling.

' +\n '

The reason for that: within the first stage alone you go from 1 STR to ' +\n 'around 265, so anything anchored to a level is either trivial at the end of ' +\n 'a stage or impossible at the start of one.

';\n return h;\n});\n\n/* --- 8. items -------------------------------------------------------------\n The one place where \"list everything\" needs help: 321 of the game's 371\n plain items are the same inventory type, so grouping by type alone leaves a\n single group of 321. Anything past MOD_WIKI_ALPHA_AT gets an alphabetical\n sub-split as well — the oldest index pattern there is, and derived rather\n than invented, which a hand-made taxonomy would not be.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_ALPHA_AT = 60;\n\n/* The game's own inventory tabs (isort): ALL / WPN / EQP / USE / OTHER. Using\n its categories rather than new ones means a group here is a tab there. */\nvar MOD_WIKI_STYPE = { 2: 'Weapons', 3: 'Equipment', 4: 'Usable', 5: 'Other' };\n\nfunction MOD_wikiItemRow(it) {\n var val = it.v !== undefined ? it.v : (it.val !== undefined ? it.val : null);\n return '
' +\n '

' + MOD_WIKI.safe(it.name) +\n (it.rar > 1 ? 'rarity ' + it.rar + '' : '') +\n (val !== null ? '' + MOD_wikiNum(val) + ' coin' : '') +\n (it.amount > 0 ? '×' + it.amount + '' : '') +\n '

' + MOD_wikiDesc(it.desc) + '
';\n}\n\n/* The letter a name files under. Several items are quoted — the master's\n manuals are literally named \"Sword Saint Manual\" — and filing those under a\n quotation mark is no use to anyone looking for S. */\nfunction MOD_wikiLetter(name) {\n var m = String(name || '').replace(/^[^0-9A-Za-z]+/, '');\n return (m.charAt(0) || '?').toUpperCase();\n}\n\nfunction MOD_wikiByLetter(a, b) {\n var x = MOD_wikiLetter(a.name) + String(a.name || '').toLowerCase();\n var y = MOD_wikiLetter(b.name) + String(b.name || '').toLowerCase();\n return x < y ? -1 : (x > y ? 1 : 0);\n}\n\n/* Flat below the threshold, alphabetically sub-grouped above it. Ranges are cut\n from the entries actually present, so they stay even as content is added. */\nfunction MOD_wikiAlpha(entries, render, level) {\n if (entries.length <= MOD_WIKI_ALPHA_AT) return entries.map(render).join('');\n var per = Math.ceil(entries.length / Math.ceil(entries.length / 40));\n var out = '';\n for (var i = 0; i < entries.length; i += per) {\n var chunk = entries.slice(i, i + per);\n var first = MOD_wikiLetter(chunk[0].name);\n var last = MOD_wikiLetter(chunk[chunk.length - 1].name);\n out += MOD_wikiGroup(first === last ? first : first + '–' + last,\n chunk.length, chunk.map(render).join(''), level || 4);\n }\n return out;\n}\n\nMOD_wikiPage('items', 'Items', function () {\n var h = '

Items

' +\n '

Everything the game defines, whether or not you have seen ' +\n 'it. Grouped the way your inventory groups it. Values are the base sell ' +\n 'price where the game gives one.

';\n\n /* item[] first, split by the inventory tab it appears under. */\n var buckets = {};\n for (var k in item) {\n var it = item[k];\n if (!it || !it.name || it.name === 'dummy') continue;\n var lab = MOD_WIKI_STYPE[it.stype] || 'Uncategorised';\n (buckets[lab] = buckets[lab] || []).push(it);\n }\n ['Usable', 'Other', 'Weapons', 'Equipment', 'Uncategorised'].forEach(function (lab) {\n var list = buckets[lab];\n if (!list || !list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(lab, list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n\n /* and the equipment objects, which live in their own globals */\n [['Weapons', wpn], ['Armour', eqp], ['Shields', sld], ['Accessories', acc]]\n .forEach(function (g) {\n var list = [];\n for (var kk in g[1]) {\n if (g[1][kk] && g[1][kk].name && g[1][kk].name !== 'dummy') list.push(g[1][kk]);\n }\n if (!list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(g[0], list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n return h;\n});\n\n/* --- 9. actions ----------------------------------------------------------- */\nMOD_wikiPage('actions', 'Actions', function () {\n var unlock = {};\n MOD_ACTION_UNLOCKS.forEach(function (u) {\n if (u.act) unlock[u.act.id] = (skl[u.skill] ? skl[u.skill].name : u.skill) +\n ' level ' + u.lv;\n });\n if (typeof act.mod_qi !== 'undefined') {\n unlock[act.mod_qi.id] = 'clearing the dojo\\'s Easiest, Easy and Normal fights';\n }\n\n var h = '

Actions

' +\n '

What you can be doing. One at a time, and only where the ' +\n 'action makes sense — unless \"Unrestricted actions\" is ticked in settings, ' +\n 'which lets several run at once anywhere. Both limits are deliberate; the ' +\n 'switch is there because they are not to everyone\\'s taste.

';\n\n var keys = [];\n for (var k in act) if (act[k] && act[k].name && act[k].name !== 'dummy') keys.push(k);\n keys.sort(function (a, b) { return act[a].name < act[b].name ? -1 : 1; });\n\n keys.forEach(function (k) {\n var a = act[k];\n var how = unlock[a.id];\n h += '
' +\n '

' + MOD_WIKI.safe(a.name) +\n (a.have === true ? 'unlocked'\n : 'locked') +\n (how ? 'from ' + MOD_WIKI.safe(how) + '' : '') +\n '

' + MOD_wikiDesc(a.desc) + '
';\n });\n return h;\n});\n\n/* --- 10. the dojo --------------------------------------------------------- */\nMOD_wikiPage('dojo', 'The dojo', function () {\n var h = '

The dojo

' +\n '

The instructor promises a reward every five levels. The base ' +\n 'game stops delivering at 30; the mod carries the same ladder to 110, on the ' +\n 'same terms.

' +\n '

Only the lowest rung you have not claimed is offered, which is how the ' +\n 'first six behave. Rewards are sized against the character experience curve ' +\n '(4×level3 + level2) — a level at 110 costs 5.3 ' +\n 'million, which is why the base game\\'s best pill is worth 0.3% of one there.

' +\n '' +\n '';\n\n MOD_DOJO_RUNGS.forEach(function (r) {\n var pill = item[r.pill];\n var extra = [];\n if (r.realmPill) extra.push('breakthrough pill for realm ' + r.realmPill);\n if (r.manual) extra.push('a choice of master\\'s manual');\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
LevelRewardCoinAlso
' + r.lv + '' + (pill ? MOD_WIKI.safe(pill.name) + ' ×' + r.n\n : '—') + '' + MOD_wikiNum(r.coin) + '' + (extra.join(', ') || '—') + '' + (global.flags[r.flag] ? 'claimed' : '') +\n '
' +\n\n '

Spirit pills

' +\n '

Character experience in a bottle. The four the mod adds continue the ' +\n 'game\\'s own series up to the new curve.

' +\n '' +\n '';\n MOD_PILLS.forEach(function (p) {\n h += '';\n });\n h += '
PillExperience
' + MOD_WIKI.safe(p[2]) + '' +\n p[3].toLocaleString() + '
' +\n\n '

Master\\'s manuals

' +\n '

A third grade of skillbook, offered at levels 50, 75 and 100 as a choice ' +\n 'the same way the first one was. Each is worth +' +\n Math.round(MOD_MANUAL_RATE * 100) + '% experience in its skill, permanently.

' +\n '' +\n '';\n MOD_MANUALS.forEach(function (m) {\n h += '';\n });\n h += '
ManualSkill
' + MOD_WIKI.safe(m[2]) + '' +\n MOD_WIKI.safe(skl[m[0]] ? (skl[m[0]].bname || skl[m[0]].name) : m[0]) +\n '
';\n return h;\n});\n\n/* --- 11. mechanics -------------------------------------------------------- */\nMOD_wikiPage('mechanics', 'Mechanics', function () {\n return '

Mechanics

' +\n\n '

Damage is subtractive, in both directions

' +\n '

A hit does the attacker\\'s strength minus the defender\\'s strength, plus ' +\n 'the weapon, plus one, before criticals. A defender\\'s STR is their ' +\n 'armour — there is no separate armour stat doing that job.

' +\n '

Two consequences worth carrying around:

' +\n '
    ' +\n '
  • Damage does not taper, it stops. Once a defender\\'s STR passes ' +\n 'yours, your hits do not get small; they land for nothing. Progress against ' +\n 'something too strong is not slow, it is zero.
  • ' +\n '
  • A stat ratio tells you nothing. Comparing your HP to an enemy\\'s ' +\n 'strength ignores the subtraction entirely and reads several times off in ' +\n 'either direction.
  • ' +\n '
' +\n '

This is also the reason enemies are never scaled by raising ' +\n 'their strength: it would raise their offence and their armour together, and ' +\n 'the second effect arrives first.

' +\n\n '

Criticals carry the late game

' +\n '

Critical rate reaches about a third at the top of the ladder and a ' +\n 'critical is roughly eight times a normal swing, so around three quarters of ' +\n 'all damage done at that point is critical damage. Anything that raises ' +\n 'critical rate or critical damage is worth more than it looks; Sharp Eye and ' +\n 'War are the two that do.

' +\n\n '

Hit chance

' +\n '

Landing a hit is a contest of agility. It is a ratio rather than a ' +\n 'subtraction, so agility behaves more gently than strength does — falling ' +\n 'behind on it costs you accuracy, not everything.

' +\n\n '

Skill sections and parents

' +\n '

Each of the ' + Object.keys(MOD_SECTIONS).length + ' sections has a parent ' +\n 'skill. A parent earns ' + Math.round(MOD_PARENT.feed * 100) + '% of everything ' +\n 'its children earn and grants no stats of its own. In return, a child below ' +\n 'the parent\\'s level trains ' + Math.round(MOD_PARENT.perLevel * 100) +\n '% faster per level of lag, up to ' + MOD_PARENT.maxGap + ' levels behind. ' +\n 'Bringing one section up makes every skill in it cheaper to catch up.

' +\n\n '

Saving

' +\n '

Three slots, switched from the bottom bar. The game builds its world once ' +\n 'at startup and cannot unload a save, so switching slots or starting a new ' +\n 'game reloads the page. Exporting a save before running anything unusual is ' +\n 'always the safe move.

';\n});\n\n/* --- the document ---------------------------------------------------------\n One string, no fetches, no dependencies: this has to work from file:// as\n readily as from a server.\n ------------------------------------------------------------------------- */\n\nfunction MOD_wikiCss() {\n return [\n ':root{color-scheme:dark}',\n '*{box-sizing:border-box}',\n /* Menlo and Consolas before the generic: with MS Gothic missing (any Mac)\n \"monospace\" is Courier, which Safari sets wider than Chrome does, and\n table cells wrapped on one and not the other. */\n 'body{margin:0;background:#060a1c;color:#dfe6f5;font:14px/1.6 \"MS Gothic\",' +\n '\"MS Mincho\",Menlo,Consolas,\"DejaVu Sans Mono\",monospace;display:flex;min-height:100vh}',\n 'a{color:#7cb0ff}',\n 'nav{width:210px;flex:0 0 210px;background:#081040;border-right:1px solid #46a;' +\n 'position:sticky;top:0;height:100vh;overflow:auto;padding:14px 0}',\n 'nav h2{font-size:13px;color:#9fb4d8;margin:0 0 10px 14px;letter-spacing:1px}',\n 'nav a{display:block;padding:6px 14px;text-decoration:none;color:#cfe0ff;' +\n 'border-left:3px solid transparent}',\n 'nav a:hover{background:#154080}',\n 'nav a.on{background:#123070;border-left-color:#7cb0ff;color:#fff}',\n 'nav .srch{margin:0 10px 12px;width:calc(100% - 20px);background:#050a20;' +\n 'color:#dfe6f5;border:1px solid #46a;padding:5px;font:inherit}',\n 'nav .xall{display:block;margin:12px 10px 0;color:#8296b8;font-size:12px;' +\n 'cursor:pointer;-webkit-user-select:none;user-select:none}',\n 'nav .xall input{vertical-align:-1px;margin-right:4px}',\n 'details.wk-g{border:1px solid #17224a;background:#0a1130;margin:8px 0}',\n 'details.wk-g>summary{cursor:pointer;padding:7px 12px;list-style:none;' +\n 'background:#0d1740}',\n 'details.wk-g>summary:hover{background:#154080}',\n 'details.wk-g>summary::-webkit-details-marker{display:none}',\n // a caret drawn rather than borrowed, so it is the same in every browser\n 'details.wk-g>summary::before{content:\"\\\\25B8\";color:#7cb0ff;' +\n 'display:inline-block;width:1em;transition:transform .12s}',\n 'details.wk-g[open]>summary::before{transform:rotate(90deg)}',\n 'details.wk-g>summary h2,details.wk-g>summary h3{display:inline;margin:0;' +\n 'font-size:15px;color:#fff}',\n '.wk-gi{padding:2px 12px 10px}',\n 'details.wk-g details.wk-g>summary h4{display:inline;margin:0;font-size:13px;' +\n 'color:#cfe0ff;font-weight:normal}',\n 'details.wk-g details.wk-g>summary{padding:5px 10px;background:#0b1435}',\n 'main{flex:1;min-width:0;padding:22px 30px 80px;max-width:1000px}',\n 'h1{font-size:26px;margin:0 0 6px;color:#fff;border-bottom:1px solid #46a;' +\n 'padding-bottom:8px}',\n 'h2{font-size:19px;color:#bcd4ff;margin:30px 0 8px}',\n 'h3{font-size:15px;color:#fff;margin:0 0 4px}',\n 'p.lede{color:#a8bcdc;margin:0 0 18px}',\n '.dim{color:#8296b8;font-weight:normal}',\n '.flav{font-size:12px;font-style:italic}',\n '.note{background:#0b1638;border-left:3px solid #46a;padding:8px 12px;' +\n 'color:#a8bcdc;margin:14px 0}',\n 'code,pre{background:#0b1638;border:1px solid #24356e;padding:1px 5px;' +\n 'color:#9fd0ff}',\n 'pre{padding:10px;overflow-x:auto}',\n 'dl dt{color:#fff;margin-top:14px}',\n 'dl dd{margin:2px 0 0 0;color:#a8bcdc}',\n 'ul{color:#a8bcdc}',\n 'table.wk-tbl{border-collapse:collapse;width:100%;margin:10px 0;' +\n 'display:block;overflow-x:auto}',\n 'table.wk-tbl th{text-align:left;color:#9fb4d8;font-weight:normal;' +\n 'border-bottom:1px solid #46a;padding:5px 9px;white-space:nowrap}',\n 'table.wk-tbl td{border-bottom:1px solid #17224a;padding:5px 9px;' +\n 'vertical-align:top}',\n 'table.wk-tbl tr.done td{color:#cfe0ff}',\n 'table.wk-tbl tr.here{background:#123070}',\n '.num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}',\n '.wk-e{border:1px solid #17224a;background:#081040;padding:9px 12px;' +\n 'margin:7px 0;border-radius:2px}',\n 'tr.wk-e{border:0;background:none}',\n '.wk-e.done{border-color:#2a4a8c}',\n '.wk-e h3{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}',\n '.lv{margin-left:auto;font-size:12px;color:#8296b8;font-weight:normal}',\n '.lv.capped{color:#ffd24a}',\n '.rank{font-size:12px;color:#8296b8;font-weight:normal}',\n '.desc{color:#a8bcdc;font-size:13px}',\n '.proj{color:#ffd24a;font-size:12px;margin-top:3px}',\n '.tag{font-size:11px;border:1px solid #46a;color:#9fb4d8;padding:0 5px;' +\n 'border-radius:2px;font-weight:normal}',\n '.tag.mod{border-color:#7a4ea8;color:#c07bff}',\n '.tag.done{border-color:#2e7d4f;color:#6fdc9a}',\n '.tag.here{border-color:#ffd24a;color:#ffd24a}',\n '.tag.warn{border-color:#a8562e;color:#ff9a5c}',\n 'table.wk-perks{border-collapse:collapse;margin:6px 0 0;width:100%}',\n 'table.wk-perks td{padding:2px 8px 2px 0;font-size:12px;color:#8296b8;' +\n 'border-bottom:1px solid #101a3c}',\n 'table.wk-perks tr.done td{color:#cfe0ff}',\n 'table.wk-perks td.tick{color:#6fdc9a;width:1em;text-align:right}',\n // fixed columns so the level bands line up between one area and the next\n 'table.wk-pop{table-layout:fixed}',\n 'table.wk-pop td:first-child{width:60%}',\n 'table.wk-pop td:nth-child(2){width:20%}',\n '.wk-title .tname{font-weight:bold}',\n '.wk-now{background:#0b1638;border:1px solid #24356e;padding:10px 14px;' +\n 'margin:0 0 18px;display:inline-block;min-width:320px}',\n '.wk-now h3{color:#9fb4d8;font-weight:normal;font-size:13px;' +\n 'letter-spacing:1px;margin-bottom:6px}',\n '.wk-now td{padding:2px 16px 2px 0}',\n '.wk-now td:last-child{color:#fff;text-align:right}',\n '.pg{display:none}.pg.on{display:block}',\n 'footer{color:#5a6c8c;font-size:12px;border-top:1px solid #17224a;' +\n 'margin-top:40px;padding-top:12px}',\n '@media(max-width:760px){body{display:block}nav{width:auto;height:auto;' +\n 'position:static;flex:none}main{padding:16px}}'\n ].join('\\n');\n}\n\n/* Filtering, page switching and the hash. Small enough to inline, and inlining\n it is the point — the document has to stand on its own. */\nfunction MOD_wikiJs() {\n return [\n 'var pages=[].slice.call(document.querySelectorAll(\".pg\"));',\n 'var links=[].slice.call(document.querySelectorAll(\"nav a[data-p]\"));',\n 'function show(id){',\n ' var found=false;',\n ' pages.forEach(function(p){var on=p.id===\"pg-\"+id;if(on)found=true;',\n ' p.className=\"pg\"+(on?\" on\":\"\")});',\n ' if(!found&&pages.length){pages[0].className=\"pg on\";id=pages[0].id.slice(3)}',\n ' links.forEach(function(a){a.className=a.dataset.p===id?\"on\":\"\"});',\n ' window.scrollTo(0,0);return id;}',\n 'links.forEach(function(a){a.addEventListener(\"click\",function(e){',\n ' e.preventDefault();location.hash=a.dataset.p;show(a.dataset.p)})});',\n 'window.addEventListener(\"hashchange\",function(){show(location.hash.slice(1))});',\n 'show(location.hash.slice(1)||\"start\");',\n // Search filters entries, hides groups left with none, and OPENS the groups\n // that still have some — a match hidden inside a collapsed group reads as\n // no match at all, which is worse than not having search.\n 'var box=document.getElementById(\"srch\");',\n 'var xall=document.getElementById(\"xall\");',\n 'var groups=[].slice.call(document.querySelectorAll(\"details.wk-g\"));',\n 'function filter(){',\n ' var q=box.value.trim().toLowerCase();',\n ' pages.forEach(function(p){',\n ' [].slice.call(p.querySelectorAll(\".wk-e\")).forEach(function(e){',\n ' e.style.display=(!q||e.textContent.toLowerCase().indexOf(q)>=0)?\"\":\"none\"})});',\n ' groups.forEach(function(g){',\n ' var any=[].slice.call(g.querySelectorAll(\".wk-e\")).some(function(e){',\n ' return e.style.display!==\"none\"});',\n ' g.style.display=any?\"\":\"none\";',\n ' g.open=xall.checked||(!!q&&any)}); }',\n 'box.addEventListener(\"input\",filter);',\n 'box.addEventListener(\"keydown\",function(e){if(e.key===\"Escape\"){box.value=\"\";filter()}});',\n // Everything is collapsed by default because three of these pages run past\n // 40,000px open. This is the way back to one long document, which is what\n // the browser's own find needs.\n 'xall.addEventListener(\"change\",filter);'\n ].join('\\n');\n}\n\nfunction MOD_wikiBuild() {\n var nav = '', body = '';\n MOD_WIKI_PAGES.forEach(function (pg) {\n nav += '' +\n MOD_WIKI.safe(pg.label) + '';\n var inner;\n /* One page throwing must not take the wiki down with it — the page says so\n and the rest still renders. */\n try { inner = pg.build(); }\n catch (e) {\n inner = '

' + MOD_WIKI.safe(pg.label) + '

This page ' +\n 'could not be built: ' + MOD_WIKI.safe(e.message) + '

';\n console.warn('[mod] wiki page \"' + pg.id + '\" failed: ' + e.message);\n }\n body += '
' + inner + '
';\n });\n\n var stamp = new Date().toLocaleString();\n return '' +\n '' +\n '' + MOD_WIKI.safe(MOD_WIKI.title) + '' +\n '' +\n '' +\n '
' + body +\n '
Generated from the running game at ' + MOD_WIKI.safe(stamp) +\n ' — mod ' + MOD_WIKI.safe(MOD.version) + '. Everything here is read from the ' +\n 'live data, including your own save, so it cannot fall out of step with the ' +\n 'game. The game is truezhangwei\\'s; the mod is not endorsed by the author.' +\n '
' +\n ' inside content closing the block early, so that is the\n one thing scrubbed. */\n safe: function (h) {\n return String(h === undefined || h === null ? '' : h).replace(/<\\/script/gi, '<\\\\/script');\n }\n};\n\n/* Descriptions carry real HTML (coloured spans, the game's separator div), so\n they are passed through rather than escaped — but the separator becomes a\n rule, since a wiki row is not a tooltip. */\nfunction MOD_wikiDesc(d) {\n var s;\n try { s = (typeof d === 'function') ? d() : d; } catch (e) { s = ''; }\n if (!s) return '';\n s = String(s).split('
 
').join('
');\n return MOD_WIKI.safe(s);\n}\n\nfunction MOD_wikiNum(n) {\n if (!isFinite(n)) return '—';\n return Math.round(n).toLocaleString();\n}\n\n/* An effect line at a level the player has not reached yet. The effect\n functions read the skill they are handed, so the level is borrowed and put\n back; anything that throws simply gets no projection. */\nfunction MOD_wikiEffectAt(key, sk, lvl) {\n var eff = MOD_EFFECTS[key];\n if (!eff) return '';\n var was = sk.lvl, out = '';\n try { sk.lvl = lvl; out = eff(sk) || ''; }\n catch (e) { out = ''; }\n finally { sk.lvl = was; }\n return MOD_WIKI.safe(out);\n}\n\n/* --- page builders --------------------------------------------------------\n Each returns the inner HTML of one page. `wk-e` marks a searchable entry and\n `wk-g` a group that hides itself when all its entries are filtered out.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_PAGES = [];\n\nfunction MOD_wikiPage(id, label, build) {\n MOD_WIKI_PAGES.push({ id: id, label: label, build: build });\n}\n\n/* A collapsible group.\n\n Not decoration: with every skill, item and title listed, three of the eleven\n pages ran past 40,000 pixels — a hundred screens of scrolling, which is the\n same as not having the content at all. Collapsed, each of those pages opens\n as an index you can read in one screen and expand where you want.\n\n
rather than a click handler, so it still works with JavaScript off,\n the browser's own find can be made to reach inside it, and the summary is\n keyboard-reachable for free. `level` is the heading level for the summary,\n since a group is sometimes a section of a page and sometimes a subsection. */\nfunction MOD_wikiGroup(heading, count, inner, level) {\n var hn = 'h' + (level || 2);\n return '
<' + hn + '>' + heading +\n (count !== null && count !== undefined\n ? ' (' + count + ')' : '') +\n '
' + inner + '
';\n}\n\n/* --- 1. orientation ------------------------------------------------------- */\nMOD_wikiPage('start', 'Start here', function () {\n var cap = MOD_levelCap();\n var realm = MOD_realm();\n var h = '

proto23

' +\n '

An idle RPG by truezhangwei, plus a mod. You train skills by ' +\n 'doing things, the things you can do open up as the story does, and none of ' +\n 'it is in a hurry.

' +\n\n '

Where you are right now

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Character level' + MOD_wikiNum(you.lvl) + '
Skill level cap' + cap + ' — ' +\n MOD_WIKI.safe(MOD_CAP.name) + '
Cultivation realm' + MOD_WIKI.safe(realm.name) +\n ' (' + realm.n + ' of ' + (MOD_REALMS.length - 1) + ')
Titles held' + MOD_titleCount() + ' — renown ' +\n (skl.rnwn ? skl.rnwn.lvl : 0) + '
Coin' + MOD_wikiNum(you.wealth || 0) + '
' +\n\n '

The four things worth knowing early

' +\n '
' +\n '
Your skill levels are capped by the story, not by grinding.
' +\n '
The cap starts at 10 and ends at 110, and it moves when you reach a new ' +\n 'place — not when you earn enough experience. If a skill has stopped ' +\n 'levelling, you are not doing it wrong; you need to go somewhere. See ' +\n 'Progression.
' +\n\n '
Armour is subtraction, so levels matter more than they look.
' +\n '
A hit does the attacker\\'s STR minus the defender\\'s STR. That means a ' +\n 'small gap in strength is a large gap in damage, and a big enough one takes ' +\n 'your damage to nothing at all rather than merely reducing it. See ' +\n 'Mechanics.
' +\n\n '
Most skills have a parent that pulls them along.
' +\n '
Every section has a parent skill that earns a share of everything its ' +\n 'children do, and any child lagging behind the parent trains faster — up to ' +\n 'twice as fast at ' + MOD_PARENT.maxGap + ' levels behind. A neglected skill is ' +\n 'cheap to bring up later, which is the only reason having this many is ' +\n 'workable.
' +\n\n '
Cultivation is a second ladder, and it is not automatic.
' +\n '
Circulating qi levels a skill; the skill does not raise your realm. ' +\n 'Reaching the level opens a bottleneck, and breaking through it costs a pill ' +\n 'and can fail. See Cultivation.
' +\n '
' +\n\n '

What is the mod and what is the game

' +\n '

The game is index.html and it is entirely truezhangwei\\'s ' +\n 'work. Everything in mod.js is a modification of it and is not ' +\n 'endorsed by the author. Pages here mark added content with ' +\n 'mod where the distinction matters. The full ' +\n 'list of changes is in the changelog, on the bottom bar.

';\n return h;\n});\n\n/* --- 2. progression ------------------------------------------------------- */\nMOD_wikiPage('progress', 'Progression', function () {\n var cap = MOD_levelCap();\n var h = '

Progression

' +\n '

Skill levels are capped by how far the story has gone. Ten ' +\n 'rungs, 10 through 110. The cap applies to every skill at once; Renown is ' +\n 'the single exception, since it is counted rather than trained.

' +\n '' +\n '';\n\n MOD_TIERS.forEach(function (t) {\n var got = null;\n try { got = MOD_tierFlag(t); } catch (e) {}\n var here = (t.cap === cap);\n var need = t.needText ? t.needText\n : (t.flags.length ? t.flags.join(' or ') : 'the start of the game');\n h += '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
CapStageWhat opens it
' + t.cap + '' + MOD_WIKI.safe(t.name) + '' + MOD_WIKI.safe(need) + '' + (here ? 'you are here'\n : got ? 'reached' : '') + '
' +\n\n '

The experience curve

' +\n '

The game\\'s own skill curve is super-exponential — level 109 to 110 ' +\n 'alone costs ' + MOD_wikiNum(MOD_vanillaExpnext(109)) + ' experience, so the top of the ladder ' +\n 'is unreachable by a factor of about ten trillion. But the author only ever ' +\n 'designed skills up to level 15, and below that his curve is fine.

' +\n (MOD_XP.vanillaTo > 0\n ? '

So the mod keeps it. Below level ' + MOD_XP.vanillaTo + ' a skill costs ' +\n 'exactly what the original charges; from there it rises by ×' +\n MOD_XP.ratio.toFixed(4) + ' a level, a ratio solved so the climb to 110 takes ' +\n 'the same time it always did.'\n : '

You have chosen the Mod before 4.2 pacing: a plain geometric curve, ' +\n '×' + MOD_XP.ratio.toFixed(3) + ' a level from level 0, much faster than the ' +\n 'original early on. The Original pacing in settings puts back the ' +\n 'author\\'s own cost below level 10.') +\n ' Character level uses the game\\'s own curve, untouched.

' +\n '' +\n '';\n [1, 5, 9, 10, 15, 25, 50, 75, 90, 109].forEach(function (l) {\n h += '';\n });\n h += '
LevelExperience for the nextOriginal game
' + l +\n '' + MOD_wikiNum(MOD_expnextFor(l)) + '' +\n MOD_wikiNum(MOD_vanillaExpnext(l)) + '
';\n return h;\n});\n\n/* --- 3. skills ------------------------------------------------------------ */\nMOD_wikiPage('skills', 'Skills', function () {\n var cap = MOD_levelCap();\n var bySection = {};\n var keys = [];\n for (var k in skl) {\n var s = skl[k];\n if (!s || typeof s !== 'object' || !s.name) continue;\n keys.push(k);\n }\n keys.forEach(function (k) {\n var s = skl[k];\n var sec = MOD_sectionOf(s);\n (bySection[sec] = bySection[sec] || []).push(k);\n });\n\n var h = '

Skills

' +\n '

' + keys.length + ' skills in ' +\n Object.keys(MOD_SECTIONS).length + ' sections. Every one has perks at ' +\n MOD_LADDER.join(', ') + ', and every one that has milestones grants titles at ' +\n MOD_TITLE_RUNGS.join(', ') + '. Levels and ticks below are yours.

';\n\n Object.keys(MOD_SECTIONS).forEach(function (sec) {\n var list = bySection[sec];\n if (!list || !list.length) return;\n var parentKey = MOD_PARENT_KEY[sec];\n var g = '';\n if (parentKey && skl[parentKey]) {\n g += '

Parent skill: ' +\n MOD_WIKI.safe(skl[parentKey].bname || skl[parentKey].name) +\n ' — earns ' + Math.round(MOD_PARENT.feed * 100) +\n '% of what its children earn, and speeds up any child that lags behind it.

';\n }\n list.sort(function (a, b) { return skl[a].name < skl[b].name ? -1 : 1; });\n list.forEach(function (k) {\n var s = skl[k];\n var lvl = Number(s.lvl) || 0;\n g += '
' +\n '

' + MOD_WIKI.safe(s.bname || s.name) +\n (s.bname ? ' (' + MOD_WIKI.safe(s.name) + ')' : '') +\n '= cap ? ' capped' : '') + '\">lv ' + lvl +\n (lvl >= cap ? ' — at the cap' : '') + '

' +\n '
' + MOD_wikiDesc(s.desc) + '
';\n\n var at110 = MOD_wikiEffectAt(k, s, 110);\n if (at110) g += '
At level 110: ' + at110 + '
';\n\n if (s.mlstn && s.mlstn.length) {\n g += '';\n s.mlstn.forEach(function (m) {\n g += '' +\n '' +\n '' +\n '';\n });\n g += '
' + m.lv + '' + MOD_WIKI.safe(m.p || '—') + '' + (m.g === true ? '✓' : '') + '
';\n }\n g += '
';\n });\n h += MOD_wikiGroup(MOD_WIKI.safe(MOD_SECTIONS[sec]), list.length, g, 2);\n });\n return h;\n});\n\n/* --- 4. cultivation ------------------------------------------------------- */\nMOD_wikiPage('cultivation', 'Cultivation', function () {\n var now = MOD_realm();\n var elig = 0;\n try { elig = MOD_realmEligible().n; } catch (e) {}\n var qic = skl.qic ? skl.qic.lvl : 0;\n\n var h = '

Cultivation

' +\n '

Ten realms above Mortal. Circulating qi levels the Qi ' +\n 'Circulation skill; the skill does not advance the realm. Reaching the level ' +\n 'opens a bottleneck, and breaking through it costs a pill and can fail — the ' +\n 'pill is spent either way.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Realm' + MOD_WIKI.safe(MOD_realmTitle()) + '
Spiritual Root' + MOD_WIKI.safe(MOD_root().name) + '
Qi Circulationlevel ' + qic + '
Insight' + MOD_insight() + '
Bottleneck open' +\n (elig > now.n ? 'yes — ' + MOD_WIKI.safe(MOD_REALMS[now.n + 1].name) +\n ' is within reach' : 'no') + '
Qi Deviation' + (MOD_deviated()\n ? MOD_deviationLeft() + ' in-game minutes left' : 'no') + '
' +\n '

Realm thresholds are the same levels that earn a title rank, so realm N ' +\n 'is the level at which a rank N title arrives. Every realm multiplies all ' +\n 'stats and health, and the multipliers do not stack with each other — the ' +\n 'realm you are in is the one that applies.

' +\n '' +\n '';\n\n MOD_REALMS.forEach(function (r) {\n var pill = r.n >= 1 ? item['mod_bp' + r.n] : null;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmNameQi Circ.StatsHealthPill
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + r.qic + '×' + r.mult + '×' + r.hp + '' + (pill ? MOD_WIKI.safe(pill.name) : '—') +\n '
' +\n\n '

Where the pills come from

' +\n '

Every breakthrough pill has a source in normal play. None of them are ' +\n 'craftable, and none are cheap — a bottleneck you can buy in bulk is not a ' +\n 'bottleneck.

' +\n '' +\n '' +\n '' +\n '' +\n '
RealmsSource
1The dojo instructor, when qi ' +\n 'circulation is first taught
2–5The Herbalist in the village ' +\n '(vendor.pha1), restocked
6–10The dojo\\'s Level Advancement ' +\n 'ladder, and the Pill Tower
' +\n '

Realm 1 is deliberately not sold at the marketplace: ' +\n 'the marketplace is behind a 40%-a-visit chance encounter, and realm 1 opens ' +\n 'long before that is reliable.

' +\n\n '

Breaking through

' +\n '

A bottleneck wants three things, which is what the genre says it wants: ' +\n 'a pill, insight, and enough consolidation to make the ' +\n 'attempt worth making. The pill and the insight are both spent whether the ' +\n 'attempt works or not.

' +\n '

The odds improve the further past the threshold you are, plus your root, ' +\n 'plus time sat in Closed Door Training, and cap short of certain.

' +\n '' +\n '';\n [0, 2, 4, 6, 8].forEach(function (o) {\n var base = Math.min(0.55 + o * 0.05 + MOD_root().odds, 0.95);\n h += '';\n });\n h += '
Levels past the thresholdChanceAfter full seclusion
+' + o + '' +\n Math.round(base * 100) + '%' +\n Math.round(Math.min(base + MOD_CULT.secludeOdds, 0.95) * 100) + '%
' +\n\n '

Insight

' +\n '

Insight is the half of the price that cannot be bought. It comes from ' +\n 'three places, and they are the three the genre names: meditating, fights you ' +\n 'nearly lost, and going somewhere you have not been.

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
SourceRate
Circulate Qi, at a wall' +\n (MOD_CULT.meditateChance * 100).toFixed(1) + '% a second
Closed Door Training' +\n (MOD_CULT.secludeChance * 100).toFixed(1) + '% a second
A kill made under ' +\n Math.round(MOD_CULT.brinkFrac * 100) + '% health — ' +\n (MOD_FIGHTS.mode === 'original'\n ? 'the author\\'s areas rarely come close once you reach them; the three past ' +\n 'the golem arena and the rank duels do, within a few fights, as does an area ' +\n 'you walk into early'\n : 'every fight costs a share of your health, so a few in a row without resting') +\n '' +\n Math.round(MOD_CULT.brinkChance * 100) + '% a kill
Entering an area for the first timealways
' +\n '

The insight a realm asks for is sized to the climb to it: about ' +\n Math.round(MOD_CULT.wallShare * 100) + '% of the Circulate Qi time the climb took, ' +\n 'spent at the wall. Seclusion earns it three times as fast, and full ' +\n 'consolidation takes as long as that on average.

' +\n '' +\n '' +\n '';\n var wkHours = function (sec) {\n return sec >= 7200 ? (sec / 3600).toFixed(sec >= 36000 ? 0 : 1) + ' h' : Math.round(sec / 60) + ' min';\n };\n MOD_REALMS.forEach(function (r) {\n if (!r.n) return;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmClimb (Circulate Qi, 1x)Insight to enterFull seclusionQi Deviation (in-game)
' + MOD_WIKI.safe(r.name) + '' + wkHours(MOD_realmClimbSec(r.n)) + '' + MOD_insightNeed(r.n) + '' + wkHours(MOD_secludeFull(r.n)) + '' + (MOD_devMinutes(r.n) / 1440).toFixed(1) + ' days
' +\n\n '

The nine layers

' +\n '

Each realm is nine layers deep, derived from where Qi Circulation sits ' +\n 'between this realm\\'s threshold and the next one\\'s — so breaking through ' +\n 'puts you at the first layer of the new realm on its own. Layers are worth ' +\n '×' + MOD_CULT.layerMult + ' stats and ×' + MOD_CULT.layerBody +\n ' body each above the first, and the top realm has no rung above it to ' +\n 'measure against, so it stays at its first layer.

' +\n '' +\n '';\n for (var li = 1; li <= MOD_CULT.layers; li++) {\n h += '';\n }\n h += '
LayerCalled
' +\n li + '' + MOD_LAYER_ORD[li] + ' layer, ' +\n MOD_WIKI.safe(MOD_layerStage(li)) + '
' +\n '

At the threshold of the next realm and before breaking ' +\n 'through, you are a half step to it — the genre\\'s own name for the ' +\n 'state this mod calls a bottleneck.

' +\n\n '

Closed Door Training

' +\n '

Seclusion does two things. It consolidates — a full sitting ' +\n '(the table above) is worth +' + Math.round(MOD_CULT.secludeOdds * 100) +\n '% on the attempt — and it shelters: a breakthrough attempted while ' +\n 'you are still sitting in it cannot end in Qi Deviation, only in a lost ' +\n 'pill. It will not start unless you are actually at a wall.

' +\n\n '

Qi Deviation

' +\n '

Failing a breakthrough out in the open leaves your cultivation base ' +\n 'unstable for as long as the seclusion you skipped (the table above, never ' +\n 'under ' + MOD_CULT.devMinutes + ' in-game minutes): stats at ×' +\n MOD_CULT.devStat + ', body at ×' + MOD_CULT.devBody + ', and no second ' +\n 'attempt until it passes. It clears on its own, or at once with a ' +\n 'Qi Settling Pill. It never kills — the death system stays out of it.

' +\n\n '

Heavenly Tribulation

' +\n '

From realm ' + MOD_CULT.tribFrom + ' up, a successful breakthrough is ' +\n 'answered. The cost is a fraction of your maximum health, so it is a gate on ' +\n 'walking in healthy rather than a damage race; the realm bonus you are ' +\n 'standing on is what pays it. Fall to it and the realm does not open.

' +\n '' +\n '';\n MOD_REALMS.forEach(function (r) {\n if (!r.n || r.n < MOD_CULT.tribFrom) return;\n h += '' +\n '' +\n '';\n });\n h += '
RealmBoltsOf your max HP
' + MOD_WIKI.safe(r.name) + '' + MOD_tribulationBolts(r.n) + '' + Math.round(MOD_tribulationCost(r.n) * 100) + '%
' +\n\n '

Spiritual Root

' +\n '

Rolled once, when the dojo finishes teaching you to fight. It is never a ' +\n 'dead end: the worst grade costs cultivation speed, not access, and a ' +\n 'Root Cleansing Pill moves you up a grade.

' +\n '' +\n '';\n var rootTotal = 0;\n MOD_ROOTS.forEach(function (r) { rootTotal += r.weight; });\n MOD_ROOTS.forEach(function (r) {\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
GradeRootChanceCultivationBreakthrough
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + Math.round(r.weight / rootTotal * 100) + '%×' + r.xp + '' + (r.odds >= 0 ? '+' : '') +\n Math.round(r.odds * 100) + '%
';\n return h;\n});\n\n/* --- 5. elemental mastery ------------------------------------------------- */\nMOD_wikiPage('mastery', 'Elemental mastery', function () {\n var h = '

Elemental mastery mod

' +\n '

Six masteries, each with a technique that fires on its own ' +\n 'during a fight. Combat is automatic and there is no ability picker, so a ' +\n 'technique is a chance on each of your swings to become something else — one ' +\n 'that scales with INT and the element rather than with your weapon.

' +\n '

Nothing fires while you are still Mortal: the channels have to be open ' +\n 'first. Chance is ' + (MOD_TECH.perLevel * 100).toFixed(1) + '% per mastery ' +\n 'level plus ' + (MOD_TECH.perRealm * 100).toFixed(1) + '% per realm, capped at ' +\n Math.round(MOD_TECH.cap * 100) + '%; a technique lands for ×' +\n MOD_TECH.power + ' of a normal strike.

' +\n '' +\n '';\n\n var realm = 0;\n try { realm = MOD_realm().n; } catch (e) {}\n MOD_MASTERY.forEach(function (m) {\n var lvl = Number(m.skill.lvl) || 0;\n var ch = realm < 1 ? 0\n : Math.min(lvl * MOD_TECH.perLevel + realm * MOD_TECH.perRealm, MOD_TECH.cap);\n h += '' +\n '' +\n '' +\n '';\n });\n h += '
MasteryTechniqueLevelChance now
' +\n MOD_WIKI.safe(m.skill.name) + '' + MOD_WIKI.safe(m.name) + '' + lvl + '' + (realm < 1 ? 'Mortal'\n : (ch * 100).toFixed(1) + '%') + '
';\n return h;\n});\n\n/* --- 6. titles ------------------------------------------------------------ */\nMOD_wikiPage('titles', 'Titles', function () {\n var perm = MOD_permanentRank();\n var held = MOD_titleCount();\n var total = 0; for (var kk in ttl) if (ttl[kk] && ttl[kk].name) total++;\n\n var h = '

Titles

' +\n '

A title\\'s rank runs 1 to 10 and is derived from the level ' +\n 'that earns it, so a rank is a statement about where in the game you are. ' +\n 'Only the title you are wearing applies — until renown makes the lower ranks ' +\n 'permanent.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '
Titles held' + held + ' of ' + total + '
Renown level' + (skl.rnwn ? skl.rnwn.lvl : 0) +\n ' of ' + MOD_RENOWN_MAX + '
Ranks that apply without wearing' +\n (perm >= 1 ? '1–' + perm : 'none yet') + '
' +\n\n '

Ranks

' +\n '';\n MOD_RANK_AT.forEach(function (lv, i) {\n var st = MOD_rankStyle(i + 1);\n h += '' +\n '';\n });\n h += '
RankEarned from levelColour
' + (i + 1) + '' + lv + '' + '●●●' + '
' +\n\n '

Renown

' +\n '

Renown is not trained. It counts the titles you hold, and each level ' +\n 'multiplies every stat by ' + (1 + MOD_RENOWN_RATE) + ' — compounding, so ' +\n 'level ' + MOD_RENOWN_MAX + ' is ×' +\n (Math.round(MOD_renownMult(MOD_RENOWN_MAX) * 1000) / 1000) + '. At renown N, ' +\n 'every title of rank N or below applies permanently.

' +\n '' +\n '';\n MOD_RENOWN_STEPS.forEach(function (n, i) {\n h += '= n ? 'done' : '') + '\">' +\n '';\n });\n h += '
RenownTitles neededAll statsPassive ranks
' + (i + 1) +\n '' + n + '×' +\n (Math.round(MOD_renownMult(i + 1) * 1000) / 1000) + '1–' + (i + 1) + '
' +\n\n '

Every title

' +\n '

Grouped by the skill that grants it. What is left is either ' +\n 'a ladder of its own — creatures killed, items gathered, the ' +\n 'cultivation realms — or a one-off from the story.

';\n\n var groups = {}, fams = {}, loose = [];\n for (var k in ttl) {\n var t = ttl[k];\n /* Unlike the picker, the wiki KEEPS the author's broken entries and says\n so — same call as area.clg, which is listed and tagged \"nothing spawns\"\n rather than quietly dropped. A reader looking for why a title never\n appears is better served by the row than by its absence. */\n if (!t || t.name === undefined || t.name === null) continue;\n var sk = MOD_TITLE_SKILL[k];\n if (sk && skl[sk]) { (groups[sk] = groups[sk] || []).push(k); continue; }\n var stem = MOD_titleStem(k);\n if (stem) { (fams[stem] = fams[stem] || []).push(k); continue; }\n loose.push(k);\n }\n /* Same rule as the picker: a family of one is not a family. */\n Object.keys(fams).forEach(function (stem) {\n if (fams[stem].length < 2) { loose.push(fams[stem][0]); delete fams[stem]; }\n });\n var order = Object.keys(groups).sort(function (a, b) {\n return skl[a].name < skl[b].name ? -1 : 1;\n });\n\n var row = function (k) {\n var t = ttl[k];\n var st = MOD_rankStyle(t.rar);\n var gen = null;\n for (var i = 0; i < MOD_SKILL_TITLES.length; i++) {\n if (MOD_SKILL_TITLES[i].key === k) { gen = MOD_SKILL_TITLES[i]; break; }\n }\n /* The author left a handful unfinished: ttl.ddcd carries the literal string\n \"null\" for both name and description, and shpt2/shpt3/mone3 are blank.\n None is granted by anything, so none can be earned. */\n var broken = !t.name || t.name === 'null';\n return '
' +\n '' +\n MOD_WIKI.safe(t.name || '(unnamed)') + '' +\n (broken ? 'unfinished' : '') +\n 'rank ' + (t.rar || 1) + '' +\n (gen ? 'lv ' + gen.rung + ' · +' +\n Math.round(gen.xp * 100) + '% ' + MOD_WIKI.safe(skl[gen.skill].name) +\n ' exp' : '') +\n (t.have === true ? 'held' : '') +\n '
' + MOD_wikiDesc(t.desc) + '
';\n };\n\n var byRank = function (a, b) { return (ttl[a].rar || 1) - (ttl[b].rar || 1); };\n order.forEach(function (sk) {\n groups[sk].sort(byRank);\n h += MOD_wikiGroup(MOD_WIKI.safe(skl[sk].bname || skl[sk].name),\n groups[sk].length, groups[sk].map(row).join(''), 3);\n });\n /* The ladders that belong to no skill, by the same derivation the picker\n uses — so the two cannot drift apart. Sorted by the label, since that is\n what the reader scans. */\n var famList = Object.keys(fams)\n .map(function (stem) {\n fams[stem].sort(byRank);\n return { stem: stem,\n label: MOD_titleFamilyLabel(stem, fams[stem].map(function (k) { return ttl[k]; })) };\n })\n .sort(function (a, b) { return a.label < b.label ? -1 : a.label > b.label ? 1 : 0; });\n if (famList.length) {\n /* Nested one level down rather than sitting beside the skills. Eleven more\n top-level summaries cost ~530px on a page that is already the tallest in\n the wiki, and these are a different KIND of thing from a skill anyway. */\n var famInner = '', famTotal = 0;\n famList.forEach(function (f) {\n famTotal += fams[f.stem].length;\n famInner += MOD_wikiGroup(MOD_WIKI.safe(f.label), fams[f.stem].length,\n fams[f.stem].map(row).join(''), 4);\n });\n h += MOD_wikiGroup('Ladders of their own', famTotal, famInner, 3);\n }\n if (loose.length) {\n loose.sort(byRank);\n /* The story titles are the one group with no skill to divide them, and\n there are enough of them to need dividing. Rank rather than the alphabet:\n it is the axis they are already sorted on, and the one a reader cares\n about — a rank is a statement about where in the game a title comes from. */\n var inner;\n if (loose.length <= MOD_WIKI_ALPHA_AT) {\n inner = loose.map(row).join('');\n } else {\n var byRankNo = {};\n loose.forEach(function (k) {\n var r = Math.min(Math.max(Number(ttl[k].rar) || 1, 1), 10);\n (byRankNo[r] = byRankNo[r] || []).push(k);\n });\n inner = '';\n Object.keys(byRankNo).sort(function (a, b) { return a - b; }).forEach(function (r) {\n var st = MOD_rankStyle(r);\n inner += MOD_wikiGroup('Rank ' + r + '', byRankNo[r].length,\n byRankNo[r].map(row).join(''), 4);\n });\n }\n h += MOD_wikiGroup('From the story', loose.length, inner, 3);\n }\n return h;\n});\n\n/* --- 7. areas ------------------------------------------------------------- */\nMOD_wikiPage('areas', 'Areas & enemies', function () {\n var h = '

Areas & enemies

' +\n '

Every area, what lives in it and at what levels, and what it ' +\n 'drops. Enemy strength is not read off these levels — see the note below the ' +\n 'table.

';\n\n /* Two of the game's 21 areas are not places and listing them misleads:\n nwh \"Somewhere\" — where global.current_z parks whenever you are NOT in\n a fight, populated by creature.default (\"Nothing\").\n tst \"Test\" — reached only from chss.tst, which has id -1 and is in no\n sector, so nothing in the game can travel there.\n The test for \"is this a place\" is whether you can get to it, and neither\n can be. Everything else is listed whether or not you have been. */\n var MOD_WIKI_NOT_PLACES = { nwh: 1, tst: 1 };\n\n var keys = [];\n for (var k in area) {\n if (area[k] && area[k].pop && !MOD_WIKI_NOT_PLACES[k]) keys.push(k);\n }\n /* Ordered by the middle of the level band, which is the closest thing the\n game has to a difficulty order. */\n keys.sort(function (a, b) {\n var mid = function (z) {\n if (!z.pop.length) return 0;\n var s = 0; z.pop.forEach(function (e) { s += (e.lvlmin + e.lvlmax) / 2; });\n return s / z.pop.length;\n };\n return mid(area[a]) - mid(area[b]);\n });\n\n keys.forEach(function (k) {\n var z = area[k];\n /* An area whose baked bands are not finite can never spawn anything — see\n the note rendered under its table. Derived rather than listed by name, so\n it stays true if the game changes. */\n var broken = !z.popc || z.popc.length !== z.pop.length ||\n z.popc.some(function (band) { return !band || !isFinite(band[1] - band[0]); });\n h += '

' + MOD_WIKI.safe(z.name) +\n ' ' + k + '' +\n (broken ? 'nothing spawns' : '') +\n '' + (z.size === -1 ? 'endless' : z.size + ' to clear') +\n '

';\n if (z.pop.length) {\n /* The spawn share is NOT pop[i].c. z_bake normalises those weights into\n area.popc — a list of [lo,hi] bands that mon_gen rolls a uniform\n random against — and the weights do not have to sum to 1, so they are\n off wherever they don't. The Southern forest's .35/.45/.25 are really\n 33/43/24%. Read the bands the game actually rolls against. */\n var bands = z.popc || [];\n h += '';\n z.pop.forEach(function (e, i) {\n var c = e.crt || {};\n var band = bands[i];\n var share = band ? band[1] - band[0] : NaN;\n h += '' +\n '' +\n '';\n });\n h += '
' + MOD_WIKI.safe(c.name || '?') + 'lv ' + e.lvlmin +\n (e.lvlmax !== e.lvlmin ? '–' + e.lvlmax : '') + '' + (isFinite(share)\n ? Math.round(share * 100) + '% of spawns'\n : '—') + '
';\n if (broken) {\n h += '
Nothing spawns here. The area\\'s entries carry ' +\n 'no spawn weight, so the table the game rolls against comes out as ' +\n 'NaN and no creature can ever match it. A bug in the base game, left ' +\n 'as it is — nothing in the game travels to this area either.
';\n }\n }\n if (z.drop && z.drop.length) {\n var d = z.drop.map(function (x) {\n return MOD_WIKI.safe((x.item && x.item.name) || '?') +\n ' ' + ((x.c || 0) * 100).toFixed(2) + '%';\n }).join(', ');\n h += '
Drops: ' + d + '
';\n }\n h += '
';\n });\n\n var fightsOrig = MOD_FIGHTS.mode === 'original';\n h += '

How hard an enemy is

' +\n '

It depends on the Fights setting, which is ' +\n (fightsOrig ? 'Original' : 'Scaled') + ' now. On Original (the default) ' +\n 'every area the author wrote is exactly as he wrote it: his creatures, his ' +\n 'level ranges, and you out-grow them as you do in his game. On Scaled every ' +\n 'area is sized to you. The three areas past the golem arena and the rank ' +\n 'duels are sized to you either way.

' +\n '

Sizing to you works like this. Enemies are not fitted to a level. Each spawn is solved against your ' +\n 'actual power at the moment it appears, aiming at roughly ' + MOD_ENEMY.kill +\n ' swings to kill it and ' + MOD_ENEMY.die + ' before it kills you, with a ' +\n 'guaranteed margin between the two so a fight is never a coin flip. Variety ' +\n 'comes from the area\\'s level band and each creature\\'s own stat weighting, ' +\n 'not from difficulty scaling.

' +\n '

The reason for that: within the first stage alone you go from 1 STR to ' +\n 'around 265, so anything anchored to a level is either trivial at the end of ' +\n 'a stage or impossible at the start of one.

';\n return h;\n});\n\n/* --- 8. items -------------------------------------------------------------\n The one place where \"list everything\" needs help: 321 of the game's 371\n plain items are the same inventory type, so grouping by type alone leaves a\n single group of 321. Anything past MOD_WIKI_ALPHA_AT gets an alphabetical\n sub-split as well — the oldest index pattern there is, and derived rather\n than invented, which a hand-made taxonomy would not be.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_ALPHA_AT = 60;\n\n/* The game's own inventory tabs (isort): ALL / WPN / EQP / USE / OTHER. Using\n its categories rather than new ones means a group here is a tab there. */\nvar MOD_WIKI_STYPE = { 2: 'Weapons', 3: 'Equipment', 4: 'Usable', 5: 'Other' };\n\nfunction MOD_wikiItemRow(it) {\n var val = it.v !== undefined ? it.v : (it.val !== undefined ? it.val : null);\n return '
' +\n '

' + MOD_WIKI.safe(it.name) +\n (it.rar > 1 ? 'rarity ' + it.rar + '' : '') +\n (val !== null ? '' + MOD_wikiNum(val) + ' coin' : '') +\n (it.amount > 0 ? '×' + it.amount + '' : '') +\n '

' + MOD_wikiDesc(it.desc) + '
';\n}\n\n/* The letter a name files under. Several items are quoted — the master's\n manuals are literally named \"Sword Saint Manual\" — and filing those under a\n quotation mark is no use to anyone looking for S. */\nfunction MOD_wikiLetter(name) {\n var m = String(name || '').replace(/^[^0-9A-Za-z]+/, '');\n return (m.charAt(0) || '?').toUpperCase();\n}\n\nfunction MOD_wikiByLetter(a, b) {\n var x = MOD_wikiLetter(a.name) + String(a.name || '').toLowerCase();\n var y = MOD_wikiLetter(b.name) + String(b.name || '').toLowerCase();\n return x < y ? -1 : (x > y ? 1 : 0);\n}\n\n/* Flat below the threshold, alphabetically sub-grouped above it. Ranges are cut\n from the entries actually present, so they stay even as content is added. */\nfunction MOD_wikiAlpha(entries, render, level) {\n if (entries.length <= MOD_WIKI_ALPHA_AT) return entries.map(render).join('');\n var per = Math.ceil(entries.length / Math.ceil(entries.length / 40));\n var out = '';\n for (var i = 0; i < entries.length; i += per) {\n var chunk = entries.slice(i, i + per);\n var first = MOD_wikiLetter(chunk[0].name);\n var last = MOD_wikiLetter(chunk[chunk.length - 1].name);\n out += MOD_wikiGroup(first === last ? first : first + '–' + last,\n chunk.length, chunk.map(render).join(''), level || 4);\n }\n return out;\n}\n\nMOD_wikiPage('items', 'Items', function () {\n var h = '

Items

' +\n '

Everything the game defines, whether or not you have seen ' +\n 'it. Grouped the way your inventory groups it. Values are the base sell ' +\n 'price where the game gives one.

';\n\n /* item[] first, split by the inventory tab it appears under. */\n var buckets = {};\n for (var k in item) {\n var it = item[k];\n if (!it || !it.name || it.name === 'dummy') continue;\n var lab = MOD_WIKI_STYPE[it.stype] || 'Uncategorised';\n (buckets[lab] = buckets[lab] || []).push(it);\n }\n ['Usable', 'Other', 'Weapons', 'Equipment', 'Uncategorised'].forEach(function (lab) {\n var list = buckets[lab];\n if (!list || !list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(lab, list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n\n /* and the equipment objects, which live in their own globals */\n [['Weapons', wpn], ['Armour', eqp], ['Shields', sld], ['Accessories', acc]]\n .forEach(function (g) {\n var list = [];\n for (var kk in g[1]) {\n if (g[1][kk] && g[1][kk].name && g[1][kk].name !== 'dummy') list.push(g[1][kk]);\n }\n if (!list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(g[0], list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n return h;\n});\n\n/* --- 9. actions ----------------------------------------------------------- */\nMOD_wikiPage('actions', 'Actions', function () {\n var unlock = {};\n MOD_ACTION_UNLOCKS.forEach(function (u) {\n if (u.act) unlock[u.act.id] = (skl[u.skill] ? skl[u.skill].name : u.skill) +\n ' level ' + u.lv;\n });\n if (typeof act.mod_qi !== 'undefined') {\n unlock[act.mod_qi.id] = 'clearing the dojo\\'s Easiest, Easy and Normal fights';\n }\n\n var h = '

Actions

' +\n '

What you can be doing. One at a time, and only where the ' +\n 'action makes sense — unless \"Unrestricted actions\" is ticked in settings, ' +\n 'which lets several run at once anywhere. Both limits are deliberate; the ' +\n 'switch is there because they are not to everyone\\'s taste.

';\n\n var keys = [];\n for (var k in act) if (act[k] && act[k].name && act[k].name !== 'dummy') keys.push(k);\n keys.sort(function (a, b) { return act[a].name < act[b].name ? -1 : 1; });\n\n keys.forEach(function (k) {\n var a = act[k];\n var how = unlock[a.id];\n h += '
' +\n '

' + MOD_WIKI.safe(a.name) +\n (a.have === true ? 'unlocked'\n : 'locked') +\n (how ? 'from ' + MOD_WIKI.safe(how) + '' : '') +\n '

' + MOD_wikiDesc(a.desc) + '
';\n });\n return h;\n});\n\n/* --- 10. the dojo --------------------------------------------------------- */\nMOD_wikiPage('dojo', 'The dojo', function () {\n var h = '

The dojo

' +\n '

The instructor promises a reward every five levels. The base ' +\n 'game stops delivering at 30; the mod carries the same ladder to 110, on the ' +\n 'same terms.

' +\n '

Only the lowest rung you have not claimed is offered, which is how the ' +\n 'first six behave. Rewards are sized against the character experience curve ' +\n '(4×level3 + level2) — a level at 110 costs 5.3 ' +\n 'million, which is why the base game\\'s best pill is worth 0.3% of one there.

' +\n '' +\n '';\n\n MOD_DOJO_RUNGS.forEach(function (r) {\n var pill = item[r.pill];\n var extra = [];\n if (r.realmPill) extra.push('breakthrough pill for realm ' + r.realmPill);\n if (r.manual) extra.push('a choice of master\\'s manual');\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
LevelRewardCoinAlso
' + r.lv + '' + (pill ? MOD_WIKI.safe(pill.name) + ' ×' + r.n\n : '—') + '' + MOD_wikiNum(r.coin) + '' + (extra.join(', ') || '—') + '' + (global.flags[r.flag] ? 'claimed' : '') +\n '
' +\n\n '

Spirit pills

' +\n '

Character experience in a bottle. The four the mod adds continue the ' +\n 'game\\'s own series up to the new curve.

' +\n '' +\n '';\n MOD_PILLS.forEach(function (p) {\n h += '';\n });\n h += '
PillExperience
' + MOD_WIKI.safe(p[2]) + '' +\n p[3].toLocaleString() + '
' +\n\n '

Master\\'s manuals

' +\n '

A third grade of skillbook, offered at levels 50, 75 and 100 as a choice ' +\n 'the same way the first one was. Each is worth +' +\n Math.round(MOD_MANUAL_RATE * 100) + '% experience in its skill, permanently.

' +\n '' +\n '';\n MOD_MANUALS.forEach(function (m) {\n h += '';\n });\n h += '
ManualSkill
' + MOD_WIKI.safe(m[2]) + '' +\n MOD_WIKI.safe(skl[m[0]] ? (skl[m[0]].bname || skl[m[0]].name) : m[0]) +\n '
';\n return h;\n});\n\n/* --- 11. mechanics -------------------------------------------------------- */\nMOD_wikiPage('mechanics', 'Mechanics', function () {\n return '

Mechanics

' +\n\n '

Damage is subtractive, in both directions

' +\n '

A hit does the attacker\\'s strength minus the defender\\'s strength, plus ' +\n 'the weapon, plus one, before criticals. A defender\\'s STR is their ' +\n 'armour — there is no separate armour stat doing that job.

' +\n '

Two consequences worth carrying around:

' +\n '
    ' +\n '
  • Damage does not taper, it stops. Once a defender\\'s STR passes ' +\n 'yours, your hits do not get small; they land for nothing. Progress against ' +\n 'something too strong is not slow, it is zero.
  • ' +\n '
  • A stat ratio tells you nothing. Comparing your HP to an enemy\\'s ' +\n 'strength ignores the subtraction entirely and reads several times off in ' +\n 'either direction.
  • ' +\n '
' +\n '

This is also the reason enemies are never scaled by raising ' +\n 'their strength: it would raise their offence and their armour together, and ' +\n 'the second effect arrives first.

' +\n\n '

Criticals carry the late game

' +\n '

Critical rate reaches about a third at the top of the ladder and a ' +\n 'critical is roughly eight times a normal swing, so around three quarters of ' +\n 'all damage done at that point is critical damage. Anything that raises ' +\n 'critical rate or critical damage is worth more than it looks; Sharp Eye and ' +\n 'War are the two that do.

' +\n\n '

Hit chance

' +\n '

Landing a hit is a contest of agility. It is a ratio rather than a ' +\n 'subtraction, so agility behaves more gently than strength does — falling ' +\n 'behind on it costs you accuracy, not everything.

' +\n\n '

Skill sections and parents

' +\n '

Each of the ' + Object.keys(MOD_SECTIONS).length + ' sections has a parent ' +\n 'skill. A parent earns ' + Math.round(MOD_PARENT.feed * 100) + '% of everything ' +\n 'its children earn and grants no stats of its own. In return, a child below ' +\n 'the parent\\'s level trains ' + Math.round(MOD_PARENT.perLevel * 100) +\n '% faster per level of lag, up to ' + MOD_PARENT.maxGap + ' levels behind. ' +\n 'Bringing one section up makes every skill in it cheaper to catch up.

' +\n\n '

Saving

' +\n '

Three slots, switched from the bottom bar. The game builds its world once ' +\n 'at startup and cannot unload a save, so switching slots or starting a new ' +\n 'game reloads the page. Exporting a save before running anything unusual is ' +\n 'always the safe move.

';\n});\n\n/* --- the document ---------------------------------------------------------\n One string, no fetches, no dependencies: this has to work from file:// as\n readily as from a server.\n ------------------------------------------------------------------------- */\n\nfunction MOD_wikiCss() {\n return [\n ':root{color-scheme:dark}',\n '*{box-sizing:border-box}',\n /* Menlo and Consolas before the generic: with MS Gothic missing (any Mac)\n \"monospace\" is Courier, which Safari sets wider than Chrome does, and\n table cells wrapped on one and not the other. */\n 'body{margin:0;background:#060a1c;color:#dfe6f5;font:14px/1.6 \"MS Gothic\",' +\n '\"MS Mincho\",Menlo,Consolas,\"DejaVu Sans Mono\",monospace;display:flex;min-height:100vh}',\n 'a{color:#7cb0ff}',\n 'nav{width:210px;flex:0 0 210px;background:#081040;border-right:1px solid #46a;' +\n 'position:sticky;top:0;height:100vh;overflow:auto;padding:14px 0}',\n 'nav h2{font-size:13px;color:#9fb4d8;margin:0 0 10px 14px;letter-spacing:1px}',\n 'nav a{display:block;padding:6px 14px;text-decoration:none;color:#cfe0ff;' +\n 'border-left:3px solid transparent}',\n 'nav a:hover{background:#154080}',\n 'nav a.on{background:#123070;border-left-color:#7cb0ff;color:#fff}',\n 'nav .srch{margin:0 10px 12px;width:calc(100% - 20px);background:#050a20;' +\n 'color:#dfe6f5;border:1px solid #46a;padding:5px;font:inherit}',\n 'nav .xall{display:block;margin:12px 10px 0;color:#8296b8;font-size:12px;' +\n 'cursor:pointer;-webkit-user-select:none;user-select:none}',\n 'nav .xall input{vertical-align:-1px;margin-right:4px}',\n 'details.wk-g{border:1px solid #17224a;background:#0a1130;margin:8px 0}',\n 'details.wk-g>summary{cursor:pointer;padding:7px 12px;list-style:none;' +\n 'background:#0d1740}',\n 'details.wk-g>summary:hover{background:#154080}',\n 'details.wk-g>summary::-webkit-details-marker{display:none}',\n // a caret drawn rather than borrowed, so it is the same in every browser\n 'details.wk-g>summary::before{content:\"\\\\25B8\";color:#7cb0ff;' +\n 'display:inline-block;width:1em;transition:transform .12s}',\n 'details.wk-g[open]>summary::before{transform:rotate(90deg)}',\n 'details.wk-g>summary h2,details.wk-g>summary h3{display:inline;margin:0;' +\n 'font-size:15px;color:#fff}',\n '.wk-gi{padding:2px 12px 10px}',\n 'details.wk-g details.wk-g>summary h4{display:inline;margin:0;font-size:13px;' +\n 'color:#cfe0ff;font-weight:normal}',\n 'details.wk-g details.wk-g>summary{padding:5px 10px;background:#0b1435}',\n 'main{flex:1;min-width:0;padding:22px 30px 80px;max-width:1000px}',\n 'h1{font-size:26px;margin:0 0 6px;color:#fff;border-bottom:1px solid #46a;' +\n 'padding-bottom:8px}',\n 'h2{font-size:19px;color:#bcd4ff;margin:30px 0 8px}',\n 'h3{font-size:15px;color:#fff;margin:0 0 4px}',\n 'p.lede{color:#a8bcdc;margin:0 0 18px}',\n '.dim{color:#8296b8;font-weight:normal}',\n '.flav{font-size:12px;font-style:italic}',\n '.note{background:#0b1638;border-left:3px solid #46a;padding:8px 12px;' +\n 'color:#a8bcdc;margin:14px 0}',\n 'code,pre{background:#0b1638;border:1px solid #24356e;padding:1px 5px;' +\n 'color:#9fd0ff}',\n 'pre{padding:10px;overflow-x:auto}',\n 'dl dt{color:#fff;margin-top:14px}',\n 'dl dd{margin:2px 0 0 0;color:#a8bcdc}',\n 'ul{color:#a8bcdc}',\n 'table.wk-tbl{border-collapse:collapse;width:100%;margin:10px 0;' +\n 'display:block;overflow-x:auto}',\n 'table.wk-tbl th{text-align:left;color:#9fb4d8;font-weight:normal;' +\n 'border-bottom:1px solid #46a;padding:5px 9px;white-space:nowrap}',\n 'table.wk-tbl td{border-bottom:1px solid #17224a;padding:5px 9px;' +\n 'vertical-align:top}',\n 'table.wk-tbl tr.done td{color:#cfe0ff}',\n 'table.wk-tbl tr.here{background:#123070}',\n '.num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}',\n '.wk-e{border:1px solid #17224a;background:#081040;padding:9px 12px;' +\n 'margin:7px 0;border-radius:2px}',\n 'tr.wk-e{border:0;background:none}',\n '.wk-e.done{border-color:#2a4a8c}',\n '.wk-e h3{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}',\n '.lv{margin-left:auto;font-size:12px;color:#8296b8;font-weight:normal}',\n '.lv.capped{color:#ffd24a}',\n '.rank{font-size:12px;color:#8296b8;font-weight:normal}',\n '.desc{color:#a8bcdc;font-size:13px}',\n '.proj{color:#ffd24a;font-size:12px;margin-top:3px}',\n '.tag{font-size:11px;border:1px solid #46a;color:#9fb4d8;padding:0 5px;' +\n 'border-radius:2px;font-weight:normal}',\n '.tag.mod{border-color:#7a4ea8;color:#c07bff}',\n '.tag.done{border-color:#2e7d4f;color:#6fdc9a}',\n '.tag.here{border-color:#ffd24a;color:#ffd24a}',\n '.tag.warn{border-color:#a8562e;color:#ff9a5c}',\n 'table.wk-perks{border-collapse:collapse;margin:6px 0 0;width:100%}',\n 'table.wk-perks td{padding:2px 8px 2px 0;font-size:12px;color:#8296b8;' +\n 'border-bottom:1px solid #101a3c}',\n 'table.wk-perks tr.done td{color:#cfe0ff}',\n 'table.wk-perks td.tick{color:#6fdc9a;width:1em;text-align:right}',\n // fixed columns so the level bands line up between one area and the next\n 'table.wk-pop{table-layout:fixed}',\n 'table.wk-pop td:first-child{width:60%}',\n 'table.wk-pop td:nth-child(2){width:20%}',\n '.wk-title .tname{font-weight:bold}',\n '.wk-now{background:#0b1638;border:1px solid #24356e;padding:10px 14px;' +\n 'margin:0 0 18px;display:inline-block;min-width:320px}',\n '.wk-now h3{color:#9fb4d8;font-weight:normal;font-size:13px;' +\n 'letter-spacing:1px;margin-bottom:6px}',\n '.wk-now td{padding:2px 16px 2px 0}',\n '.wk-now td:last-child{color:#fff;text-align:right}',\n '.pg{display:none}.pg.on{display:block}',\n 'footer{color:#5a6c8c;font-size:12px;border-top:1px solid #17224a;' +\n 'margin-top:40px;padding-top:12px}',\n '@media(max-width:760px){body{display:block}nav{width:auto;height:auto;' +\n 'position:static;flex:none}main{padding:16px}}'\n ].join('\\n');\n}\n\n/* Filtering, page switching and the hash. Small enough to inline, and inlining\n it is the point — the document has to stand on its own. */\nfunction MOD_wikiJs() {\n return [\n 'var pages=[].slice.call(document.querySelectorAll(\".pg\"));',\n 'var links=[].slice.call(document.querySelectorAll(\"nav a[data-p]\"));',\n 'function show(id){',\n ' var found=false;',\n ' pages.forEach(function(p){var on=p.id===\"pg-\"+id;if(on)found=true;',\n ' p.className=\"pg\"+(on?\" on\":\"\")});',\n ' if(!found&&pages.length){pages[0].className=\"pg on\";id=pages[0].id.slice(3)}',\n ' links.forEach(function(a){a.className=a.dataset.p===id?\"on\":\"\"});',\n ' window.scrollTo(0,0);return id;}',\n 'links.forEach(function(a){a.addEventListener(\"click\",function(e){',\n ' e.preventDefault();location.hash=a.dataset.p;show(a.dataset.p)})});',\n 'window.addEventListener(\"hashchange\",function(){show(location.hash.slice(1))});',\n 'show(location.hash.slice(1)||\"start\");',\n // Search filters entries, hides groups left with none, and OPENS the groups\n // that still have some — a match hidden inside a collapsed group reads as\n // no match at all, which is worse than not having search.\n 'var box=document.getElementById(\"srch\");',\n 'var xall=document.getElementById(\"xall\");',\n 'var groups=[].slice.call(document.querySelectorAll(\"details.wk-g\"));',\n 'function filter(){',\n ' var q=box.value.trim().toLowerCase();',\n ' pages.forEach(function(p){',\n ' [].slice.call(p.querySelectorAll(\".wk-e\")).forEach(function(e){',\n ' e.style.display=(!q||e.textContent.toLowerCase().indexOf(q)>=0)?\"\":\"none\"})});',\n ' groups.forEach(function(g){',\n ' var any=[].slice.call(g.querySelectorAll(\".wk-e\")).some(function(e){',\n ' return e.style.display!==\"none\"});',\n ' g.style.display=any?\"\":\"none\";',\n ' g.open=xall.checked||(!!q&&any)}); }',\n 'box.addEventListener(\"input\",filter);',\n 'box.addEventListener(\"keydown\",function(e){if(e.key===\"Escape\"){box.value=\"\";filter()}});',\n // Everything is collapsed by default because three of these pages run past\n // 40,000px open. This is the way back to one long document, which is what\n // the browser's own find needs.\n 'xall.addEventListener(\"change\",filter);'\n ].join('\\n');\n}\n\nfunction MOD_wikiBuild() {\n var nav = '', body = '';\n MOD_WIKI_PAGES.forEach(function (pg) {\n nav += '' +\n MOD_WIKI.safe(pg.label) + '';\n var inner;\n /* One page throwing must not take the wiki down with it — the page says so\n and the rest still renders. */\n try { inner = pg.build(); }\n catch (e) {\n inner = '

' + MOD_WIKI.safe(pg.label) + '

This page ' +\n 'could not be built: ' + MOD_WIKI.safe(e.message) + '

';\n console.warn('[mod] wiki page \"' + pg.id + '\" failed: ' + e.message);\n }\n body += '
' + inner + '
';\n });\n\n var stamp = new Date().toLocaleString();\n return '' +\n '' +\n '' + MOD_WIKI.safe(MOD_WIKI.title) + '' +\n '' +\n '' +\n '
' + body +\n '
Generated from the running game at ' + MOD_WIKI.safe(stamp) +\n ' — mod ' + MOD_WIKI.safe(MOD.version) + '. Everything here is read from the ' +\n 'live data, including your own save, so it cannot fall out of step with the ' +\n 'game. The game is truezhangwei\\'s; the mod is not endorsed by the author.' +\n '
' +\n ' inside content closing the block early, so that is the\n one thing scrubbed. */\n safe: function (h) {\n return String(h === undefined || h === null ? '' : h).replace(/<\\/script/gi, '<\\\\/script');\n }\n};\n\n/* Descriptions carry real HTML (coloured spans, the game's separator div), so\n they are passed through rather than escaped — but the separator becomes a\n rule, since a wiki row is not a tooltip. */\nfunction MOD_wikiDesc(d) {\n var s;\n try { s = (typeof d === 'function') ? d() : d; } catch (e) { s = ''; }\n if (!s) return '';\n s = String(s).split('
 
').join('
');\n return MOD_WIKI.safe(s);\n}\n\nfunction MOD_wikiNum(n) {\n if (!isFinite(n)) return '—';\n return Math.round(n).toLocaleString();\n}\n\n/* An effect line at a level the player has not reached yet. The effect\n functions read the skill they are handed, so the level is borrowed and put\n back; anything that throws simply gets no projection. */\nfunction MOD_wikiEffectAt(key, sk, lvl) {\n var eff = MOD_EFFECTS[key];\n if (!eff) return '';\n var was = sk.lvl, out = '';\n try { sk.lvl = lvl; out = eff(sk) || ''; }\n catch (e) { out = ''; }\n finally { sk.lvl = was; }\n return MOD_WIKI.safe(out);\n}\n\n/* --- page builders --------------------------------------------------------\n Each returns the inner HTML of one page. `wk-e` marks a searchable entry and\n `wk-g` a group that hides itself when all its entries are filtered out.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_PAGES = [];\n\nfunction MOD_wikiPage(id, label, build) {\n MOD_WIKI_PAGES.push({ id: id, label: label, build: build });\n}\n\n/* A collapsible group.\n\n Not decoration: with every skill, item and title listed, three of the eleven\n pages ran past 40,000 pixels — a hundred screens of scrolling, which is the\n same as not having the content at all. Collapsed, each of those pages opens\n as an index you can read in one screen and expand where you want.\n\n
rather than a click handler, so it still works with JavaScript off,\n the browser's own find can be made to reach inside it, and the summary is\n keyboard-reachable for free. `level` is the heading level for the summary,\n since a group is sometimes a section of a page and sometimes a subsection. */\nfunction MOD_wikiGroup(heading, count, inner, level) {\n var hn = 'h' + (level || 2);\n return '
<' + hn + '>' + heading +\n (count !== null && count !== undefined\n ? ' (' + count + ')' : '') +\n '
' + inner + '
';\n}\n\n/* --- 1. orientation ------------------------------------------------------- */\nMOD_wikiPage('start', 'Start here', function () {\n var cap = MOD_levelCap();\n var realm = MOD_realm();\n var h = '

proto23

' +\n '

An idle RPG by truezhangwei, plus a mod. You train skills by ' +\n 'doing things, the things you can do open up as the story does, and none of ' +\n 'it is in a hurry.

' +\n\n '

Where you are right now

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Character level' + MOD_wikiNum(you.lvl) + '
Skill level cap' + cap + ' — ' +\n MOD_WIKI.safe(MOD_CAP.name) + '
Cultivation realm' + MOD_WIKI.safe(realm.name) +\n ' (' + realm.n + ' of ' + (MOD_REALMS.length - 1) + ')
Titles held' + MOD_titleCount() + ' — renown ' +\n (skl.rnwn ? skl.rnwn.lvl : 0) + '
Coin' + MOD_wikiNum(you.wealth || 0) + '
' +\n\n '

The four things worth knowing early

' +\n '
' +\n '
Your skill levels are capped by the story, not by grinding.
' +\n '
The cap starts at 10 and ends at 110, and it moves when you reach a new ' +\n 'place — not when you earn enough experience. If a skill has stopped ' +\n 'levelling, you are not doing it wrong; you need to go somewhere. See ' +\n 'Progression.
' +\n\n '
Armour is subtraction, so levels matter more than they look.
' +\n '
A hit does the attacker\\'s STR minus the defender\\'s STR. That means a ' +\n 'small gap in strength is a large gap in damage, and a big enough one takes ' +\n 'your damage to nothing at all rather than merely reducing it. See ' +\n 'Mechanics.
' +\n\n '
Most skills have a parent that pulls them along.
' +\n '
Every section has a parent skill that earns a share of everything its ' +\n 'children do, and any child lagging behind the parent trains faster — up to ' +\n 'twice as fast at ' + MOD_PARENT.maxGap + ' levels behind. A neglected skill is ' +\n 'cheap to bring up later, which is the only reason having this many is ' +\n 'workable.
' +\n\n '
Cultivation is a second ladder, and it is not automatic.
' +\n '
Circulating qi levels a skill; the skill does not raise your realm. ' +\n 'Reaching the level opens a bottleneck, and breaking through it costs a pill ' +\n 'and can fail. See Cultivation.
' +\n '
' +\n\n '

What is the mod and what is the game

' +\n '

The game is index.html and it is entirely truezhangwei\\'s ' +\n 'work. Everything in mod.js is a modification of it and is not ' +\n 'endorsed by the author. Pages here mark added content with ' +\n 'mod where the distinction matters. The full ' +\n 'list of changes is in the changelog, on the bottom bar.

';\n return h;\n});\n\n/* --- 2. progression ------------------------------------------------------- */\nMOD_wikiPage('progress', 'Progression', function () {\n var cap = MOD_levelCap();\n var h = '

Progression

' +\n '

Skill levels are capped by how far the story has gone. Ten ' +\n 'rungs, 10 through 110. The cap applies to every skill at once; Renown is ' +\n 'the single exception, since it is counted rather than trained.

' +\n '' +\n '';\n\n MOD_TIERS.forEach(function (t) {\n var got = null;\n try { got = MOD_tierFlag(t); } catch (e) {}\n var here = (t.cap === cap);\n var need = t.needText ? t.needText\n : (t.flags.length ? t.flags.join(' or ') : 'the start of the game');\n h += '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
CapStageWhat opens it
' + t.cap + '' + MOD_WIKI.safe(t.name) + '' + MOD_WIKI.safe(need) + '' + (here ? 'you are here'\n : got ? 'reached' : '') + '
' +\n\n '

The experience curve

' +\n '

The game\\'s own skill curve is super-exponential — level 109 to 110 ' +\n 'alone costs ' + MOD_wikiNum(MOD_vanillaExpnext(109)) + ' experience, so the top of the ladder ' +\n 'is unreachable by a factor of about ten trillion. But the author only ever ' +\n 'designed skills up to level 15, and below that his curve is fine.

' +\n (MOD_XP.vanillaTo > 0\n ? '

So the mod keeps it. Below level ' + MOD_XP.vanillaTo + ' a skill costs ' +\n 'exactly what the original charges; from there it rises by ×' +\n MOD_XP.ratio.toFixed(4) + ' a level, a ratio solved so the climb to 110 takes ' +\n 'the same time it always did.'\n : '

You have chosen the Mod before 4.2 pacing: a plain geometric curve, ' +\n '×' + MOD_XP.ratio.toFixed(3) + ' a level from level 0, much faster than the ' +\n 'original early on. The Original pacing in settings puts back the ' +\n 'author\\'s own cost below level 10.') +\n ' Character level uses the game\\'s own curve, untouched.

' +\n '' +\n '';\n [1, 5, 9, 10, 15, 25, 50, 75, 90, 109].forEach(function (l) {\n h += '';\n });\n h += '
LevelExperience for the nextOriginal game
' + l +\n '' + MOD_wikiNum(MOD_expnextFor(l)) + '' +\n MOD_wikiNum(MOD_vanillaExpnext(l)) + '
';\n return h;\n});\n\n/* --- 3. skills ------------------------------------------------------------ */\nMOD_wikiPage('skills', 'Skills', function () {\n var cap = MOD_levelCap();\n var bySection = {};\n var keys = [];\n for (var k in skl) {\n var s = skl[k];\n if (!s || typeof s !== 'object' || !s.name) continue;\n keys.push(k);\n }\n keys.forEach(function (k) {\n var s = skl[k];\n var sec = MOD_sectionOf(s);\n (bySection[sec] = bySection[sec] || []).push(k);\n });\n\n var h = '

Skills

' +\n '

' + keys.length + ' skills in ' +\n Object.keys(MOD_SECTIONS).length + ' sections. Every one has perks at ' +\n MOD_LADDER.join(', ') + ', and every one that has milestones grants titles at ' +\n MOD_TITLE_RUNGS.join(', ') + '. Levels and ticks below are yours.

';\n\n Object.keys(MOD_SECTIONS).forEach(function (sec) {\n var list = bySection[sec];\n if (!list || !list.length) return;\n var parentKey = MOD_PARENT_KEY[sec];\n var g = '';\n if (parentKey && skl[parentKey]) {\n g += '

Parent skill: ' +\n MOD_WIKI.safe(skl[parentKey].bname || skl[parentKey].name) +\n ' — earns ' + Math.round(MOD_PARENT.feed * 100) +\n '% of what its children earn, and speeds up any child that lags behind it.

';\n }\n list.sort(function (a, b) { return skl[a].name < skl[b].name ? -1 : 1; });\n list.forEach(function (k) {\n var s = skl[k];\n var lvl = Number(s.lvl) || 0;\n g += '
' +\n '

' + MOD_WIKI.safe(s.bname || s.name) +\n (s.bname ? ' (' + MOD_WIKI.safe(s.name) + ')' : '') +\n '= cap ? ' capped' : '') + '\">lv ' + lvl +\n (lvl >= cap ? ' — at the cap' : '') + '

' +\n '
' + MOD_wikiDesc(s.desc) + '
';\n\n var at110 = MOD_wikiEffectAt(k, s, 110);\n if (at110) g += '
At level 110: ' + at110 + '
';\n\n if (s.mlstn && s.mlstn.length) {\n g += '';\n s.mlstn.forEach(function (m) {\n g += '' +\n '' +\n '' +\n '';\n });\n g += '
' + m.lv + '' + MOD_WIKI.safe(m.p || '—') + '' + (m.g === true ? '✓' : '') + '
';\n }\n g += '
';\n });\n h += MOD_wikiGroup(MOD_WIKI.safe(MOD_SECTIONS[sec]), list.length, g, 2);\n });\n return h;\n});\n\n/* --- 4. cultivation ------------------------------------------------------- */\nMOD_wikiPage('cultivation', 'Cultivation', function () {\n var now = MOD_realm();\n var elig = 0;\n try { elig = MOD_realmEligible().n; } catch (e) {}\n var qic = skl.qic ? skl.qic.lvl : 0;\n\n var h = '

Cultivation

' +\n '

Ten realms above Mortal. Circulating qi levels the Qi ' +\n 'Circulation skill; the skill does not advance the realm. Reaching the level ' +\n 'opens a bottleneck, and breaking through it costs a pill and can fail — the ' +\n 'pill is spent either way.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Realm' + MOD_WIKI.safe(MOD_realmTitle()) + '
Spiritual Root' + MOD_WIKI.safe(MOD_root().name) + '
Qi Circulationlevel ' + qic + '
Insight' + MOD_insight() + '
Bottleneck open' +\n (elig > now.n ? 'yes — ' + MOD_WIKI.safe(MOD_REALMS[now.n + 1].name) +\n ' is within reach' : 'no') + '
Qi Deviation' + (MOD_deviated()\n ? MOD_deviationLeft() + ' in-game minutes left' : 'no') + '
' +\n '

Realm thresholds are the same levels that earn a title rank, so realm N ' +\n 'is the level at which a rank N title arrives. Every realm multiplies all ' +\n 'stats and health, and the multipliers do not stack with each other — the ' +\n 'realm you are in is the one that applies.

' +\n '' +\n '';\n\n MOD_REALMS.forEach(function (r) {\n var pill = r.n >= 1 ? item['mod_bp' + r.n] : null;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmNameQi Circ.StatsHealthPill
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + r.qic + '×' + r.mult + '×' + r.hp + '' + (pill ? MOD_WIKI.safe(pill.name) : '—') +\n '
' +\n\n '

Where the pills come from

' +\n '

Every breakthrough pill has a source in normal play. None of them are ' +\n 'craftable, and none are cheap — a bottleneck you can buy in bulk is not a ' +\n 'bottleneck.

' +\n '' +\n '' +\n '' +\n '' +\n '
RealmsSource
1The dojo instructor, when qi ' +\n 'circulation is first taught
2–5The Herbalist in the village ' +\n '(vendor.pha1), restocked
6–10The dojo\\'s Level Advancement ' +\n 'ladder, and the Pill Tower
' +\n '

Realm 1 is deliberately not sold at the marketplace: ' +\n 'the marketplace is behind a 40%-a-visit chance encounter, and realm 1 opens ' +\n 'long before that is reliable.

' +\n\n '

Breaking through

' +\n '

A bottleneck wants three things, which is what the genre says it wants: ' +\n 'a pill, insight, and enough consolidation to make the ' +\n 'attempt worth making. The pill and the insight are both spent whether the ' +\n 'attempt works or not.

' +\n '

The odds improve the further past the threshold you are, plus your root, ' +\n 'plus time sat in Closed Door Training, and cap short of certain.

' +\n '' +\n '';\n [0, 2, 4, 6, 8].forEach(function (o) {\n var base = Math.min(0.55 + o * 0.05 + MOD_root().odds, 0.95);\n h += '';\n });\n h += '
Levels past the thresholdChanceAfter full seclusion
+' + o + '' +\n Math.round(base * 100) + '%' +\n Math.round(Math.min(base + MOD_CULT.secludeOdds, 0.95) * 100) + '%
' +\n\n '

Insight

' +\n '

Insight is the half of the price that cannot be bought. It comes from ' +\n 'three places, and they are the three the genre names: meditating, fights you ' +\n 'nearly lost, and going somewhere you have not been.

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
SourceRate
Circulate Qi, at a wall' +\n (MOD_CULT.meditateChance * 100).toFixed(1) + '% a second
Closed Door Training' +\n (MOD_CULT.secludeChance * 100).toFixed(1) + '% a second
A kill made under ' +\n Math.round(MOD_CULT.brinkFrac * 100) + '% health — ' +\n (MOD_FIGHTS.mode === 'original'\n ? 'the author\\'s areas rarely come close once you reach them; the three past ' +\n 'the golem arena and the rank duels do, within a few fights, as does an area ' +\n 'you walk into early'\n : 'every fight costs a share of your health, so a few in a row without resting') +\n '' +\n Math.round(MOD_CULT.brinkChance * 100) + '% a kill
Entering an area for the first timealways
' +\n '

The insight a realm asks for is sized to the climb to it: about ' +\n Math.round(MOD_CULT.wallShare * 100) + '% of the Circulate Qi time the climb took, ' +\n 'spent at the wall. Seclusion earns it three times as fast, and full ' +\n 'consolidation takes as long as that on average.

' +\n '' +\n '' +\n '';\n var wkHours = function (sec) {\n return sec >= 7200 ? (sec / 3600).toFixed(sec >= 36000 ? 0 : 1) + ' h' : Math.round(sec / 60) + ' min';\n };\n MOD_REALMS.forEach(function (r) {\n if (!r.n) return;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmClimb (Circulate Qi, 1x)Insight to enterFull seclusionQi Deviation (in-game)
' + MOD_WIKI.safe(r.name) + '' + wkHours(MOD_realmClimbSec(r.n)) + '' + MOD_insightNeed(r.n) + '' + wkHours(MOD_secludeFull(r.n)) + '' + (MOD_devMinutes(r.n) / 1440).toFixed(1) + ' days
' +\n\n '

The nine layers

' +\n '

Each realm is nine layers deep, derived from where Qi Circulation sits ' +\n 'between this realm\\'s threshold and the next one\\'s — so breaking through ' +\n 'puts you at the first layer of the new realm on its own. Layers are worth ' +\n '×' + MOD_CULT.layerMult + ' stats and ×' + MOD_CULT.layerBody +\n ' body each above the first, and the top realm has no rung above it to ' +\n 'measure against, so it stays at its first layer.

' +\n '' +\n '';\n for (var li = 1; li <= MOD_CULT.layers; li++) {\n h += '';\n }\n h += '
LayerCalled
' +\n li + '' + MOD_LAYER_ORD[li] + ' layer, ' +\n MOD_WIKI.safe(MOD_layerStage(li)) + '
' +\n '

At the threshold of the next realm and before breaking ' +\n 'through, you are a half step to it — the genre\\'s own name for the ' +\n 'state this mod calls a bottleneck.

' +\n\n '

Closed Door Training

' +\n '

Seclusion does two things. It consolidates — a full sitting ' +\n '(the table above) is worth +' + Math.round(MOD_CULT.secludeOdds * 100) +\n '% on the attempt — and it shelters: a breakthrough attempted while ' +\n 'you are still sitting in it cannot end in Qi Deviation, only in a lost ' +\n 'pill. It will not start unless you are actually at a wall.

' +\n\n '

Qi Deviation

' +\n '

Failing a breakthrough out in the open leaves your cultivation base ' +\n 'unstable for as long as the seclusion you skipped (the table above, never ' +\n 'under ' + MOD_CULT.devMinutes + ' in-game minutes): stats at ×' +\n MOD_CULT.devStat + ', body at ×' + MOD_CULT.devBody + ', and no second ' +\n 'attempt until it passes. It clears on its own, or at once with a ' +\n 'Qi Settling Pill. It never kills — the death system stays out of it.

' +\n\n '

Heavenly Tribulation

' +\n '

From realm ' + MOD_CULT.tribFrom + ' up, a successful breakthrough is ' +\n 'answered. The cost is a fraction of your maximum health, so it is a gate on ' +\n 'walking in healthy rather than a damage race; the realm bonus you are ' +\n 'standing on is what pays it. Fall to it and the realm does not open.

' +\n '' +\n '';\n MOD_REALMS.forEach(function (r) {\n if (!r.n || r.n < MOD_CULT.tribFrom) return;\n h += '' +\n '' +\n '';\n });\n h += '
RealmBoltsOf your max HP
' + MOD_WIKI.safe(r.name) + '' + MOD_tribulationBolts(r.n) + '' + Math.round(MOD_tribulationCost(r.n) * 100) + '%
' +\n\n '

Spiritual Root

' +\n '

Rolled once, when the dojo finishes teaching you to fight. It is never a ' +\n 'dead end: the worst grade costs cultivation speed, not access, and a ' +\n 'Root Cleansing Pill moves you up a grade.

' +\n '' +\n '';\n var rootTotal = 0;\n MOD_ROOTS.forEach(function (r) { rootTotal += r.weight; });\n MOD_ROOTS.forEach(function (r) {\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
GradeRootChanceCultivationBreakthrough
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + Math.round(r.weight / rootTotal * 100) + '%×' + r.xp + '' + (r.odds >= 0 ? '+' : '') +\n Math.round(r.odds * 100) + '%
';\n return h;\n});\n\n/* --- 5. elemental mastery ------------------------------------------------- */\nMOD_wikiPage('mastery', 'Elemental mastery', function () {\n var h = '

Elemental mastery mod

' +\n '

Six masteries, each with a technique that fires on its own ' +\n 'during a fight. Combat is automatic and there is no ability picker, so a ' +\n 'technique is a chance on each of your swings to become something else — one ' +\n 'that scales with INT and the element rather than with your weapon.

' +\n '

Nothing fires while you are still Mortal: the channels have to be open ' +\n 'first. Chance is ' + (MOD_TECH.perLevel * 100).toFixed(1) + '% per mastery ' +\n 'level plus ' + (MOD_TECH.perRealm * 100).toFixed(1) + '% per realm, capped at ' +\n Math.round(MOD_TECH.cap * 100) + '%; a technique lands for ×' +\n MOD_TECH.power + ' of a normal strike.

' +\n '' +\n '';\n\n var realm = 0;\n try { realm = MOD_realm().n; } catch (e) {}\n MOD_MASTERY.forEach(function (m) {\n var lvl = Number(m.skill.lvl) || 0;\n var ch = realm < 1 ? 0\n : Math.min(lvl * MOD_TECH.perLevel + realm * MOD_TECH.perRealm, MOD_TECH.cap);\n h += '' +\n '' +\n '' +\n '';\n });\n h += '
MasteryTechniqueLevelChance now
' +\n MOD_WIKI.safe(m.skill.name) + '' + MOD_WIKI.safe(m.name) + '' + lvl + '' + (realm < 1 ? 'Mortal'\n : (ch * 100).toFixed(1) + '%') + '
';\n return h;\n});\n\n/* --- 6. titles ------------------------------------------------------------ */\nMOD_wikiPage('titles', 'Titles', function () {\n var perm = MOD_permanentRank();\n var held = MOD_titleCount();\n var total = 0; for (var kk in ttl) if (ttl[kk] && ttl[kk].name) total++;\n\n var h = '

Titles

' +\n '

A title\\'s rank runs 1 to 10 and is derived from the level ' +\n 'that earns it, so a rank is a statement about where in the game you are. ' +\n 'Only the title you are wearing applies — until renown makes the lower ranks ' +\n 'permanent.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '
Titles held' + held + ' of ' + total + '
Renown level' + (skl.rnwn ? skl.rnwn.lvl : 0) +\n ' of ' + MOD_RENOWN_MAX + '
Ranks that apply without wearing' +\n (perm >= 1 ? '1–' + perm : 'none yet') + '
' +\n\n '

Ranks

' +\n '';\n MOD_RANK_AT.forEach(function (lv, i) {\n var st = MOD_rankStyle(i + 1);\n h += '' +\n '';\n });\n h += '
RankEarned from levelColour
' + (i + 1) + '' + lv + '' + '●●●' + '
' +\n\n '

Renown

' +\n '

Renown is not trained. It counts the titles you hold, and each level ' +\n 'multiplies every stat by ' + (1 + MOD_RENOWN_RATE) + ' — compounding, so ' +\n 'level ' + MOD_RENOWN_MAX + ' is ×' +\n (Math.round(MOD_renownMult(MOD_RENOWN_MAX) * 1000) / 1000) + '. At renown N, ' +\n 'every title of rank N or below applies permanently.

' +\n '' +\n '';\n MOD_RENOWN_STEPS.forEach(function (n, i) {\n h += '= n ? 'done' : '') + '\">' +\n '';\n });\n h += '
RenownTitles neededAll statsPassive ranks
' + (i + 1) +\n '' + n + '×' +\n (Math.round(MOD_renownMult(i + 1) * 1000) / 1000) + '1–' + (i + 1) + '
' +\n\n '

Every title

' +\n '

Grouped by the skill that grants it. What is left is either ' +\n 'a ladder of its own — creatures killed, items gathered, the ' +\n 'cultivation realms — or a one-off from the story.

';\n\n var groups = {}, fams = {}, loose = [];\n for (var k in ttl) {\n var t = ttl[k];\n /* Unlike the picker, the wiki KEEPS the author's broken entries and says\n so — same call as area.clg, which is listed and tagged \"nothing spawns\"\n rather than quietly dropped. A reader looking for why a title never\n appears is better served by the row than by its absence. */\n if (!t || t.name === undefined || t.name === null) continue;\n var sk = MOD_TITLE_SKILL[k];\n if (sk && skl[sk]) { (groups[sk] = groups[sk] || []).push(k); continue; }\n var stem = MOD_titleStem(k);\n if (stem) { (fams[stem] = fams[stem] || []).push(k); continue; }\n loose.push(k);\n }\n /* Same rule as the picker: a family of one is not a family. */\n Object.keys(fams).forEach(function (stem) {\n if (fams[stem].length < 2) { loose.push(fams[stem][0]); delete fams[stem]; }\n });\n var order = Object.keys(groups).sort(function (a, b) {\n return skl[a].name < skl[b].name ? -1 : 1;\n });\n\n var row = function (k) {\n var t = ttl[k];\n var st = MOD_rankStyle(t.rar);\n var gen = null;\n for (var i = 0; i < MOD_SKILL_TITLES.length; i++) {\n if (MOD_SKILL_TITLES[i].key === k) { gen = MOD_SKILL_TITLES[i]; break; }\n }\n /* The author left a handful unfinished: ttl.ddcd carries the literal string\n \"null\" for both name and description, and shpt2/shpt3/mone3 are blank.\n None is granted by anything, so none can be earned. */\n var broken = !t.name || t.name === 'null';\n return '
' +\n '' +\n MOD_WIKI.safe(t.name || '(unnamed)') + '' +\n (broken ? 'unfinished' : '') +\n 'rank ' + (t.rar || 1) + '' +\n (gen ? 'lv ' + gen.rung + ' · +' +\n Math.round(gen.xp * 100) + '% ' + MOD_WIKI.safe(skl[gen.skill].name) +\n ' exp' : '') +\n (t.have === true ? 'held' : '') +\n '
' + MOD_wikiDesc(t.desc) + '
';\n };\n\n var byRank = function (a, b) { return (ttl[a].rar || 1) - (ttl[b].rar || 1); };\n order.forEach(function (sk) {\n groups[sk].sort(byRank);\n h += MOD_wikiGroup(MOD_WIKI.safe(skl[sk].bname || skl[sk].name),\n groups[sk].length, groups[sk].map(row).join(''), 3);\n });\n /* The ladders that belong to no skill, by the same derivation the picker\n uses — so the two cannot drift apart. Sorted by the label, since that is\n what the reader scans. */\n var famList = Object.keys(fams)\n .map(function (stem) {\n fams[stem].sort(byRank);\n return { stem: stem,\n label: MOD_titleFamilyLabel(stem, fams[stem].map(function (k) { return ttl[k]; })) };\n })\n .sort(function (a, b) { return a.label < b.label ? -1 : a.label > b.label ? 1 : 0; });\n if (famList.length) {\n /* Nested one level down rather than sitting beside the skills. Eleven more\n top-level summaries cost ~530px on a page that is already the tallest in\n the wiki, and these are a different KIND of thing from a skill anyway. */\n var famInner = '', famTotal = 0;\n famList.forEach(function (f) {\n famTotal += fams[f.stem].length;\n famInner += MOD_wikiGroup(MOD_WIKI.safe(f.label), fams[f.stem].length,\n fams[f.stem].map(row).join(''), 4);\n });\n h += MOD_wikiGroup('Ladders of their own', famTotal, famInner, 3);\n }\n if (loose.length) {\n loose.sort(byRank);\n /* The story titles are the one group with no skill to divide them, and\n there are enough of them to need dividing. Rank rather than the alphabet:\n it is the axis they are already sorted on, and the one a reader cares\n about — a rank is a statement about where in the game a title comes from. */\n var inner;\n if (loose.length <= MOD_WIKI_ALPHA_AT) {\n inner = loose.map(row).join('');\n } else {\n var byRankNo = {};\n loose.forEach(function (k) {\n var r = Math.min(Math.max(Number(ttl[k].rar) || 1, 1), 10);\n (byRankNo[r] = byRankNo[r] || []).push(k);\n });\n inner = '';\n Object.keys(byRankNo).sort(function (a, b) { return a - b; }).forEach(function (r) {\n var st = MOD_rankStyle(r);\n inner += MOD_wikiGroup('Rank ' + r + '', byRankNo[r].length,\n byRankNo[r].map(row).join(''), 4);\n });\n }\n h += MOD_wikiGroup('From the story', loose.length, inner, 3);\n }\n return h;\n});\n\n/* --- 7. areas ------------------------------------------------------------- */\nMOD_wikiPage('areas', 'Areas & enemies', function () {\n var h = '

Areas & enemies

' +\n '

Every area, what lives in it and at what levels, and what it ' +\n 'drops. Enemy strength is not read off these levels — see the note below the ' +\n 'table.

';\n\n /* Two of the game's 21 areas are not places and listing them misleads:\n nwh \"Somewhere\" — where global.current_z parks whenever you are NOT in\n a fight, populated by creature.default (\"Nothing\").\n tst \"Test\" — reached only from chss.tst, which has id -1 and is in no\n sector, so nothing in the game can travel there.\n The test for \"is this a place\" is whether you can get to it, and neither\n can be. Everything else is listed whether or not you have been. */\n var MOD_WIKI_NOT_PLACES = { nwh: 1, tst: 1 };\n\n var keys = [];\n for (var k in area) {\n if (area[k] && area[k].pop && !MOD_WIKI_NOT_PLACES[k]) keys.push(k);\n }\n /* Ordered by the middle of the level band, which is the closest thing the\n game has to a difficulty order. */\n keys.sort(function (a, b) {\n var mid = function (z) {\n if (!z.pop.length) return 0;\n var s = 0; z.pop.forEach(function (e) { s += (e.lvlmin + e.lvlmax) / 2; });\n return s / z.pop.length;\n };\n return mid(area[a]) - mid(area[b]);\n });\n\n keys.forEach(function (k) {\n var z = area[k];\n /* An area whose baked bands are not finite can never spawn anything — see\n the note rendered under its table. Derived rather than listed by name, so\n it stays true if the game changes. */\n var broken = !z.popc || z.popc.length !== z.pop.length ||\n z.popc.some(function (band) { return !band || !isFinite(band[1] - band[0]); });\n h += '

' + MOD_WIKI.safe(z.name) +\n ' ' + k + '' +\n (broken ? 'nothing spawns' : '') +\n '' + (z.size === -1 ? 'endless' : z.size + ' to clear') +\n '

';\n if (z.pop.length) {\n /* The spawn share is NOT pop[i].c. z_bake normalises those weights into\n area.popc — a list of [lo,hi] bands that mon_gen rolls a uniform\n random against — and the weights do not have to sum to 1, so they are\n off wherever they don't. The Southern forest's .35/.45/.25 are really\n 33/43/24%. Read the bands the game actually rolls against. */\n var bands = z.popc || [];\n h += '';\n z.pop.forEach(function (e, i) {\n var c = e.crt || {};\n var band = bands[i];\n var share = band ? band[1] - band[0] : NaN;\n h += '' +\n '' +\n '';\n });\n h += '
' + MOD_WIKI.safe(c.name || '?') + 'lv ' + e.lvlmin +\n (e.lvlmax !== e.lvlmin ? '–' + e.lvlmax : '') + '' + (isFinite(share)\n ? Math.round(share * 100) + '% of spawns'\n : '—') + '
';\n if (broken) {\n h += '
Nothing spawns here. The area\\'s entries carry ' +\n 'no spawn weight, so the table the game rolls against comes out as ' +\n 'NaN and no creature can ever match it. A bug in the base game, left ' +\n 'as it is — nothing in the game travels to this area either.
';\n }\n }\n if (z.drop && z.drop.length) {\n var d = z.drop.map(function (x) {\n return MOD_WIKI.safe((x.item && x.item.name) || '?') +\n ' ' + ((x.c || 0) * 100).toFixed(2) + '%';\n }).join(', ');\n h += '
Drops: ' + d + '
';\n }\n h += '
';\n });\n\n var fightsOrig = MOD_FIGHTS.mode === 'original';\n h += '

How hard an enemy is

' +\n '

It depends on the Fights setting, which is ' +\n (fightsOrig ? 'Original' : 'Scaled') + ' now. On Original (the default) ' +\n 'every area the author wrote is exactly as he wrote it: his creatures, his ' +\n 'level ranges, and you out-grow them as you do in his game. On Scaled every ' +\n 'area is sized to you. The three areas past the golem arena and the rank ' +\n 'duels are sized to you either way.

' +\n '

Sizing to you works like this. Enemies are not fitted to a level. Each spawn is solved against your ' +\n 'actual power at the moment it appears, aiming at roughly ' + MOD_ENEMY.kill +\n ' swings to kill it and ' + MOD_ENEMY.die + ' before it kills you, with a ' +\n 'guaranteed margin between the two so a fight is never a coin flip. Variety ' +\n 'comes from the area\\'s level band and each creature\\'s own stat weighting, ' +\n 'not from difficulty scaling.

' +\n '

The reason for that: within the first stage alone you go from 1 STR to ' +\n 'around 265, so anything anchored to a level is either trivial at the end of ' +\n 'a stage or impossible at the start of one.

';\n return h;\n});\n\n/* --- 8. items -------------------------------------------------------------\n The one place where \"list everything\" needs help: 321 of the game's 371\n plain items are the same inventory type, so grouping by type alone leaves a\n single group of 321. Anything past MOD_WIKI_ALPHA_AT gets an alphabetical\n sub-split as well — the oldest index pattern there is, and derived rather\n than invented, which a hand-made taxonomy would not be.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_ALPHA_AT = 60;\n\n/* The game's own inventory tabs (isort): ALL / WPN / EQP / USE / OTHER. Using\n its categories rather than new ones means a group here is a tab there. */\nvar MOD_WIKI_STYPE = { 2: 'Weapons', 3: 'Equipment', 4: 'Usable', 5: 'Other' };\n\nfunction MOD_wikiItemRow(it) {\n var val = it.v !== undefined ? it.v : (it.val !== undefined ? it.val : null);\n return '
' +\n '

' + MOD_WIKI.safe(it.name) +\n (it.rar > 1 ? 'rarity ' + it.rar + '' : '') +\n (val !== null ? '' + MOD_wikiNum(val) + ' coin' : '') +\n (it.amount > 0 ? '×' + it.amount + '' : '') +\n '

' + MOD_wikiDesc(it.desc) + '
';\n}\n\n/* The letter a name files under. Several items are quoted — the master's\n manuals are literally named \"Sword Saint Manual\" — and filing those under a\n quotation mark is no use to anyone looking for S. */\nfunction MOD_wikiLetter(name) {\n var m = String(name || '').replace(/^[^0-9A-Za-z]+/, '');\n return (m.charAt(0) || '?').toUpperCase();\n}\n\nfunction MOD_wikiByLetter(a, b) {\n var x = MOD_wikiLetter(a.name) + String(a.name || '').toLowerCase();\n var y = MOD_wikiLetter(b.name) + String(b.name || '').toLowerCase();\n return x < y ? -1 : (x > y ? 1 : 0);\n}\n\n/* Flat below the threshold, alphabetically sub-grouped above it. Ranges are cut\n from the entries actually present, so they stay even as content is added. */\nfunction MOD_wikiAlpha(entries, render, level) {\n if (entries.length <= MOD_WIKI_ALPHA_AT) return entries.map(render).join('');\n var per = Math.ceil(entries.length / Math.ceil(entries.length / 40));\n var out = '';\n for (var i = 0; i < entries.length; i += per) {\n var chunk = entries.slice(i, i + per);\n var first = MOD_wikiLetter(chunk[0].name);\n var last = MOD_wikiLetter(chunk[chunk.length - 1].name);\n out += MOD_wikiGroup(first === last ? first : first + '–' + last,\n chunk.length, chunk.map(render).join(''), level || 4);\n }\n return out;\n}\n\nMOD_wikiPage('items', 'Items', function () {\n var h = '

Items

' +\n '

Everything the game defines, whether or not you have seen ' +\n 'it. Grouped the way your inventory groups it. Values are the base sell ' +\n 'price where the game gives one.

';\n\n /* item[] first, split by the inventory tab it appears under. */\n var buckets = {};\n for (var k in item) {\n var it = item[k];\n if (!it || !it.name || it.name === 'dummy') continue;\n var lab = MOD_WIKI_STYPE[it.stype] || 'Uncategorised';\n (buckets[lab] = buckets[lab] || []).push(it);\n }\n ['Usable', 'Other', 'Weapons', 'Equipment', 'Uncategorised'].forEach(function (lab) {\n var list = buckets[lab];\n if (!list || !list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(lab, list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n\n /* and the equipment objects, which live in their own globals */\n [['Weapons', wpn], ['Armour', eqp], ['Shields', sld], ['Accessories', acc]]\n .forEach(function (g) {\n var list = [];\n for (var kk in g[1]) {\n if (g[1][kk] && g[1][kk].name && g[1][kk].name !== 'dummy') list.push(g[1][kk]);\n }\n if (!list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(g[0], list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n return h;\n});\n\n/* --- 9. actions ----------------------------------------------------------- */\nMOD_wikiPage('actions', 'Actions', function () {\n var unlock = {};\n MOD_ACTION_UNLOCKS.forEach(function (u) {\n if (u.act) unlock[u.act.id] = (skl[u.skill] ? skl[u.skill].name : u.skill) +\n ' level ' + u.lv;\n });\n if (typeof act.mod_qi !== 'undefined') {\n unlock[act.mod_qi.id] = 'clearing the dojo\\'s Easiest, Easy and Normal fights';\n }\n\n var h = '

Actions

' +\n '

What you can be doing. One at a time, and only where the ' +\n 'action makes sense — unless \"Unrestricted actions\" is ticked in settings, ' +\n 'which lets several run at once anywhere. Both limits are deliberate; the ' +\n 'switch is there because they are not to everyone\\'s taste.

';\n\n var keys = [];\n for (var k in act) if (act[k] && act[k].name && act[k].name !== 'dummy') keys.push(k);\n keys.sort(function (a, b) { return act[a].name < act[b].name ? -1 : 1; });\n\n keys.forEach(function (k) {\n var a = act[k];\n var how = unlock[a.id];\n h += '
' +\n '

' + MOD_WIKI.safe(a.name) +\n (a.have === true ? 'unlocked'\n : 'locked') +\n (how ? 'from ' + MOD_WIKI.safe(how) + '' : '') +\n '

' + MOD_wikiDesc(a.desc) + '
';\n });\n return h;\n});\n\n/* --- 10. the dojo --------------------------------------------------------- */\nMOD_wikiPage('dojo', 'The dojo', function () {\n var h = '

The dojo

' +\n '

The instructor promises a reward every five levels. The base ' +\n 'game stops delivering at 30; the mod carries the same ladder to 110, on the ' +\n 'same terms.

' +\n '

Only the lowest rung you have not claimed is offered, which is how the ' +\n 'first six behave. Rewards are sized against the character experience curve ' +\n '(4×level3 + level2) — a level at 110 costs 5.3 ' +\n 'million, which is why the base game\\'s best pill is worth 0.3% of one there.

' +\n '' +\n '';\n\n MOD_DOJO_RUNGS.forEach(function (r) {\n var pill = item[r.pill];\n var extra = [];\n if (r.realmPill) extra.push('breakthrough pill for realm ' + r.realmPill);\n if (r.manual) extra.push('a choice of master\\'s manual');\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
LevelRewardCoinAlso
' + r.lv + '' + (pill ? MOD_WIKI.safe(pill.name) + ' ×' + r.n\n : '—') + '' + MOD_wikiNum(r.coin) + '' + (extra.join(', ') || '—') + '' + (global.flags[r.flag] ? 'claimed' : '') +\n '
' +\n\n '

Spirit pills

' +\n '

Character experience in a bottle. The four the mod adds continue the ' +\n 'game\\'s own series up to the new curve.

' +\n '' +\n '';\n MOD_PILLS.forEach(function (p) {\n h += '';\n });\n h += '
PillExperience
' + MOD_WIKI.safe(p[2]) + '' +\n p[3].toLocaleString() + '
' +\n\n '

Master\\'s manuals

' +\n '

A third grade of skillbook, offered at levels 50, 75 and 100 as a choice ' +\n 'the same way the first one was. Each is worth +' +\n Math.round(MOD_MANUAL_RATE * 100) + '% experience in its skill, permanently.

' +\n '' +\n '';\n MOD_MANUALS.forEach(function (m) {\n h += '';\n });\n h += '
ManualSkill
' + MOD_WIKI.safe(m[2]) + '' +\n MOD_WIKI.safe(skl[m[0]] ? (skl[m[0]].bname || skl[m[0]].name) : m[0]) +\n '
';\n return h;\n});\n\n/* --- 11. mechanics -------------------------------------------------------- */\nMOD_wikiPage('mechanics', 'Mechanics', function () {\n return '

Mechanics

' +\n\n '

Damage is subtractive, in both directions

' +\n '

A hit does the attacker\\'s strength minus the defender\\'s strength, plus ' +\n 'the weapon, plus one, before criticals. A defender\\'s STR is their ' +\n 'armour — there is no separate armour stat doing that job.

' +\n '

Two consequences worth carrying around:

' +\n '
    ' +\n '
  • Damage does not taper, it stops. Once a defender\\'s STR passes ' +\n 'yours, your hits do not get small; they land for nothing. Progress against ' +\n 'something too strong is not slow, it is zero.
  • ' +\n '
  • A stat ratio tells you nothing. Comparing your HP to an enemy\\'s ' +\n 'strength ignores the subtraction entirely and reads several times off in ' +\n 'either direction.
  • ' +\n '
' +\n '

This is also the reason enemies are never scaled by raising ' +\n 'their strength: it would raise their offence and their armour together, and ' +\n 'the second effect arrives first.

' +\n\n '

Criticals carry the late game

' +\n '

Critical rate reaches about a third at the top of the ladder and a ' +\n 'critical is roughly eight times a normal swing, so around three quarters of ' +\n 'all damage done at that point is critical damage. Anything that raises ' +\n 'critical rate or critical damage is worth more than it looks; Sharp Eye and ' +\n 'War are the two that do.

' +\n\n '

Hit chance

' +\n '

Landing a hit is a contest of agility. It is a ratio rather than a ' +\n 'subtraction, so agility behaves more gently than strength does — falling ' +\n 'behind on it costs you accuracy, not everything.

' +\n\n '

Skill sections and parents

' +\n '

Each of the ' + Object.keys(MOD_SECTIONS).length + ' sections has a parent ' +\n 'skill. A parent earns ' + Math.round(MOD_PARENT.feed * 100) + '% of everything ' +\n 'its children earn and grants no stats of its own. In return, a child below ' +\n 'the parent\\'s level trains ' + Math.round(MOD_PARENT.perLevel * 100) +\n '% faster per level of lag, up to ' + MOD_PARENT.maxGap + ' levels behind. ' +\n 'Bringing one section up makes every skill in it cheaper to catch up.

' +\n\n '

Saving

' +\n '

Three slots, switched from the bottom bar. The game builds its world once ' +\n 'at startup and cannot unload a save, so switching slots or starting a new ' +\n 'game reloads the page. Exporting a save before running anything unusual is ' +\n 'always the safe move.

';\n});\n\n/* --- the document ---------------------------------------------------------\n One string, no fetches, no dependencies: this has to work from file:// as\n readily as from a server.\n ------------------------------------------------------------------------- */\n\nfunction MOD_wikiCss() {\n return [\n ':root{color-scheme:dark}',\n '*{box-sizing:border-box}',\n /* Menlo and Consolas before the generic: with MS Gothic missing (any Mac)\n \"monospace\" is Courier, which Safari sets wider than Chrome does, and\n table cells wrapped on one and not the other. */\n 'body{margin:0;background:#060a1c;color:#dfe6f5;font:14px/1.6 \"MS Gothic\",' +\n '\"MS Mincho\",Menlo,Consolas,\"DejaVu Sans Mono\",monospace;display:flex;min-height:100vh}',\n 'a{color:#7cb0ff}',\n 'nav{width:210px;flex:0 0 210px;background:#081040;border-right:1px solid #46a;' +\n 'position:sticky;top:0;height:100vh;overflow:auto;padding:14px 0}',\n 'nav h2{font-size:13px;color:#9fb4d8;margin:0 0 10px 14px;letter-spacing:1px}',\n 'nav a{display:block;padding:6px 14px;text-decoration:none;color:#cfe0ff;' +\n 'border-left:3px solid transparent}',\n 'nav a:hover{background:#154080}',\n 'nav a.on{background:#123070;border-left-color:#7cb0ff;color:#fff}',\n 'nav .srch{margin:0 10px 12px;width:calc(100% - 20px);background:#050a20;' +\n 'color:#dfe6f5;border:1px solid #46a;padding:5px;font:inherit}',\n 'nav .xall{display:block;margin:12px 10px 0;color:#8296b8;font-size:12px;' +\n 'cursor:pointer;-webkit-user-select:none;user-select:none}',\n 'nav .xall input{vertical-align:-1px;margin-right:4px}',\n 'details.wk-g{border:1px solid #17224a;background:#0a1130;margin:8px 0}',\n 'details.wk-g>summary{cursor:pointer;padding:7px 12px;list-style:none;' +\n 'background:#0d1740}',\n 'details.wk-g>summary:hover{background:#154080}',\n 'details.wk-g>summary::-webkit-details-marker{display:none}',\n // a caret drawn rather than borrowed, so it is the same in every browser\n 'details.wk-g>summary::before{content:\"\\\\25B8\";color:#7cb0ff;' +\n 'display:inline-block;width:1em;transition:transform .12s}',\n 'details.wk-g[open]>summary::before{transform:rotate(90deg)}',\n 'details.wk-g>summary h2,details.wk-g>summary h3{display:inline;margin:0;' +\n 'font-size:15px;color:#fff}',\n '.wk-gi{padding:2px 12px 10px}',\n 'details.wk-g details.wk-g>summary h4{display:inline;margin:0;font-size:13px;' +\n 'color:#cfe0ff;font-weight:normal}',\n 'details.wk-g details.wk-g>summary{padding:5px 10px;background:#0b1435}',\n 'main{flex:1;min-width:0;padding:22px 30px 80px;max-width:1000px}',\n 'h1{font-size:26px;margin:0 0 6px;color:#fff;border-bottom:1px solid #46a;' +\n 'padding-bottom:8px}',\n 'h2{font-size:19px;color:#bcd4ff;margin:30px 0 8px}',\n 'h3{font-size:15px;color:#fff;margin:0 0 4px}',\n 'p.lede{color:#a8bcdc;margin:0 0 18px}',\n '.dim{color:#8296b8;font-weight:normal}',\n '.flav{font-size:12px;font-style:italic}',\n '.note{background:#0b1638;border-left:3px solid #46a;padding:8px 12px;' +\n 'color:#a8bcdc;margin:14px 0}',\n 'code,pre{background:#0b1638;border:1px solid #24356e;padding:1px 5px;' +\n 'color:#9fd0ff}',\n 'pre{padding:10px;overflow-x:auto}',\n 'dl dt{color:#fff;margin-top:14px}',\n 'dl dd{margin:2px 0 0 0;color:#a8bcdc}',\n 'ul{color:#a8bcdc}',\n 'table.wk-tbl{border-collapse:collapse;width:100%;margin:10px 0;' +\n 'display:block;overflow-x:auto}',\n 'table.wk-tbl th{text-align:left;color:#9fb4d8;font-weight:normal;' +\n 'border-bottom:1px solid #46a;padding:5px 9px;white-space:nowrap}',\n 'table.wk-tbl td{border-bottom:1px solid #17224a;padding:5px 9px;' +\n 'vertical-align:top}',\n 'table.wk-tbl tr.done td{color:#cfe0ff}',\n 'table.wk-tbl tr.here{background:#123070}',\n '.num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}',\n '.wk-e{border:1px solid #17224a;background:#081040;padding:9px 12px;' +\n 'margin:7px 0;border-radius:2px}',\n 'tr.wk-e{border:0;background:none}',\n '.wk-e.done{border-color:#2a4a8c}',\n '.wk-e h3{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}',\n '.lv{margin-left:auto;font-size:12px;color:#8296b8;font-weight:normal}',\n '.lv.capped{color:#ffd24a}',\n '.rank{font-size:12px;color:#8296b8;font-weight:normal}',\n '.desc{color:#a8bcdc;font-size:13px}',\n '.proj{color:#ffd24a;font-size:12px;margin-top:3px}',\n '.tag{font-size:11px;border:1px solid #46a;color:#9fb4d8;padding:0 5px;' +\n 'border-radius:2px;font-weight:normal}',\n '.tag.mod{border-color:#7a4ea8;color:#c07bff}',\n '.tag.done{border-color:#2e7d4f;color:#6fdc9a}',\n '.tag.here{border-color:#ffd24a;color:#ffd24a}',\n '.tag.warn{border-color:#a8562e;color:#ff9a5c}',\n 'table.wk-perks{border-collapse:collapse;margin:6px 0 0;width:100%}',\n 'table.wk-perks td{padding:2px 8px 2px 0;font-size:12px;color:#8296b8;' +\n 'border-bottom:1px solid #101a3c}',\n 'table.wk-perks tr.done td{color:#cfe0ff}',\n 'table.wk-perks td.tick{color:#6fdc9a;width:1em;text-align:right}',\n // fixed columns so the level bands line up between one area and the next\n 'table.wk-pop{table-layout:fixed}',\n 'table.wk-pop td:first-child{width:60%}',\n 'table.wk-pop td:nth-child(2){width:20%}',\n '.wk-title .tname{font-weight:bold}',\n '.wk-now{background:#0b1638;border:1px solid #24356e;padding:10px 14px;' +\n 'margin:0 0 18px;display:inline-block;min-width:320px}',\n '.wk-now h3{color:#9fb4d8;font-weight:normal;font-size:13px;' +\n 'letter-spacing:1px;margin-bottom:6px}',\n '.wk-now td{padding:2px 16px 2px 0}',\n '.wk-now td:last-child{color:#fff;text-align:right}',\n '.pg{display:none}.pg.on{display:block}',\n 'footer{color:#5a6c8c;font-size:12px;border-top:1px solid #17224a;' +\n 'margin-top:40px;padding-top:12px}',\n '@media(max-width:760px){body{display:block}nav{width:auto;height:auto;' +\n 'position:static;flex:none}main{padding:16px}}'\n ].join('\\n');\n}\n\n/* Filtering, page switching and the hash. Small enough to inline, and inlining\n it is the point — the document has to stand on its own. */\nfunction MOD_wikiJs() {\n return [\n 'var pages=[].slice.call(document.querySelectorAll(\".pg\"));',\n 'var links=[].slice.call(document.querySelectorAll(\"nav a[data-p]\"));',\n 'function show(id){',\n ' var found=false;',\n ' pages.forEach(function(p){var on=p.id===\"pg-\"+id;if(on)found=true;',\n ' p.className=\"pg\"+(on?\" on\":\"\")});',\n ' if(!found&&pages.length){pages[0].className=\"pg on\";id=pages[0].id.slice(3)}',\n ' links.forEach(function(a){a.className=a.dataset.p===id?\"on\":\"\"});',\n ' window.scrollTo(0,0);return id;}',\n 'links.forEach(function(a){a.addEventListener(\"click\",function(e){',\n ' e.preventDefault();location.hash=a.dataset.p;show(a.dataset.p)})});',\n 'window.addEventListener(\"hashchange\",function(){show(location.hash.slice(1))});',\n 'show(location.hash.slice(1)||\"start\");',\n // Search filters entries, hides groups left with none, and OPENS the groups\n // that still have some — a match hidden inside a collapsed group reads as\n // no match at all, which is worse than not having search.\n 'var box=document.getElementById(\"srch\");',\n 'var xall=document.getElementById(\"xall\");',\n 'var groups=[].slice.call(document.querySelectorAll(\"details.wk-g\"));',\n 'function filter(){',\n ' var q=box.value.trim().toLowerCase();',\n ' pages.forEach(function(p){',\n ' [].slice.call(p.querySelectorAll(\".wk-e\")).forEach(function(e){',\n ' e.style.display=(!q||e.textContent.toLowerCase().indexOf(q)>=0)?\"\":\"none\"})});',\n ' groups.forEach(function(g){',\n ' var any=[].slice.call(g.querySelectorAll(\".wk-e\")).some(function(e){',\n ' return e.style.display!==\"none\"});',\n ' g.style.display=any?\"\":\"none\";',\n ' g.open=xall.checked||(!!q&&any)}); }',\n 'box.addEventListener(\"input\",filter);',\n 'box.addEventListener(\"keydown\",function(e){if(e.key===\"Escape\"){box.value=\"\";filter()}});',\n // Everything is collapsed by default because three of these pages run past\n // 40,000px open. This is the way back to one long document, which is what\n // the browser's own find needs.\n 'xall.addEventListener(\"change\",filter);'\n ].join('\\n');\n}\n\nfunction MOD_wikiBuild() {\n var nav = '', body = '';\n MOD_WIKI_PAGES.forEach(function (pg) {\n nav += '' +\n MOD_WIKI.safe(pg.label) + '';\n var inner;\n /* One page throwing must not take the wiki down with it — the page says so\n and the rest still renders. */\n try { inner = pg.build(); }\n catch (e) {\n inner = '

' + MOD_WIKI.safe(pg.label) + '

This page ' +\n 'could not be built: ' + MOD_WIKI.safe(e.message) + '

';\n console.warn('[mod] wiki page \"' + pg.id + '\" failed: ' + e.message);\n }\n body += '
' + inner + '
';\n });\n\n var stamp = new Date().toLocaleString();\n return '' +\n '' +\n '' + MOD_WIKI.safe(MOD_WIKI.title) + '' +\n '' +\n '' +\n '
' + body +\n '
Generated from the running game at ' + MOD_WIKI.safe(stamp) +\n ' — mod ' + MOD_WIKI.safe(MOD.version) + '. Everything here is read from the ' +\n 'live data, including your own save, so it cannot fall out of step with the ' +\n 'game. The game is truezhangwei\\'s; the mod is not endorsed by the author.' +\n '
' +\n ' inside content closing the block early, so that is the\n one thing scrubbed. */\n safe: function (h) {\n return String(h === undefined || h === null ? '' : h).replace(/<\\/script/gi, '<\\\\/script');\n }\n};\n\n/* Descriptions carry real HTML (coloured spans, the game's separator div), so\n they are passed through rather than escaped — but the separator becomes a\n rule, since a wiki row is not a tooltip. */\nfunction MOD_wikiDesc(d) {\n var s;\n try { s = (typeof d === 'function') ? d() : d; } catch (e) { s = ''; }\n if (!s) return '';\n s = String(s).split('
 
').join('
');\n return MOD_WIKI.safe(s);\n}\n\nfunction MOD_wikiNum(n) {\n if (!isFinite(n)) return '—';\n return Math.round(n).toLocaleString();\n}\n\n/* An effect line at a level the player has not reached yet. The effect\n functions read the skill they are handed, so the level is borrowed and put\n back; anything that throws simply gets no projection. */\nfunction MOD_wikiEffectAt(key, sk, lvl) {\n var eff = MOD_EFFECTS[key];\n if (!eff) return '';\n var was = sk.lvl, out = '';\n try { sk.lvl = lvl; out = eff(sk) || ''; }\n catch (e) { out = ''; }\n finally { sk.lvl = was; }\n return MOD_WIKI.safe(out);\n}\n\n/* --- page builders --------------------------------------------------------\n Each returns the inner HTML of one page. `wk-e` marks a searchable entry and\n `wk-g` a group that hides itself when all its entries are filtered out.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_PAGES = [];\n\nfunction MOD_wikiPage(id, label, build) {\n MOD_WIKI_PAGES.push({ id: id, label: label, build: build });\n}\n\n/* A collapsible group.\n\n Not decoration: with every skill, item and title listed, three of the eleven\n pages ran past 40,000 pixels — a hundred screens of scrolling, which is the\n same as not having the content at all. Collapsed, each of those pages opens\n as an index you can read in one screen and expand where you want.\n\n
rather than a click handler, so it still works with JavaScript off,\n the browser's own find can be made to reach inside it, and the summary is\n keyboard-reachable for free. `level` is the heading level for the summary,\n since a group is sometimes a section of a page and sometimes a subsection. */\nfunction MOD_wikiGroup(heading, count, inner, level) {\n var hn = 'h' + (level || 2);\n return '
<' + hn + '>' + heading +\n (count !== null && count !== undefined\n ? ' (' + count + ')' : '') +\n '
' + inner + '
';\n}\n\n/* --- 1. orientation ------------------------------------------------------- */\nMOD_wikiPage('start', 'Start here', function () {\n var cap = MOD_levelCap();\n var realm = MOD_realm();\n var h = '

proto23

' +\n '

An idle RPG by truezhangwei, plus a mod. You train skills by ' +\n 'doing things, the things you can do open up as the story does, and none of ' +\n 'it is in a hurry.

' +\n\n '

Where you are right now

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Character level' + MOD_wikiNum(you.lvl) + '
Skill level cap' + cap + ' — ' +\n MOD_WIKI.safe(MOD_CAP.name) + '
Cultivation realm' + MOD_WIKI.safe(realm.name) +\n ' (' + realm.n + ' of ' + (MOD_REALMS.length - 1) + ')
Titles held' + MOD_titleCount() + ' — renown ' +\n (skl.rnwn ? skl.rnwn.lvl : 0) + '
Coin' + MOD_wikiNum(you.wealth || 0) + '
' +\n\n '

The four things worth knowing early

' +\n '
' +\n '
Your skill levels are capped by the story, not by grinding.
' +\n '
The cap starts at 10 and ends at 110, and it moves when you reach a new ' +\n 'place — not when you earn enough experience. If a skill has stopped ' +\n 'levelling, you are not doing it wrong; you need to go somewhere. See ' +\n 'Progression.
' +\n\n '
Armour is subtraction, so levels matter more than they look.
' +\n '
A hit does the attacker\\'s STR minus the defender\\'s STR. That means a ' +\n 'small gap in strength is a large gap in damage, and a big enough one takes ' +\n 'your damage to nothing at all rather than merely reducing it. See ' +\n 'Mechanics.
' +\n\n '
Most skills have a parent that pulls them along.
' +\n '
Every section has a parent skill that earns a share of everything its ' +\n 'children do, and any child lagging behind the parent trains faster — up to ' +\n 'twice as fast at ' + MOD_PARENT.maxGap + ' levels behind. A neglected skill is ' +\n 'cheap to bring up later, which is the only reason having this many is ' +\n 'workable.
' +\n\n '
Cultivation is a second ladder, and it is not automatic.
' +\n '
Circulating qi levels a skill; the skill does not raise your realm. ' +\n 'Reaching the level opens a bottleneck, and breaking through it costs a pill ' +\n 'and can fail. See Cultivation.
' +\n '
' +\n\n '

What is the mod and what is the game

' +\n '

The game is index.html and it is entirely truezhangwei\\'s ' +\n 'work. Everything in mod.js is a modification of it and is not ' +\n 'endorsed by the author. Pages here mark added content with ' +\n 'mod where the distinction matters. The full ' +\n 'list of changes is in the changelog, on the bottom bar.

';\n return h;\n});\n\n/* --- 2. progression ------------------------------------------------------- */\nMOD_wikiPage('progress', 'Progression', function () {\n var cap = MOD_levelCap();\n var h = '

Progression

' +\n '

Skill levels are capped by how far the story has gone. Ten ' +\n 'rungs, 10 through 110. The cap applies to every skill at once; Renown is ' +\n 'the single exception, since it is counted rather than trained.

' +\n '' +\n '';\n\n MOD_TIERS.forEach(function (t) {\n var got = null;\n try { got = MOD_tierFlag(t); } catch (e) {}\n var here = (t.cap === cap);\n var need = t.needText ? t.needText\n : (t.flags.length ? t.flags.join(' or ') : 'the start of the game');\n h += '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
CapStageWhat opens it
' + t.cap + '' + MOD_WIKI.safe(t.name) + '' + MOD_WIKI.safe(need) + '' + (here ? 'you are here'\n : got ? 'reached' : '') + '
' +\n\n '

The experience curve

' +\n '

The game\\'s own skill curve is super-exponential — level 109 to 110 ' +\n 'alone costs ' + MOD_wikiNum(MOD_vanillaExpnext(109)) + ' experience, so the top of the ladder ' +\n 'is unreachable by a factor of about ten trillion. But the author only ever ' +\n 'designed skills up to level 15, and below that his curve is fine.

' +\n (MOD_XP.vanillaTo > 0\n ? '

So the mod keeps it. Below level ' + MOD_XP.vanillaTo + ' a skill costs ' +\n 'exactly what the original charges; from there it rises by ×' +\n MOD_XP.ratio.toFixed(4) + ' a level, a ratio solved so the climb to 110 takes ' +\n 'the same time it always did.'\n : '

You have chosen the Mod before 4.2 pacing: a plain geometric curve, ' +\n '×' + MOD_XP.ratio.toFixed(3) + ' a level from level 0, much faster than the ' +\n 'original early on. The Original pacing in settings puts back the ' +\n 'author\\'s own cost below level 10.') +\n ' Character level uses the game\\'s own curve, untouched.

' +\n '' +\n '';\n [1, 5, 9, 10, 15, 25, 50, 75, 90, 109].forEach(function (l) {\n h += '';\n });\n h += '
LevelExperience for the nextOriginal game
' + l +\n '' + MOD_wikiNum(MOD_expnextFor(l)) + '' +\n MOD_wikiNum(MOD_vanillaExpnext(l)) + '
';\n return h;\n});\n\n/* --- 3. skills ------------------------------------------------------------ */\nMOD_wikiPage('skills', 'Skills', function () {\n var cap = MOD_levelCap();\n var bySection = {};\n var keys = [];\n for (var k in skl) {\n var s = skl[k];\n if (!s || typeof s !== 'object' || !s.name) continue;\n keys.push(k);\n }\n keys.forEach(function (k) {\n var s = skl[k];\n var sec = MOD_sectionOf(s);\n (bySection[sec] = bySection[sec] || []).push(k);\n });\n\n var h = '

Skills

' +\n '

' + keys.length + ' skills in ' +\n Object.keys(MOD_SECTIONS).length + ' sections. Every one has perks at ' +\n MOD_LADDER.join(', ') + ', and every one that has milestones grants titles at ' +\n MOD_TITLE_RUNGS.join(', ') + '. Levels and ticks below are yours.

';\n\n Object.keys(MOD_SECTIONS).forEach(function (sec) {\n var list = bySection[sec];\n if (!list || !list.length) return;\n var parentKey = MOD_PARENT_KEY[sec];\n var g = '';\n if (parentKey && skl[parentKey]) {\n g += '

Parent skill: ' +\n MOD_WIKI.safe(skl[parentKey].bname || skl[parentKey].name) +\n ' — earns ' + Math.round(MOD_PARENT.feed * 100) +\n '% of what its children earn, and speeds up any child that lags behind it.

';\n }\n list.sort(function (a, b) { return skl[a].name < skl[b].name ? -1 : 1; });\n list.forEach(function (k) {\n var s = skl[k];\n var lvl = Number(s.lvl) || 0;\n g += '
' +\n '

' + MOD_WIKI.safe(s.bname || s.name) +\n (s.bname ? ' (' + MOD_WIKI.safe(s.name) + ')' : '') +\n '= cap ? ' capped' : '') + '\">lv ' + lvl +\n (lvl >= cap ? ' — at the cap' : '') + '

' +\n '
' + MOD_wikiDesc(s.desc) + '
';\n\n var at110 = MOD_wikiEffectAt(k, s, 110);\n if (at110) g += '
At level 110: ' + at110 + '
';\n\n if (s.mlstn && s.mlstn.length) {\n g += '';\n s.mlstn.forEach(function (m) {\n g += '' +\n '' +\n '' +\n '';\n });\n g += '
' + m.lv + '' + MOD_WIKI.safe(m.p || '—') + '' + (m.g === true ? '✓' : '') + '
';\n }\n g += '
';\n });\n h += MOD_wikiGroup(MOD_WIKI.safe(MOD_SECTIONS[sec]), list.length, g, 2);\n });\n return h;\n});\n\n/* --- 4. cultivation ------------------------------------------------------- */\nMOD_wikiPage('cultivation', 'Cultivation', function () {\n var now = MOD_realm();\n var elig = 0;\n try { elig = MOD_realmEligible().n; } catch (e) {}\n var qic = skl.qic ? skl.qic.lvl : 0;\n\n var h = '

Cultivation

' +\n '

Ten realms above Mortal. Circulating qi levels the Qi ' +\n 'Circulation skill; the skill does not advance the realm. Reaching the level ' +\n 'opens a bottleneck, and breaking through it costs a pill and can fail — the ' +\n 'pill is spent either way.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Realm' + MOD_WIKI.safe(MOD_realmTitle()) + '
Spiritual Root' + MOD_WIKI.safe(MOD_root().name) + '
Qi Circulationlevel ' + qic + '
Insight' + MOD_insight() + '
Bottleneck open' +\n (elig > now.n ? 'yes — ' + MOD_WIKI.safe(MOD_REALMS[now.n + 1].name) +\n ' is within reach' : 'no') + '
Qi Deviation' + (MOD_deviated()\n ? MOD_deviationLeft() + ' in-game minutes left' : 'no') + '
' +\n '

Realm thresholds are the same levels that earn a title rank, so realm N ' +\n 'is the level at which a rank N title arrives. Every realm multiplies all ' +\n 'stats and health, and the multipliers do not stack with each other — the ' +\n 'realm you are in is the one that applies.

' +\n '' +\n '';\n\n MOD_REALMS.forEach(function (r) {\n var pill = r.n >= 1 ? item['mod_bp' + r.n] : null;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmNameQi Circ.StatsHealthPill
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + r.qic + '×' + r.mult + '×' + r.hp + '' + (pill ? MOD_WIKI.safe(pill.name) : '—') +\n '
' +\n\n '

Where the pills come from

' +\n '

Every breakthrough pill has a source in normal play. None of them are ' +\n 'craftable, and none are cheap — a bottleneck you can buy in bulk is not a ' +\n 'bottleneck.

' +\n '' +\n '' +\n '' +\n '' +\n '
RealmsSource
1The dojo instructor, when qi ' +\n 'circulation is first taught
2–5The Herbalist in the village ' +\n '(vendor.pha1), restocked
6–10The dojo\\'s Level Advancement ' +\n 'ladder, and the Pill Tower
' +\n '

Realm 1 is deliberately not sold at the marketplace: ' +\n 'the marketplace is behind a 40%-a-visit chance encounter, and realm 1 opens ' +\n 'long before that is reliable.

' +\n\n '

Breaking through

' +\n '

A bottleneck wants three things, which is what the genre says it wants: ' +\n 'a pill, insight, and enough consolidation to make the ' +\n 'attempt worth making. The pill and the insight are both spent whether the ' +\n 'attempt works or not.

' +\n '

The odds improve the further past the threshold you are, plus your root, ' +\n 'plus time sat in Closed Door Training, and cap short of certain.

' +\n '' +\n '';\n [0, 2, 4, 6, 8].forEach(function (o) {\n var base = Math.min(0.55 + o * 0.05 + MOD_root().odds, 0.95);\n h += '';\n });\n h += '
Levels past the thresholdChanceAfter full seclusion
+' + o + '' +\n Math.round(base * 100) + '%' +\n Math.round(Math.min(base + MOD_CULT.secludeOdds, 0.95) * 100) + '%
' +\n\n '

Insight

' +\n '

Insight is the half of the price that cannot be bought. It comes from ' +\n 'three places, and they are the three the genre names: meditating, fights you ' +\n 'nearly lost, and going somewhere you have not been.

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
SourceRate
Circulate Qi, at a wall' +\n (MOD_CULT.meditateChance * 100).toFixed(1) + '% a second
Closed Door Training' +\n (MOD_CULT.secludeChance * 100).toFixed(1) + '% a second
A kill made under ' +\n Math.round(MOD_CULT.brinkFrac * 100) + '% health — ' +\n (MOD_FIGHTS.mode === 'original'\n ? 'the author\\'s areas rarely come close once you reach them; the three past ' +\n 'the golem arena and the rank duels do, within a few fights, as does an area ' +\n 'you walk into early'\n : 'every fight costs a share of your health, so a few in a row without resting') +\n '' +\n Math.round(MOD_CULT.brinkChance * 100) + '% a kill
Entering an area for the first timealways
' +\n '

The insight a realm asks for is sized to the climb to it: about ' +\n Math.round(MOD_CULT.wallShare * 100) + '% of the Circulate Qi time the climb took, ' +\n 'spent at the wall. Seclusion earns it three times as fast, and full ' +\n 'consolidation takes as long as that on average.

' +\n '' +\n '' +\n '';\n var wkHours = function (sec) {\n return sec >= 7200 ? (sec / 3600).toFixed(sec >= 36000 ? 0 : 1) + ' h' : Math.round(sec / 60) + ' min';\n };\n MOD_REALMS.forEach(function (r) {\n if (!r.n) return;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmClimb (Circulate Qi, 1x)Insight to enterFull seclusionQi Deviation (in-game)
' + MOD_WIKI.safe(r.name) + '' + wkHours(MOD_realmClimbSec(r.n)) + '' + MOD_insightNeed(r.n) + '' + wkHours(MOD_secludeFull(r.n)) + '' + (MOD_devMinutes(r.n) / 1440).toFixed(1) + ' days
' +\n\n '

The nine layers

' +\n '

Each realm is nine layers deep, derived from where Qi Circulation sits ' +\n 'between this realm\\'s threshold and the next one\\'s — so breaking through ' +\n 'puts you at the first layer of the new realm on its own. Layers are worth ' +\n '×' + MOD_CULT.layerMult + ' stats and ×' + MOD_CULT.layerBody +\n ' body each above the first, and the top realm has no rung above it to ' +\n 'measure against, so it stays at its first layer.

' +\n '' +\n '';\n for (var li = 1; li <= MOD_CULT.layers; li++) {\n h += '';\n }\n h += '
LayerCalled
' +\n li + '' + MOD_LAYER_ORD[li] + ' layer, ' +\n MOD_WIKI.safe(MOD_layerStage(li)) + '
' +\n '

At the threshold of the next realm and before breaking ' +\n 'through, you are a half step to it — the genre\\'s own name for the ' +\n 'state this mod calls a bottleneck.

' +\n\n '

Closed Door Training

' +\n '

Seclusion does two things. It consolidates — a full sitting ' +\n '(the table above) is worth +' + Math.round(MOD_CULT.secludeOdds * 100) +\n '% on the attempt — and it shelters: a breakthrough attempted while ' +\n 'you are still sitting in it cannot end in Qi Deviation, only in a lost ' +\n 'pill. It will not start unless you are actually at a wall.

' +\n\n '

Qi Deviation

' +\n '

Failing a breakthrough out in the open leaves your cultivation base ' +\n 'unstable for as long as the seclusion you skipped (the table above, never ' +\n 'under ' + MOD_CULT.devMinutes + ' in-game minutes): stats at ×' +\n MOD_CULT.devStat + ', body at ×' + MOD_CULT.devBody + ', and no second ' +\n 'attempt until it passes. It clears on its own, or at once with a ' +\n 'Qi Settling Pill. It never kills — the death system stays out of it.

' +\n\n '

Heavenly Tribulation

' +\n '

From realm ' + MOD_CULT.tribFrom + ' up, a successful breakthrough is ' +\n 'answered. The cost is a fraction of your maximum health, so it is a gate on ' +\n 'walking in healthy rather than a damage race; the realm bonus you are ' +\n 'standing on is what pays it. Fall to it and the realm does not open.

' +\n '' +\n '';\n MOD_REALMS.forEach(function (r) {\n if (!r.n || r.n < MOD_CULT.tribFrom) return;\n h += '' +\n '' +\n '';\n });\n h += '
RealmBoltsOf your max HP
' + MOD_WIKI.safe(r.name) + '' + MOD_tribulationBolts(r.n) + '' + Math.round(MOD_tribulationCost(r.n) * 100) + '%
' +\n\n '

Spiritual Root

' +\n '

Rolled once, when the dojo finishes teaching you to fight. It is never a ' +\n 'dead end: the worst grade costs cultivation speed, not access, and a ' +\n 'Root Cleansing Pill moves you up a grade.

' +\n '' +\n '';\n var rootTotal = 0;\n MOD_ROOTS.forEach(function (r) { rootTotal += r.weight; });\n MOD_ROOTS.forEach(function (r) {\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
GradeRootChanceCultivationBreakthrough
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + Math.round(r.weight / rootTotal * 100) + '%×' + r.xp + '' + (r.odds >= 0 ? '+' : '') +\n Math.round(r.odds * 100) + '%
';\n return h;\n});\n\n/* --- 5. elemental mastery ------------------------------------------------- */\nMOD_wikiPage('mastery', 'Elemental mastery', function () {\n var h = '

Elemental mastery mod

' +\n '

Six masteries, each with a technique that fires on its own ' +\n 'during a fight. Combat is automatic and there is no ability picker, so a ' +\n 'technique is a chance on each of your swings to become something else — one ' +\n 'that scales with INT and the element rather than with your weapon.

' +\n '

Nothing fires while you are still Mortal: the channels have to be open ' +\n 'first. Chance is ' + (MOD_TECH.perLevel * 100).toFixed(1) + '% per mastery ' +\n 'level plus ' + (MOD_TECH.perRealm * 100).toFixed(1) + '% per realm, capped at ' +\n Math.round(MOD_TECH.cap * 100) + '%; a technique lands for ×' +\n MOD_TECH.power + ' of a normal strike.

' +\n '' +\n '';\n\n var realm = 0;\n try { realm = MOD_realm().n; } catch (e) {}\n MOD_MASTERY.forEach(function (m) {\n var lvl = Number(m.skill.lvl) || 0;\n var ch = realm < 1 ? 0\n : Math.min(lvl * MOD_TECH.perLevel + realm * MOD_TECH.perRealm, MOD_TECH.cap);\n h += '' +\n '' +\n '' +\n '';\n });\n h += '
MasteryTechniqueLevelChance now
' +\n MOD_WIKI.safe(m.skill.name) + '' + MOD_WIKI.safe(m.name) + '' + lvl + '' + (realm < 1 ? 'Mortal'\n : (ch * 100).toFixed(1) + '%') + '
';\n return h;\n});\n\n/* --- 6. titles ------------------------------------------------------------ */\nMOD_wikiPage('titles', 'Titles', function () {\n var perm = MOD_permanentRank();\n var held = MOD_titleCount();\n var total = 0; for (var kk in ttl) if (ttl[kk] && ttl[kk].name) total++;\n\n var h = '

Titles

' +\n '

A title\\'s rank runs 1 to 10 and is derived from the level ' +\n 'that earns it, so a rank is a statement about where in the game you are. ' +\n 'Only the title you are wearing applies — until renown makes the lower ranks ' +\n 'permanent.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '
Titles held' + held + ' of ' + total + '
Renown level' + (skl.rnwn ? skl.rnwn.lvl : 0) +\n ' of ' + MOD_RENOWN_MAX + '
Ranks that apply without wearing' +\n (perm >= 1 ? '1–' + perm : 'none yet') + '
' +\n\n '

Ranks

' +\n '';\n MOD_RANK_AT.forEach(function (lv, i) {\n var st = MOD_rankStyle(i + 1);\n h += '' +\n '';\n });\n h += '
RankEarned from levelColour
' + (i + 1) + '' + lv + '' + '●●●' + '
' +\n\n '

Renown

' +\n '

Renown is not trained. It counts the titles you hold, and each level ' +\n 'multiplies every stat by ' + (1 + MOD_RENOWN_RATE) + ' — compounding, so ' +\n 'level ' + MOD_RENOWN_MAX + ' is ×' +\n (Math.round(MOD_renownMult(MOD_RENOWN_MAX) * 1000) / 1000) + '. At renown N, ' +\n 'every title of rank N or below applies permanently.

' +\n '' +\n '';\n MOD_RENOWN_STEPS.forEach(function (n, i) {\n h += '= n ? 'done' : '') + '\">' +\n '';\n });\n h += '
RenownTitles neededAll statsPassive ranks
' + (i + 1) +\n '' + n + '×' +\n (Math.round(MOD_renownMult(i + 1) * 1000) / 1000) + '1–' + (i + 1) + '
' +\n\n '

Every title

' +\n '

Grouped by the skill that grants it. What is left is either ' +\n 'a ladder of its own — creatures killed, items gathered, the ' +\n 'cultivation realms — or a one-off from the story.

';\n\n var groups = {}, fams = {}, loose = [];\n for (var k in ttl) {\n var t = ttl[k];\n /* Unlike the picker, the wiki KEEPS the author's broken entries and says\n so — same call as area.clg, which is listed and tagged \"nothing spawns\"\n rather than quietly dropped. A reader looking for why a title never\n appears is better served by the row than by its absence. */\n if (!t || t.name === undefined || t.name === null) continue;\n var sk = MOD_TITLE_SKILL[k];\n if (sk && skl[sk]) { (groups[sk] = groups[sk] || []).push(k); continue; }\n var stem = MOD_titleStem(k);\n if (stem) { (fams[stem] = fams[stem] || []).push(k); continue; }\n loose.push(k);\n }\n /* Same rule as the picker: a family of one is not a family. */\n Object.keys(fams).forEach(function (stem) {\n if (fams[stem].length < 2) { loose.push(fams[stem][0]); delete fams[stem]; }\n });\n var order = Object.keys(groups).sort(function (a, b) {\n return skl[a].name < skl[b].name ? -1 : 1;\n });\n\n var row = function (k) {\n var t = ttl[k];\n var st = MOD_rankStyle(t.rar);\n var gen = null;\n for (var i = 0; i < MOD_SKILL_TITLES.length; i++) {\n if (MOD_SKILL_TITLES[i].key === k) { gen = MOD_SKILL_TITLES[i]; break; }\n }\n /* The author left a handful unfinished: ttl.ddcd carries the literal string\n \"null\" for both name and description, and shpt2/shpt3/mone3 are blank.\n None is granted by anything, so none can be earned. */\n var broken = !t.name || t.name === 'null';\n return '
' +\n '' +\n MOD_WIKI.safe(t.name || '(unnamed)') + '' +\n (broken ? 'unfinished' : '') +\n 'rank ' + (t.rar || 1) + '' +\n (gen ? 'lv ' + gen.rung + ' · +' +\n Math.round(gen.xp * 100) + '% ' + MOD_WIKI.safe(skl[gen.skill].name) +\n ' exp' : '') +\n (t.have === true ? 'held' : '') +\n '
' + MOD_wikiDesc(t.desc) + '
';\n };\n\n var byRank = function (a, b) { return (ttl[a].rar || 1) - (ttl[b].rar || 1); };\n order.forEach(function (sk) {\n groups[sk].sort(byRank);\n h += MOD_wikiGroup(MOD_WIKI.safe(skl[sk].bname || skl[sk].name),\n groups[sk].length, groups[sk].map(row).join(''), 3);\n });\n /* The ladders that belong to no skill, by the same derivation the picker\n uses — so the two cannot drift apart. Sorted by the label, since that is\n what the reader scans. */\n var famList = Object.keys(fams)\n .map(function (stem) {\n fams[stem].sort(byRank);\n return { stem: stem,\n label: MOD_titleFamilyLabel(stem, fams[stem].map(function (k) { return ttl[k]; })) };\n })\n .sort(function (a, b) { return a.label < b.label ? -1 : a.label > b.label ? 1 : 0; });\n if (famList.length) {\n /* Nested one level down rather than sitting beside the skills. Eleven more\n top-level summaries cost ~530px on a page that is already the tallest in\n the wiki, and these are a different KIND of thing from a skill anyway. */\n var famInner = '', famTotal = 0;\n famList.forEach(function (f) {\n famTotal += fams[f.stem].length;\n famInner += MOD_wikiGroup(MOD_WIKI.safe(f.label), fams[f.stem].length,\n fams[f.stem].map(row).join(''), 4);\n });\n h += MOD_wikiGroup('Ladders of their own', famTotal, famInner, 3);\n }\n if (loose.length) {\n loose.sort(byRank);\n /* The story titles are the one group with no skill to divide them, and\n there are enough of them to need dividing. Rank rather than the alphabet:\n it is the axis they are already sorted on, and the one a reader cares\n about — a rank is a statement about where in the game a title comes from. */\n var inner;\n if (loose.length <= MOD_WIKI_ALPHA_AT) {\n inner = loose.map(row).join('');\n } else {\n var byRankNo = {};\n loose.forEach(function (k) {\n var r = Math.min(Math.max(Number(ttl[k].rar) || 1, 1), 10);\n (byRankNo[r] = byRankNo[r] || []).push(k);\n });\n inner = '';\n Object.keys(byRankNo).sort(function (a, b) { return a - b; }).forEach(function (r) {\n var st = MOD_rankStyle(r);\n inner += MOD_wikiGroup('Rank ' + r + '', byRankNo[r].length,\n byRankNo[r].map(row).join(''), 4);\n });\n }\n h += MOD_wikiGroup('From the story', loose.length, inner, 3);\n }\n return h;\n});\n\n/* --- 7. areas ------------------------------------------------------------- */\nMOD_wikiPage('areas', 'Areas & enemies', function () {\n var h = '

Areas & enemies

' +\n '

Every area, what lives in it and at what levels, and what it ' +\n 'drops. Enemy strength is not read off these levels — see the note below the ' +\n 'table.

';\n\n /* Two of the game's 21 areas are not places and listing them misleads:\n nwh \"Somewhere\" — where global.current_z parks whenever you are NOT in\n a fight, populated by creature.default (\"Nothing\").\n tst \"Test\" — reached only from chss.tst, which has id -1 and is in no\n sector, so nothing in the game can travel there.\n The test for \"is this a place\" is whether you can get to it, and neither\n can be. Everything else is listed whether or not you have been. */\n var MOD_WIKI_NOT_PLACES = { nwh: 1, tst: 1 };\n\n var keys = [];\n for (var k in area) {\n if (area[k] && area[k].pop && !MOD_WIKI_NOT_PLACES[k]) keys.push(k);\n }\n /* Ordered by the middle of the level band, which is the closest thing the\n game has to a difficulty order. */\n keys.sort(function (a, b) {\n var mid = function (z) {\n if (!z.pop.length) return 0;\n var s = 0; z.pop.forEach(function (e) { s += (e.lvlmin + e.lvlmax) / 2; });\n return s / z.pop.length;\n };\n return mid(area[a]) - mid(area[b]);\n });\n\n keys.forEach(function (k) {\n var z = area[k];\n /* An area whose baked bands are not finite can never spawn anything — see\n the note rendered under its table. Derived rather than listed by name, so\n it stays true if the game changes. */\n var broken = !z.popc || z.popc.length !== z.pop.length ||\n z.popc.some(function (band) { return !band || !isFinite(band[1] - band[0]); });\n h += '

' + MOD_WIKI.safe(z.name) +\n ' ' + k + '' +\n (broken ? 'nothing spawns' : '') +\n '' + (z.size === -1 ? 'endless' : z.size + ' to clear') +\n '

';\n if (z.pop.length) {\n /* The spawn share is NOT pop[i].c. z_bake normalises those weights into\n area.popc — a list of [lo,hi] bands that mon_gen rolls a uniform\n random against — and the weights do not have to sum to 1, so they are\n off wherever they don't. The Southern forest's .35/.45/.25 are really\n 33/43/24%. Read the bands the game actually rolls against. */\n var bands = z.popc || [];\n h += '';\n z.pop.forEach(function (e, i) {\n var c = e.crt || {};\n var band = bands[i];\n var share = band ? band[1] - band[0] : NaN;\n h += '' +\n '' +\n '';\n });\n h += '
' + MOD_WIKI.safe(c.name || '?') + 'lv ' + e.lvlmin +\n (e.lvlmax !== e.lvlmin ? '–' + e.lvlmax : '') + '' + (isFinite(share)\n ? Math.round(share * 100) + '% of spawns'\n : '—') + '
';\n if (broken) {\n h += '
Nothing spawns here. The area\\'s entries carry ' +\n 'no spawn weight, so the table the game rolls against comes out as ' +\n 'NaN and no creature can ever match it. A bug in the base game, left ' +\n 'as it is — nothing in the game travels to this area either.
';\n }\n }\n if (z.drop && z.drop.length) {\n var d = z.drop.map(function (x) {\n return MOD_WIKI.safe((x.item && x.item.name) || '?') +\n ' ' + ((x.c || 0) * 100).toFixed(2) + '%';\n }).join(', ');\n h += '
Drops: ' + d + '
';\n }\n h += '
';\n });\n\n var fightsOrig = MOD_FIGHTS.mode === 'original';\n h += '

How hard an enemy is

' +\n '

It depends on the Fights setting, which is ' +\n (fightsOrig ? 'Original' : 'Scaled') + ' now. On Original (the default) ' +\n 'every area the author wrote is exactly as he wrote it: his creatures, his ' +\n 'level ranges, and you out-grow them as you do in his game. On Scaled every ' +\n 'area is sized to you. The three areas past the golem arena and the rank ' +\n 'duels are sized to you either way.

' +\n '

Sizing to you works like this. Enemies are not fitted to a level. Each spawn is solved against your ' +\n 'actual power at the moment it appears, aiming at roughly ' + MOD_ENEMY.kill +\n ' swings to kill it and ' + MOD_ENEMY.die + ' before it kills you, with a ' +\n 'guaranteed margin between the two so a fight is never a coin flip. Variety ' +\n 'comes from the area\\'s level band and each creature\\'s own stat weighting, ' +\n 'not from difficulty scaling.

' +\n '

The reason for that: within the first stage alone you go from 1 STR to ' +\n 'around 265, so anything anchored to a level is either trivial at the end of ' +\n 'a stage or impossible at the start of one.

';\n return h;\n});\n\n/* --- 8. items -------------------------------------------------------------\n The one place where \"list everything\" needs help: 321 of the game's 371\n plain items are the same inventory type, so grouping by type alone leaves a\n single group of 321. Anything past MOD_WIKI_ALPHA_AT gets an alphabetical\n sub-split as well — the oldest index pattern there is, and derived rather\n than invented, which a hand-made taxonomy would not be.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_ALPHA_AT = 60;\n\n/* The game's own inventory tabs (isort): ALL / WPN / EQP / USE / OTHER. Using\n its categories rather than new ones means a group here is a tab there. */\nvar MOD_WIKI_STYPE = { 2: 'Weapons', 3: 'Equipment', 4: 'Usable', 5: 'Other' };\n\nfunction MOD_wikiItemRow(it) {\n var val = it.v !== undefined ? it.v : (it.val !== undefined ? it.val : null);\n return '
' +\n '

' + MOD_WIKI.safe(it.name) +\n (it.rar > 1 ? 'rarity ' + it.rar + '' : '') +\n (val !== null ? '' + MOD_wikiNum(val) + ' coin' : '') +\n (it.amount > 0 ? '×' + it.amount + '' : '') +\n '

' + MOD_wikiDesc(it.desc) + '
';\n}\n\n/* The letter a name files under. Several items are quoted — the master's\n manuals are literally named \"Sword Saint Manual\" — and filing those under a\n quotation mark is no use to anyone looking for S. */\nfunction MOD_wikiLetter(name) {\n var m = String(name || '').replace(/^[^0-9A-Za-z]+/, '');\n return (m.charAt(0) || '?').toUpperCase();\n}\n\nfunction MOD_wikiByLetter(a, b) {\n var x = MOD_wikiLetter(a.name) + String(a.name || '').toLowerCase();\n var y = MOD_wikiLetter(b.name) + String(b.name || '').toLowerCase();\n return x < y ? -1 : (x > y ? 1 : 0);\n}\n\n/* Flat below the threshold, alphabetically sub-grouped above it. Ranges are cut\n from the entries actually present, so they stay even as content is added. */\nfunction MOD_wikiAlpha(entries, render, level) {\n if (entries.length <= MOD_WIKI_ALPHA_AT) return entries.map(render).join('');\n var per = Math.ceil(entries.length / Math.ceil(entries.length / 40));\n var out = '';\n for (var i = 0; i < entries.length; i += per) {\n var chunk = entries.slice(i, i + per);\n var first = MOD_wikiLetter(chunk[0].name);\n var last = MOD_wikiLetter(chunk[chunk.length - 1].name);\n out += MOD_wikiGroup(first === last ? first : first + '–' + last,\n chunk.length, chunk.map(render).join(''), level || 4);\n }\n return out;\n}\n\nMOD_wikiPage('items', 'Items', function () {\n var h = '

Items

' +\n '

Everything the game defines, whether or not you have seen ' +\n 'it. Grouped the way your inventory groups it. Values are the base sell ' +\n 'price where the game gives one.

';\n\n /* item[] first, split by the inventory tab it appears under. */\n var buckets = {};\n for (var k in item) {\n var it = item[k];\n if (!it || !it.name || it.name === 'dummy') continue;\n var lab = MOD_WIKI_STYPE[it.stype] || 'Uncategorised';\n (buckets[lab] = buckets[lab] || []).push(it);\n }\n ['Usable', 'Other', 'Weapons', 'Equipment', 'Uncategorised'].forEach(function (lab) {\n var list = buckets[lab];\n if (!list || !list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(lab, list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n\n /* and the equipment objects, which live in their own globals */\n [['Weapons', wpn], ['Armour', eqp], ['Shields', sld], ['Accessories', acc]]\n .forEach(function (g) {\n var list = [];\n for (var kk in g[1]) {\n if (g[1][kk] && g[1][kk].name && g[1][kk].name !== 'dummy') list.push(g[1][kk]);\n }\n if (!list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(g[0], list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n return h;\n});\n\n/* --- 9. actions ----------------------------------------------------------- */\nMOD_wikiPage('actions', 'Actions', function () {\n var unlock = {};\n MOD_ACTION_UNLOCKS.forEach(function (u) {\n if (u.act) unlock[u.act.id] = (skl[u.skill] ? skl[u.skill].name : u.skill) +\n ' level ' + u.lv;\n });\n if (typeof act.mod_qi !== 'undefined') {\n unlock[act.mod_qi.id] = 'clearing the dojo\\'s Easiest, Easy and Normal fights';\n }\n\n var h = '

Actions

' +\n '

What you can be doing. One at a time, and only where the ' +\n 'action makes sense — unless \"Unrestricted actions\" is ticked in settings, ' +\n 'which lets several run at once anywhere. Both limits are deliberate; the ' +\n 'switch is there because they are not to everyone\\'s taste.

';\n\n var keys = [];\n for (var k in act) if (act[k] && act[k].name && act[k].name !== 'dummy') keys.push(k);\n keys.sort(function (a, b) { return act[a].name < act[b].name ? -1 : 1; });\n\n keys.forEach(function (k) {\n var a = act[k];\n var how = unlock[a.id];\n h += '
' +\n '

' + MOD_WIKI.safe(a.name) +\n (a.have === true ? 'unlocked'\n : 'locked') +\n (how ? 'from ' + MOD_WIKI.safe(how) + '' : '') +\n '

' + MOD_wikiDesc(a.desc) + '
';\n });\n return h;\n});\n\n/* --- 10. the dojo --------------------------------------------------------- */\nMOD_wikiPage('dojo', 'The dojo', function () {\n var h = '

The dojo

' +\n '

The instructor promises a reward every five levels. The base ' +\n 'game stops delivering at 30; the mod carries the same ladder to 110, on the ' +\n 'same terms.

' +\n '

Only the lowest rung you have not claimed is offered, which is how the ' +\n 'first six behave. Rewards are sized against the character experience curve ' +\n '(4×level3 + level2) — a level at 110 costs 5.3 ' +\n 'million, which is why the base game\\'s best pill is worth 0.3% of one there.

' +\n '' +\n '';\n\n MOD_DOJO_RUNGS.forEach(function (r) {\n var pill = item[r.pill];\n var extra = [];\n if (r.realmPill) extra.push('breakthrough pill for realm ' + r.realmPill);\n if (r.manual) extra.push('a choice of master\\'s manual');\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
LevelRewardCoinAlso
' + r.lv + '' + (pill ? MOD_WIKI.safe(pill.name) + ' ×' + r.n\n : '—') + '' + MOD_wikiNum(r.coin) + '' + (extra.join(', ') || '—') + '' + (global.flags[r.flag] ? 'claimed' : '') +\n '
' +\n\n '

Spirit pills

' +\n '

Character experience in a bottle. The four the mod adds continue the ' +\n 'game\\'s own series up to the new curve.

' +\n '' +\n '';\n MOD_PILLS.forEach(function (p) {\n h += '';\n });\n h += '
PillExperience
' + MOD_WIKI.safe(p[2]) + '' +\n p[3].toLocaleString() + '
' +\n\n '

Master\\'s manuals

' +\n '

A third grade of skillbook, offered at levels 50, 75 and 100 as a choice ' +\n 'the same way the first one was. Each is worth +' +\n Math.round(MOD_MANUAL_RATE * 100) + '% experience in its skill, permanently.

' +\n '' +\n '';\n MOD_MANUALS.forEach(function (m) {\n h += '';\n });\n h += '
ManualSkill
' + MOD_WIKI.safe(m[2]) + '' +\n MOD_WIKI.safe(skl[m[0]] ? (skl[m[0]].bname || skl[m[0]].name) : m[0]) +\n '
';\n return h;\n});\n\n/* --- 11. mechanics -------------------------------------------------------- */\nMOD_wikiPage('mechanics', 'Mechanics', function () {\n return '

Mechanics

' +\n\n '

Damage is subtractive, in both directions

' +\n '

A hit does the attacker\\'s strength minus the defender\\'s strength, plus ' +\n 'the weapon, plus one, before criticals. A defender\\'s STR is their ' +\n 'armour — there is no separate armour stat doing that job.

' +\n '

Two consequences worth carrying around:

' +\n '
    ' +\n '
  • Damage does not taper, it stops. Once a defender\\'s STR passes ' +\n 'yours, your hits do not get small; they land for nothing. Progress against ' +\n 'something too strong is not slow, it is zero.
  • ' +\n '
  • A stat ratio tells you nothing. Comparing your HP to an enemy\\'s ' +\n 'strength ignores the subtraction entirely and reads several times off in ' +\n 'either direction.
  • ' +\n '
' +\n '

This is also the reason enemies are never scaled by raising ' +\n 'their strength: it would raise their offence and their armour together, and ' +\n 'the second effect arrives first.

' +\n\n '

Criticals carry the late game

' +\n '

Critical rate reaches about a third at the top of the ladder and a ' +\n 'critical is roughly eight times a normal swing, so around three quarters of ' +\n 'all damage done at that point is critical damage. Anything that raises ' +\n 'critical rate or critical damage is worth more than it looks; Sharp Eye and ' +\n 'War are the two that do.

' +\n\n '

Hit chance

' +\n '

Landing a hit is a contest of agility. It is a ratio rather than a ' +\n 'subtraction, so agility behaves more gently than strength does — falling ' +\n 'behind on it costs you accuracy, not everything.

' +\n\n '

Skill sections and parents

' +\n '

Each of the ' + Object.keys(MOD_SECTIONS).length + ' sections has a parent ' +\n 'skill. A parent earns ' + Math.round(MOD_PARENT.feed * 100) + '% of everything ' +\n 'its children earn and grants no stats of its own. In return, a child below ' +\n 'the parent\\'s level trains ' + Math.round(MOD_PARENT.perLevel * 100) +\n '% faster per level of lag, up to ' + MOD_PARENT.maxGap + ' levels behind. ' +\n 'Bringing one section up makes every skill in it cheaper to catch up.

' +\n\n '

Saving

' +\n '

Three slots, switched from the bottom bar. The game builds its world once ' +\n 'at startup and cannot unload a save, so switching slots or starting a new ' +\n 'game reloads the page. Exporting a save before running anything unusual is ' +\n 'always the safe move.

';\n});\n\n/* --- the document ---------------------------------------------------------\n One string, no fetches, no dependencies: this has to work from file:// as\n readily as from a server.\n ------------------------------------------------------------------------- */\n\nfunction MOD_wikiCss() {\n return [\n ':root{color-scheme:dark}',\n '*{box-sizing:border-box}',\n /* Menlo and Consolas before the generic: with MS Gothic missing (any Mac)\n \"monospace\" is Courier, which Safari sets wider than Chrome does, and\n table cells wrapped on one and not the other. */\n 'body{margin:0;background:#060a1c;color:#dfe6f5;font:14px/1.6 \"MS Gothic\",' +\n '\"MS Mincho\",Menlo,Consolas,\"DejaVu Sans Mono\",monospace;display:flex;min-height:100vh}',\n 'a{color:#7cb0ff}',\n 'nav{width:210px;flex:0 0 210px;background:#081040;border-right:1px solid #46a;' +\n 'position:sticky;top:0;height:100vh;overflow:auto;padding:14px 0}',\n 'nav h2{font-size:13px;color:#9fb4d8;margin:0 0 10px 14px;letter-spacing:1px}',\n 'nav a{display:block;padding:6px 14px;text-decoration:none;color:#cfe0ff;' +\n 'border-left:3px solid transparent}',\n 'nav a:hover{background:#154080}',\n 'nav a.on{background:#123070;border-left-color:#7cb0ff;color:#fff}',\n 'nav .srch{margin:0 10px 12px;width:calc(100% - 20px);background:#050a20;' +\n 'color:#dfe6f5;border:1px solid #46a;padding:5px;font:inherit}',\n 'nav .xall{display:block;margin:12px 10px 0;color:#8296b8;font-size:12px;' +\n 'cursor:pointer;-webkit-user-select:none;user-select:none}',\n 'nav .xall input{vertical-align:-1px;margin-right:4px}',\n 'details.wk-g{border:1px solid #17224a;background:#0a1130;margin:8px 0}',\n 'details.wk-g>summary{cursor:pointer;padding:7px 12px;list-style:none;' +\n 'background:#0d1740}',\n 'details.wk-g>summary:hover{background:#154080}',\n 'details.wk-g>summary::-webkit-details-marker{display:none}',\n // a caret drawn rather than borrowed, so it is the same in every browser\n 'details.wk-g>summary::before{content:\"\\\\25B8\";color:#7cb0ff;' +\n 'display:inline-block;width:1em;transition:transform .12s}',\n 'details.wk-g[open]>summary::before{transform:rotate(90deg)}',\n 'details.wk-g>summary h2,details.wk-g>summary h3{display:inline;margin:0;' +\n 'font-size:15px;color:#fff}',\n '.wk-gi{padding:2px 12px 10px}',\n 'details.wk-g details.wk-g>summary h4{display:inline;margin:0;font-size:13px;' +\n 'color:#cfe0ff;font-weight:normal}',\n 'details.wk-g details.wk-g>summary{padding:5px 10px;background:#0b1435}',\n 'main{flex:1;min-width:0;padding:22px 30px 80px;max-width:1000px}',\n 'h1{font-size:26px;margin:0 0 6px;color:#fff;border-bottom:1px solid #46a;' +\n 'padding-bottom:8px}',\n 'h2{font-size:19px;color:#bcd4ff;margin:30px 0 8px}',\n 'h3{font-size:15px;color:#fff;margin:0 0 4px}',\n 'p.lede{color:#a8bcdc;margin:0 0 18px}',\n '.dim{color:#8296b8;font-weight:normal}',\n '.flav{font-size:12px;font-style:italic}',\n '.note{background:#0b1638;border-left:3px solid #46a;padding:8px 12px;' +\n 'color:#a8bcdc;margin:14px 0}',\n 'code,pre{background:#0b1638;border:1px solid #24356e;padding:1px 5px;' +\n 'color:#9fd0ff}',\n 'pre{padding:10px;overflow-x:auto}',\n 'dl dt{color:#fff;margin-top:14px}',\n 'dl dd{margin:2px 0 0 0;color:#a8bcdc}',\n 'ul{color:#a8bcdc}',\n 'table.wk-tbl{border-collapse:collapse;width:100%;margin:10px 0;' +\n 'display:block;overflow-x:auto}',\n 'table.wk-tbl th{text-align:left;color:#9fb4d8;font-weight:normal;' +\n 'border-bottom:1px solid #46a;padding:5px 9px;white-space:nowrap}',\n 'table.wk-tbl td{border-bottom:1px solid #17224a;padding:5px 9px;' +\n 'vertical-align:top}',\n 'table.wk-tbl tr.done td{color:#cfe0ff}',\n 'table.wk-tbl tr.here{background:#123070}',\n '.num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}',\n '.wk-e{border:1px solid #17224a;background:#081040;padding:9px 12px;' +\n 'margin:7px 0;border-radius:2px}',\n 'tr.wk-e{border:0;background:none}',\n '.wk-e.done{border-color:#2a4a8c}',\n '.wk-e h3{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}',\n '.lv{margin-left:auto;font-size:12px;color:#8296b8;font-weight:normal}',\n '.lv.capped{color:#ffd24a}',\n '.rank{font-size:12px;color:#8296b8;font-weight:normal}',\n '.desc{color:#a8bcdc;font-size:13px}',\n '.proj{color:#ffd24a;font-size:12px;margin-top:3px}',\n '.tag{font-size:11px;border:1px solid #46a;color:#9fb4d8;padding:0 5px;' +\n 'border-radius:2px;font-weight:normal}',\n '.tag.mod{border-color:#7a4ea8;color:#c07bff}',\n '.tag.done{border-color:#2e7d4f;color:#6fdc9a}',\n '.tag.here{border-color:#ffd24a;color:#ffd24a}',\n '.tag.warn{border-color:#a8562e;color:#ff9a5c}',\n 'table.wk-perks{border-collapse:collapse;margin:6px 0 0;width:100%}',\n 'table.wk-perks td{padding:2px 8px 2px 0;font-size:12px;color:#8296b8;' +\n 'border-bottom:1px solid #101a3c}',\n 'table.wk-perks tr.done td{color:#cfe0ff}',\n 'table.wk-perks td.tick{color:#6fdc9a;width:1em;text-align:right}',\n // fixed columns so the level bands line up between one area and the next\n 'table.wk-pop{table-layout:fixed}',\n 'table.wk-pop td:first-child{width:60%}',\n 'table.wk-pop td:nth-child(2){width:20%}',\n '.wk-title .tname{font-weight:bold}',\n '.wk-now{background:#0b1638;border:1px solid #24356e;padding:10px 14px;' +\n 'margin:0 0 18px;display:inline-block;min-width:320px}',\n '.wk-now h3{color:#9fb4d8;font-weight:normal;font-size:13px;' +\n 'letter-spacing:1px;margin-bottom:6px}',\n '.wk-now td{padding:2px 16px 2px 0}',\n '.wk-now td:last-child{color:#fff;text-align:right}',\n '.pg{display:none}.pg.on{display:block}',\n 'footer{color:#5a6c8c;font-size:12px;border-top:1px solid #17224a;' +\n 'margin-top:40px;padding-top:12px}',\n '@media(max-width:760px){body{display:block}nav{width:auto;height:auto;' +\n 'position:static;flex:none}main{padding:16px}}'\n ].join('\\n');\n}\n\n/* Filtering, page switching and the hash. Small enough to inline, and inlining\n it is the point — the document has to stand on its own. */\nfunction MOD_wikiJs() {\n return [\n 'var pages=[].slice.call(document.querySelectorAll(\".pg\"));',\n 'var links=[].slice.call(document.querySelectorAll(\"nav a[data-p]\"));',\n 'function show(id){',\n ' var found=false;',\n ' pages.forEach(function(p){var on=p.id===\"pg-\"+id;if(on)found=true;',\n ' p.className=\"pg\"+(on?\" on\":\"\")});',\n ' if(!found&&pages.length){pages[0].className=\"pg on\";id=pages[0].id.slice(3)}',\n ' links.forEach(function(a){a.className=a.dataset.p===id?\"on\":\"\"});',\n ' window.scrollTo(0,0);return id;}',\n 'links.forEach(function(a){a.addEventListener(\"click\",function(e){',\n ' e.preventDefault();location.hash=a.dataset.p;show(a.dataset.p)})});',\n 'window.addEventListener(\"hashchange\",function(){show(location.hash.slice(1))});',\n 'show(location.hash.slice(1)||\"start\");',\n // Search filters entries, hides groups left with none, and OPENS the groups\n // that still have some — a match hidden inside a collapsed group reads as\n // no match at all, which is worse than not having search.\n 'var box=document.getElementById(\"srch\");',\n 'var xall=document.getElementById(\"xall\");',\n 'var groups=[].slice.call(document.querySelectorAll(\"details.wk-g\"));',\n 'function filter(){',\n ' var q=box.value.trim().toLowerCase();',\n ' pages.forEach(function(p){',\n ' [].slice.call(p.querySelectorAll(\".wk-e\")).forEach(function(e){',\n ' e.style.display=(!q||e.textContent.toLowerCase().indexOf(q)>=0)?\"\":\"none\"})});',\n ' groups.forEach(function(g){',\n ' var any=[].slice.call(g.querySelectorAll(\".wk-e\")).some(function(e){',\n ' return e.style.display!==\"none\"});',\n ' g.style.display=any?\"\":\"none\";',\n ' g.open=xall.checked||(!!q&&any)}); }',\n 'box.addEventListener(\"input\",filter);',\n 'box.addEventListener(\"keydown\",function(e){if(e.key===\"Escape\"){box.value=\"\";filter()}});',\n // Everything is collapsed by default because three of these pages run past\n // 40,000px open. This is the way back to one long document, which is what\n // the browser's own find needs.\n 'xall.addEventListener(\"change\",filter);'\n ].join('\\n');\n}\n\nfunction MOD_wikiBuild() {\n var nav = '', body = '';\n MOD_WIKI_PAGES.forEach(function (pg) {\n nav += '' +\n MOD_WIKI.safe(pg.label) + '';\n var inner;\n /* One page throwing must not take the wiki down with it — the page says so\n and the rest still renders. */\n try { inner = pg.build(); }\n catch (e) {\n inner = '

' + MOD_WIKI.safe(pg.label) + '

This page ' +\n 'could not be built: ' + MOD_WIKI.safe(e.message) + '

';\n console.warn('[mod] wiki page \"' + pg.id + '\" failed: ' + e.message);\n }\n body += '
' + inner + '
';\n });\n\n var stamp = new Date().toLocaleString();\n return '' +\n '' +\n '' + MOD_WIKI.safe(MOD_WIKI.title) + '' +\n '' +\n '' +\n '
' + body +\n '
Generated from the running game at ' + MOD_WIKI.safe(stamp) +\n ' — mod ' + MOD_WIKI.safe(MOD.version) + '. Everything here is read from the ' +\n 'live data, including your own save, so it cannot fall out of step with the ' +\n 'game. The game is truezhangwei\\'s; the mod is not endorsed by the author.' +\n '
' +\n ' inside content closing the block early, so that is the\n one thing scrubbed. */\n safe: function (h) {\n return String(h === undefined || h === null ? '' : h).replace(/<\\/script/gi, '<\\\\/script');\n }\n};\n\n/* Descriptions carry real HTML (coloured spans, the game's separator div), so\n they are passed through rather than escaped — but the separator becomes a\n rule, since a wiki row is not a tooltip. */\nfunction MOD_wikiDesc(d) {\n var s;\n try { s = (typeof d === 'function') ? d() : d; } catch (e) { s = ''; }\n if (!s) return '';\n s = String(s).split('
 
').join('
');\n return MOD_WIKI.safe(s);\n}\n\nfunction MOD_wikiNum(n) {\n if (!isFinite(n)) return '—';\n return Math.round(n).toLocaleString();\n}\n\n/* An effect line at a level the player has not reached yet. The effect\n functions read the skill they are handed, so the level is borrowed and put\n back; anything that throws simply gets no projection. */\nfunction MOD_wikiEffectAt(key, sk, lvl) {\n var eff = MOD_EFFECTS[key];\n if (!eff) return '';\n var was = sk.lvl, out = '';\n try { sk.lvl = lvl; out = eff(sk) || ''; }\n catch (e) { out = ''; }\n finally { sk.lvl = was; }\n return MOD_WIKI.safe(out);\n}\n\n/* --- page builders --------------------------------------------------------\n Each returns the inner HTML of one page. `wk-e` marks a searchable entry and\n `wk-g` a group that hides itself when all its entries are filtered out.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_PAGES = [];\n\nfunction MOD_wikiPage(id, label, build) {\n MOD_WIKI_PAGES.push({ id: id, label: label, build: build });\n}\n\n/* A collapsible group.\n\n Not decoration: with every skill, item and title listed, three of the eleven\n pages ran past 40,000 pixels — a hundred screens of scrolling, which is the\n same as not having the content at all. Collapsed, each of those pages opens\n as an index you can read in one screen and expand where you want.\n\n
rather than a click handler, so it still works with JavaScript off,\n the browser's own find can be made to reach inside it, and the summary is\n keyboard-reachable for free. `level` is the heading level for the summary,\n since a group is sometimes a section of a page and sometimes a subsection. */\nfunction MOD_wikiGroup(heading, count, inner, level) {\n var hn = 'h' + (level || 2);\n return '
<' + hn + '>' + heading +\n (count !== null && count !== undefined\n ? ' (' + count + ')' : '') +\n '
' + inner + '
';\n}\n\n/* --- 1. orientation ------------------------------------------------------- */\nMOD_wikiPage('start', 'Start here', function () {\n var cap = MOD_levelCap();\n var realm = MOD_realm();\n var h = '

proto23

' +\n '

An idle RPG by truezhangwei, plus a mod. You train skills by ' +\n 'doing things, the things you can do open up as the story does, and none of ' +\n 'it is in a hurry.

' +\n\n '

Where you are right now

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Character level' + MOD_wikiNum(you.lvl) + '
Skill level cap' + cap + ' — ' +\n MOD_WIKI.safe(MOD_CAP.name) + '
Cultivation realm' + MOD_WIKI.safe(realm.name) +\n ' (' + realm.n + ' of ' + (MOD_REALMS.length - 1) + ')
Titles held' + MOD_titleCount() + ' — renown ' +\n (skl.rnwn ? skl.rnwn.lvl : 0) + '
Coin' + MOD_wikiNum(you.wealth || 0) + '
' +\n\n '

The four things worth knowing early

' +\n '
' +\n '
Your skill levels are capped by the story, not by grinding.
' +\n '
The cap starts at 10 and ends at 110, and it moves when you reach a new ' +\n 'place — not when you earn enough experience. If a skill has stopped ' +\n 'levelling, you are not doing it wrong; you need to go somewhere. See ' +\n 'Progression.
' +\n\n '
Armour is subtraction, so levels matter more than they look.
' +\n '
A hit does the attacker\\'s STR minus the defender\\'s STR. That means a ' +\n 'small gap in strength is a large gap in damage, and a big enough one takes ' +\n 'your damage to nothing at all rather than merely reducing it. See ' +\n 'Mechanics.
' +\n\n '
Most skills have a parent that pulls them along.
' +\n '
Every section has a parent skill that earns a share of everything its ' +\n 'children do, and any child lagging behind the parent trains faster — up to ' +\n 'twice as fast at ' + MOD_PARENT.maxGap + ' levels behind. A neglected skill is ' +\n 'cheap to bring up later, which is the only reason having this many is ' +\n 'workable.
' +\n\n '
Cultivation is a second ladder, and it is not automatic.
' +\n '
Circulating qi levels a skill; the skill does not raise your realm. ' +\n 'Reaching the level opens a bottleneck, and breaking through it costs a pill ' +\n 'and can fail. See Cultivation.
' +\n '
' +\n\n '

What is the mod and what is the game

' +\n '

The game is index.html and it is entirely truezhangwei\\'s ' +\n 'work. Everything in mod.js is a modification of it and is not ' +\n 'endorsed by the author. Pages here mark added content with ' +\n 'mod where the distinction matters. The full ' +\n 'list of changes is in the changelog, on the bottom bar.

';\n return h;\n});\n\n/* --- 2. progression ------------------------------------------------------- */\nMOD_wikiPage('progress', 'Progression', function () {\n var cap = MOD_levelCap();\n var h = '

Progression

' +\n '

Skill levels are capped by how far the story has gone. Ten ' +\n 'rungs, 10 through 110. The cap applies to every skill at once; Renown is ' +\n 'the single exception, since it is counted rather than trained.

' +\n '' +\n '';\n\n MOD_TIERS.forEach(function (t) {\n var got = null;\n try { got = MOD_tierFlag(t); } catch (e) {}\n var here = (t.cap === cap);\n var need = t.needText ? t.needText\n : (t.flags.length ? t.flags.join(' or ') : 'the start of the game');\n h += '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
CapStageWhat opens it
' + t.cap + '' + MOD_WIKI.safe(t.name) + '' + MOD_WIKI.safe(need) + '' + (here ? 'you are here'\n : got ? 'reached' : '') + '
' +\n\n '

The experience curve

' +\n '

The game\\'s own skill curve is super-exponential — level 109 to 110 ' +\n 'alone costs ' + MOD_wikiNum(MOD_vanillaExpnext(109)) + ' experience, so the top of the ladder ' +\n 'is unreachable by a factor of about ten trillion. But the author only ever ' +\n 'designed skills up to level 15, and below that his curve is fine.

' +\n (MOD_XP.vanillaTo > 0\n ? '

So the mod keeps it. Below level ' + MOD_XP.vanillaTo + ' a skill costs ' +\n 'exactly what the original charges; from there it rises by ×' +\n MOD_XP.ratio.toFixed(4) + ' a level, a ratio solved so the climb to 110 takes ' +\n 'the same time it always did.'\n : '

You have chosen the Mod before 4.2 pacing: a plain geometric curve, ' +\n '×' + MOD_XP.ratio.toFixed(3) + ' a level from level 0, much faster than the ' +\n 'original early on. The Original pacing in settings puts back the ' +\n 'author\\'s own cost below level 10.') +\n ' Character level uses the game\\'s own curve, untouched.

' +\n '' +\n '';\n [1, 5, 9, 10, 15, 25, 50, 75, 90, 109].forEach(function (l) {\n h += '';\n });\n h += '
LevelExperience for the nextOriginal game
' + l +\n '' + MOD_wikiNum(MOD_expnextFor(l)) + '' +\n MOD_wikiNum(MOD_vanillaExpnext(l)) + '
';\n return h;\n});\n\n/* --- 3. skills ------------------------------------------------------------ */\nMOD_wikiPage('skills', 'Skills', function () {\n var cap = MOD_levelCap();\n var bySection = {};\n var keys = [];\n for (var k in skl) {\n var s = skl[k];\n if (!s || typeof s !== 'object' || !s.name) continue;\n keys.push(k);\n }\n keys.forEach(function (k) {\n var s = skl[k];\n var sec = MOD_sectionOf(s);\n (bySection[sec] = bySection[sec] || []).push(k);\n });\n\n var h = '

Skills

' +\n '

' + keys.length + ' skills in ' +\n Object.keys(MOD_SECTIONS).length + ' sections. Every one has perks at ' +\n MOD_LADDER.join(', ') + ', and every one that has milestones grants titles at ' +\n MOD_TITLE_RUNGS.join(', ') + '. Levels and ticks below are yours.

';\n\n Object.keys(MOD_SECTIONS).forEach(function (sec) {\n var list = bySection[sec];\n if (!list || !list.length) return;\n var parentKey = MOD_PARENT_KEY[sec];\n var g = '';\n if (parentKey && skl[parentKey]) {\n g += '

Parent skill: ' +\n MOD_WIKI.safe(skl[parentKey].bname || skl[parentKey].name) +\n ' — earns ' + Math.round(MOD_PARENT.feed * 100) +\n '% of what its children earn, and speeds up any child that lags behind it.

';\n }\n list.sort(function (a, b) { return skl[a].name < skl[b].name ? -1 : 1; });\n list.forEach(function (k) {\n var s = skl[k];\n var lvl = Number(s.lvl) || 0;\n g += '
' +\n '

' + MOD_WIKI.safe(s.bname || s.name) +\n (s.bname ? ' (' + MOD_WIKI.safe(s.name) + ')' : '') +\n '= cap ? ' capped' : '') + '\">lv ' + lvl +\n (lvl >= cap ? ' — at the cap' : '') + '

' +\n '
' + MOD_wikiDesc(s.desc) + '
';\n\n var at110 = MOD_wikiEffectAt(k, s, 110);\n if (at110) g += '
At level 110: ' + at110 + '
';\n\n if (s.mlstn && s.mlstn.length) {\n g += '';\n s.mlstn.forEach(function (m) {\n g += '' +\n '' +\n '' +\n '';\n });\n g += '
' + m.lv + '' + MOD_WIKI.safe(m.p || '—') + '' + (m.g === true ? '✓' : '') + '
';\n }\n g += '
';\n });\n h += MOD_wikiGroup(MOD_WIKI.safe(MOD_SECTIONS[sec]), list.length, g, 2);\n });\n return h;\n});\n\n/* --- 4. cultivation ------------------------------------------------------- */\nMOD_wikiPage('cultivation', 'Cultivation', function () {\n var now = MOD_realm();\n var elig = 0;\n try { elig = MOD_realmEligible().n; } catch (e) {}\n var qic = skl.qic ? skl.qic.lvl : 0;\n\n var h = '

Cultivation

' +\n '

Ten realms above Mortal. Circulating qi levels the Qi ' +\n 'Circulation skill; the skill does not advance the realm. Reaching the level ' +\n 'opens a bottleneck, and breaking through it costs a pill and can fail — the ' +\n 'pill is spent either way.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Realm' + MOD_WIKI.safe(MOD_realmTitle()) + '
Spiritual Root' + MOD_WIKI.safe(MOD_root().name) + '
Qi Circulationlevel ' + qic + '
Insight' + MOD_insight() + '
Bottleneck open' +\n (elig > now.n ? 'yes — ' + MOD_WIKI.safe(MOD_REALMS[now.n + 1].name) +\n ' is within reach' : 'no') + '
Qi Deviation' + (MOD_deviated()\n ? MOD_deviationLeft() + ' in-game minutes left' : 'no') + '
' +\n '

Realm thresholds are the same levels that earn a title rank, so realm N ' +\n 'is the level at which a rank N title arrives. Every realm multiplies all ' +\n 'stats and health, and the multipliers do not stack with each other — the ' +\n 'realm you are in is the one that applies.

' +\n '' +\n '';\n\n MOD_REALMS.forEach(function (r) {\n var pill = r.n >= 1 ? item['mod_bp' + r.n] : null;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmNameQi Circ.StatsHealthPill
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + r.qic + '×' + r.mult + '×' + r.hp + '' + (pill ? MOD_WIKI.safe(pill.name) : '—') +\n '
' +\n\n '

Where the pills come from

' +\n '

Every breakthrough pill has a source in normal play. None of them are ' +\n 'craftable, and none are cheap — a bottleneck you can buy in bulk is not a ' +\n 'bottleneck.

' +\n '' +\n '' +\n '' +\n '' +\n '
RealmsSource
1The dojo instructor, when qi ' +\n 'circulation is first taught
2–5The Herbalist in the village ' +\n '(vendor.pha1), restocked
6–10The dojo\\'s Level Advancement ' +\n 'ladder, and the Pill Tower
' +\n '

Realm 1 is deliberately not sold at the marketplace: ' +\n 'the marketplace is behind a 40%-a-visit chance encounter, and realm 1 opens ' +\n 'long before that is reliable.

' +\n\n '

Breaking through

' +\n '

A bottleneck wants three things, which is what the genre says it wants: ' +\n 'a pill, insight, and enough consolidation to make the ' +\n 'attempt worth making. The pill and the insight are both spent whether the ' +\n 'attempt works or not.

' +\n '

The odds improve the further past the threshold you are, plus your root, ' +\n 'plus time sat in Closed Door Training, and cap short of certain.

' +\n '' +\n '';\n [0, 2, 4, 6, 8].forEach(function (o) {\n var base = Math.min(0.55 + o * 0.05 + MOD_root().odds, 0.95);\n h += '';\n });\n h += '
Levels past the thresholdChanceAfter full seclusion
+' + o + '' +\n Math.round(base * 100) + '%' +\n Math.round(Math.min(base + MOD_CULT.secludeOdds, 0.95) * 100) + '%
' +\n\n '

Insight

' +\n '

Insight is the half of the price that cannot be bought. It comes from ' +\n 'three places, and they are the three the genre names: meditating, fights you ' +\n 'nearly lost, and going somewhere you have not been.

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
SourceRate
Circulate Qi, at a wall' +\n (MOD_CULT.meditateChance * 100).toFixed(1) + '% a second
Closed Door Training' +\n (MOD_CULT.secludeChance * 100).toFixed(1) + '% a second
A kill made under ' +\n Math.round(MOD_CULT.brinkFrac * 100) + '% health — ' +\n (MOD_FIGHTS.mode === 'original'\n ? 'the author\\'s areas rarely come close once you reach them; the three past ' +\n 'the golem arena and the rank duels do, within a few fights, as does an area ' +\n 'you walk into early'\n : 'every fight costs a share of your health, so a few in a row without resting') +\n '' +\n Math.round(MOD_CULT.brinkChance * 100) + '% a kill
Entering an area for the first timealways
' +\n '

The insight a realm asks for is sized to the climb to it: about ' +\n Math.round(MOD_CULT.wallShare * 100) + '% of the Circulate Qi time the climb took, ' +\n 'spent at the wall. Seclusion earns it three times as fast, and full ' +\n 'consolidation takes as long as that on average.

' +\n '' +\n '' +\n '';\n var wkHours = function (sec) {\n return sec >= 7200 ? (sec / 3600).toFixed(sec >= 36000 ? 0 : 1) + ' h' : Math.round(sec / 60) + ' min';\n };\n MOD_REALMS.forEach(function (r) {\n if (!r.n) return;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmClimb (Circulate Qi, 1x)Insight to enterFull seclusionQi Deviation (in-game)
' + MOD_WIKI.safe(r.name) + '' + wkHours(MOD_realmClimbSec(r.n)) + '' + MOD_insightNeed(r.n) + '' + wkHours(MOD_secludeFull(r.n)) + '' + (MOD_devMinutes(r.n) / 1440).toFixed(1) + ' days
' +\n\n '

The nine layers

' +\n '

Each realm is nine layers deep, derived from where Qi Circulation sits ' +\n 'between this realm\\'s threshold and the next one\\'s — so breaking through ' +\n 'puts you at the first layer of the new realm on its own. Layers are worth ' +\n '×' + MOD_CULT.layerMult + ' stats and ×' + MOD_CULT.layerBody +\n ' body each above the first, and the top realm has no rung above it to ' +\n 'measure against, so it stays at its first layer.

' +\n '' +\n '';\n for (var li = 1; li <= MOD_CULT.layers; li++) {\n h += '';\n }\n h += '
LayerCalled
' +\n li + '' + MOD_LAYER_ORD[li] + ' layer, ' +\n MOD_WIKI.safe(MOD_layerStage(li)) + '
' +\n '

At the threshold of the next realm and before breaking ' +\n 'through, you are a half step to it — the genre\\'s own name for the ' +\n 'state this mod calls a bottleneck.

' +\n\n '

Closed Door Training

' +\n '

Seclusion does two things. It consolidates — a full sitting ' +\n '(the table above) is worth +' + Math.round(MOD_CULT.secludeOdds * 100) +\n '% on the attempt — and it shelters: a breakthrough attempted while ' +\n 'you are still sitting in it cannot end in Qi Deviation, only in a lost ' +\n 'pill. It will not start unless you are actually at a wall.

' +\n\n '

Qi Deviation

' +\n '

Failing a breakthrough out in the open leaves your cultivation base ' +\n 'unstable for as long as the seclusion you skipped (the table above, never ' +\n 'under ' + MOD_CULT.devMinutes + ' in-game minutes): stats at ×' +\n MOD_CULT.devStat + ', body at ×' + MOD_CULT.devBody + ', and no second ' +\n 'attempt until it passes. It clears on its own, or at once with a ' +\n 'Qi Settling Pill. It never kills — the death system stays out of it.

' +\n\n '

Heavenly Tribulation

' +\n '

From realm ' + MOD_CULT.tribFrom + ' up, a successful breakthrough is ' +\n 'answered. The cost is a fraction of your maximum health, so it is a gate on ' +\n 'walking in healthy rather than a damage race; the realm bonus you are ' +\n 'standing on is what pays it. Fall to it and the realm does not open.

' +\n '' +\n '';\n MOD_REALMS.forEach(function (r) {\n if (!r.n || r.n < MOD_CULT.tribFrom) return;\n h += '' +\n '' +\n '';\n });\n h += '
RealmBoltsOf your max HP
' + MOD_WIKI.safe(r.name) + '' + MOD_tribulationBolts(r.n) + '' + Math.round(MOD_tribulationCost(r.n) * 100) + '%
' +\n\n '

Spiritual Root

' +\n '

Rolled once, when the dojo finishes teaching you to fight. It is never a ' +\n 'dead end: the worst grade costs cultivation speed, not access, and a ' +\n 'Root Cleansing Pill moves you up a grade.

' +\n '' +\n '';\n var rootTotal = 0;\n MOD_ROOTS.forEach(function (r) { rootTotal += r.weight; });\n MOD_ROOTS.forEach(function (r) {\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
GradeRootChanceCultivationBreakthrough
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + Math.round(r.weight / rootTotal * 100) + '%×' + r.xp + '' + (r.odds >= 0 ? '+' : '') +\n Math.round(r.odds * 100) + '%
';\n return h;\n});\n\n/* --- 5. elemental mastery ------------------------------------------------- */\nMOD_wikiPage('mastery', 'Elemental mastery', function () {\n var h = '

Elemental mastery mod

' +\n '

Six masteries, each with a technique that fires on its own ' +\n 'during a fight. Combat is automatic and there is no ability picker, so a ' +\n 'technique is a chance on each of your swings to become something else — one ' +\n 'that scales with INT and the element rather than with your weapon.

' +\n '

Nothing fires while you are still Mortal: the channels have to be open ' +\n 'first. Chance is ' + (MOD_TECH.perLevel * 100).toFixed(1) + '% per mastery ' +\n 'level plus ' + (MOD_TECH.perRealm * 100).toFixed(1) + '% per realm, capped at ' +\n Math.round(MOD_TECH.cap * 100) + '%; a technique lands for ×' +\n MOD_TECH.power + ' of a normal strike.

' +\n '' +\n '';\n\n var realm = 0;\n try { realm = MOD_realm().n; } catch (e) {}\n MOD_MASTERY.forEach(function (m) {\n var lvl = Number(m.skill.lvl) || 0;\n var ch = realm < 1 ? 0\n : Math.min(lvl * MOD_TECH.perLevel + realm * MOD_TECH.perRealm, MOD_TECH.cap);\n h += '' +\n '' +\n '' +\n '';\n });\n h += '
MasteryTechniqueLevelChance now
' +\n MOD_WIKI.safe(m.skill.name) + '' + MOD_WIKI.safe(m.name) + '' + lvl + '' + (realm < 1 ? 'Mortal'\n : (ch * 100).toFixed(1) + '%') + '
';\n return h;\n});\n\n/* --- 6. titles ------------------------------------------------------------ */\nMOD_wikiPage('titles', 'Titles', function () {\n var perm = MOD_permanentRank();\n var held = MOD_titleCount();\n var total = 0; for (var kk in ttl) if (ttl[kk] && ttl[kk].name) total++;\n\n var h = '

Titles

' +\n '

A title\\'s rank runs 1 to 10 and is derived from the level ' +\n 'that earns it, so a rank is a statement about where in the game you are. ' +\n 'Only the title you are wearing applies — until renown makes the lower ranks ' +\n 'permanent.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '
Titles held' + held + ' of ' + total + '
Renown level' + (skl.rnwn ? skl.rnwn.lvl : 0) +\n ' of ' + MOD_RENOWN_MAX + '
Ranks that apply without wearing' +\n (perm >= 1 ? '1–' + perm : 'none yet') + '
' +\n\n '

Ranks

' +\n '';\n MOD_RANK_AT.forEach(function (lv, i) {\n var st = MOD_rankStyle(i + 1);\n h += '' +\n '';\n });\n h += '
RankEarned from levelColour
' + (i + 1) + '' + lv + '' + '●●●' + '
' +\n\n '

Renown

' +\n '

Renown is not trained. It counts the titles you hold, and each level ' +\n 'multiplies every stat by ' + (1 + MOD_RENOWN_RATE) + ' — compounding, so ' +\n 'level ' + MOD_RENOWN_MAX + ' is ×' +\n (Math.round(MOD_renownMult(MOD_RENOWN_MAX) * 1000) / 1000) + '. At renown N, ' +\n 'every title of rank N or below applies permanently.

' +\n '' +\n '';\n MOD_RENOWN_STEPS.forEach(function (n, i) {\n h += '= n ? 'done' : '') + '\">' +\n '';\n });\n h += '
RenownTitles neededAll statsPassive ranks
' + (i + 1) +\n '' + n + '×' +\n (Math.round(MOD_renownMult(i + 1) * 1000) / 1000) + '1–' + (i + 1) + '
' +\n\n '

Every title

' +\n '

Grouped by the skill that grants it. What is left is either ' +\n 'a ladder of its own — creatures killed, items gathered, the ' +\n 'cultivation realms — or a one-off from the story.

';\n\n var groups = {}, fams = {}, loose = [];\n for (var k in ttl) {\n var t = ttl[k];\n /* Unlike the picker, the wiki KEEPS the author's broken entries and says\n so — same call as area.clg, which is listed and tagged \"nothing spawns\"\n rather than quietly dropped. A reader looking for why a title never\n appears is better served by the row than by its absence. */\n if (!t || t.name === undefined || t.name === null) continue;\n var sk = MOD_TITLE_SKILL[k];\n if (sk && skl[sk]) { (groups[sk] = groups[sk] || []).push(k); continue; }\n var stem = MOD_titleStem(k);\n if (stem) { (fams[stem] = fams[stem] || []).push(k); continue; }\n loose.push(k);\n }\n /* Same rule as the picker: a family of one is not a family. */\n Object.keys(fams).forEach(function (stem) {\n if (fams[stem].length < 2) { loose.push(fams[stem][0]); delete fams[stem]; }\n });\n var order = Object.keys(groups).sort(function (a, b) {\n return skl[a].name < skl[b].name ? -1 : 1;\n });\n\n var row = function (k) {\n var t = ttl[k];\n var st = MOD_rankStyle(t.rar);\n var gen = null;\n for (var i = 0; i < MOD_SKILL_TITLES.length; i++) {\n if (MOD_SKILL_TITLES[i].key === k) { gen = MOD_SKILL_TITLES[i]; break; }\n }\n /* The author left a handful unfinished: ttl.ddcd carries the literal string\n \"null\" for both name and description, and shpt2/shpt3/mone3 are blank.\n None is granted by anything, so none can be earned. */\n var broken = !t.name || t.name === 'null';\n return '
' +\n '' +\n MOD_WIKI.safe(t.name || '(unnamed)') + '' +\n (broken ? 'unfinished' : '') +\n 'rank ' + (t.rar || 1) + '' +\n (gen ? 'lv ' + gen.rung + ' · +' +\n Math.round(gen.xp * 100) + '% ' + MOD_WIKI.safe(skl[gen.skill].name) +\n ' exp' : '') +\n (t.have === true ? 'held' : '') +\n '
' + MOD_wikiDesc(t.desc) + '
';\n };\n\n var byRank = function (a, b) { return (ttl[a].rar || 1) - (ttl[b].rar || 1); };\n order.forEach(function (sk) {\n groups[sk].sort(byRank);\n h += MOD_wikiGroup(MOD_WIKI.safe(skl[sk].bname || skl[sk].name),\n groups[sk].length, groups[sk].map(row).join(''), 3);\n });\n /* The ladders that belong to no skill, by the same derivation the picker\n uses — so the two cannot drift apart. Sorted by the label, since that is\n what the reader scans. */\n var famList = Object.keys(fams)\n .map(function (stem) {\n fams[stem].sort(byRank);\n return { stem: stem,\n label: MOD_titleFamilyLabel(stem, fams[stem].map(function (k) { return ttl[k]; })) };\n })\n .sort(function (a, b) { return a.label < b.label ? -1 : a.label > b.label ? 1 : 0; });\n if (famList.length) {\n /* Nested one level down rather than sitting beside the skills. Eleven more\n top-level summaries cost ~530px on a page that is already the tallest in\n the wiki, and these are a different KIND of thing from a skill anyway. */\n var famInner = '', famTotal = 0;\n famList.forEach(function (f) {\n famTotal += fams[f.stem].length;\n famInner += MOD_wikiGroup(MOD_WIKI.safe(f.label), fams[f.stem].length,\n fams[f.stem].map(row).join(''), 4);\n });\n h += MOD_wikiGroup('Ladders of their own', famTotal, famInner, 3);\n }\n if (loose.length) {\n loose.sort(byRank);\n /* The story titles are the one group with no skill to divide them, and\n there are enough of them to need dividing. Rank rather than the alphabet:\n it is the axis they are already sorted on, and the one a reader cares\n about — a rank is a statement about where in the game a title comes from. */\n var inner;\n if (loose.length <= MOD_WIKI_ALPHA_AT) {\n inner = loose.map(row).join('');\n } else {\n var byRankNo = {};\n loose.forEach(function (k) {\n var r = Math.min(Math.max(Number(ttl[k].rar) || 1, 1), 10);\n (byRankNo[r] = byRankNo[r] || []).push(k);\n });\n inner = '';\n Object.keys(byRankNo).sort(function (a, b) { return a - b; }).forEach(function (r) {\n var st = MOD_rankStyle(r);\n inner += MOD_wikiGroup('Rank ' + r + '', byRankNo[r].length,\n byRankNo[r].map(row).join(''), 4);\n });\n }\n h += MOD_wikiGroup('From the story', loose.length, inner, 3);\n }\n return h;\n});\n\n/* --- 7. areas ------------------------------------------------------------- */\nMOD_wikiPage('areas', 'Areas & enemies', function () {\n var h = '

Areas & enemies

' +\n '

Every area, what lives in it and at what levels, and what it ' +\n 'drops. Enemy strength is not read off these levels — see the note below the ' +\n 'table.

';\n\n /* Two of the game's 21 areas are not places and listing them misleads:\n nwh \"Somewhere\" — where global.current_z parks whenever you are NOT in\n a fight, populated by creature.default (\"Nothing\").\n tst \"Test\" — reached only from chss.tst, which has id -1 and is in no\n sector, so nothing in the game can travel there.\n The test for \"is this a place\" is whether you can get to it, and neither\n can be. Everything else is listed whether or not you have been. */\n var MOD_WIKI_NOT_PLACES = { nwh: 1, tst: 1 };\n\n var keys = [];\n for (var k in area) {\n if (area[k] && area[k].pop && !MOD_WIKI_NOT_PLACES[k]) keys.push(k);\n }\n /* Ordered by the middle of the level band, which is the closest thing the\n game has to a difficulty order. */\n keys.sort(function (a, b) {\n var mid = function (z) {\n if (!z.pop.length) return 0;\n var s = 0; z.pop.forEach(function (e) { s += (e.lvlmin + e.lvlmax) / 2; });\n return s / z.pop.length;\n };\n return mid(area[a]) - mid(area[b]);\n });\n\n keys.forEach(function (k) {\n var z = area[k];\n /* An area whose baked bands are not finite can never spawn anything — see\n the note rendered under its table. Derived rather than listed by name, so\n it stays true if the game changes. */\n var broken = !z.popc || z.popc.length !== z.pop.length ||\n z.popc.some(function (band) { return !band || !isFinite(band[1] - band[0]); });\n h += '

' + MOD_WIKI.safe(z.name) +\n ' ' + k + '' +\n (broken ? 'nothing spawns' : '') +\n '' + (z.size === -1 ? 'endless' : z.size + ' to clear') +\n '

';\n if (z.pop.length) {\n /* The spawn share is NOT pop[i].c. z_bake normalises those weights into\n area.popc — a list of [lo,hi] bands that mon_gen rolls a uniform\n random against — and the weights do not have to sum to 1, so they are\n off wherever they don't. The Southern forest's .35/.45/.25 are really\n 33/43/24%. Read the bands the game actually rolls against. */\n var bands = z.popc || [];\n h += '';\n z.pop.forEach(function (e, i) {\n var c = e.crt || {};\n var band = bands[i];\n var share = band ? band[1] - band[0] : NaN;\n h += '' +\n '' +\n '';\n });\n h += '
' + MOD_WIKI.safe(c.name || '?') + 'lv ' + e.lvlmin +\n (e.lvlmax !== e.lvlmin ? '–' + e.lvlmax : '') + '' + (isFinite(share)\n ? Math.round(share * 100) + '% of spawns'\n : '—') + '
';\n if (broken) {\n h += '
Nothing spawns here. The area\\'s entries carry ' +\n 'no spawn weight, so the table the game rolls against comes out as ' +\n 'NaN and no creature can ever match it. A bug in the base game, left ' +\n 'as it is — nothing in the game travels to this area either.
';\n }\n }\n if (z.drop && z.drop.length) {\n var d = z.drop.map(function (x) {\n return MOD_WIKI.safe((x.item && x.item.name) || '?') +\n ' ' + ((x.c || 0) * 100).toFixed(2) + '%';\n }).join(', ');\n h += '
Drops: ' + d + '
';\n }\n h += '
';\n });\n\n var fightsOrig = MOD_FIGHTS.mode === 'original';\n h += '

How hard an enemy is

' +\n '

It depends on the Fights setting, which is ' +\n (fightsOrig ? 'Original' : 'Scaled') + ' now. On Original (the default) ' +\n 'every area the author wrote is exactly as he wrote it: his creatures, his ' +\n 'level ranges, and you out-grow them as you do in his game. On Scaled every ' +\n 'area is sized to you. The three areas past the golem arena and the rank ' +\n 'duels are sized to you either way.

' +\n '

Sizing to you works like this. Enemies are not fitted to a level. Each spawn is solved against your ' +\n 'actual power at the moment it appears, aiming at roughly ' + MOD_ENEMY.kill +\n ' swings to kill it and ' + MOD_ENEMY.die + ' before it kills you, with a ' +\n 'guaranteed margin between the two so a fight is never a coin flip. Variety ' +\n 'comes from the area\\'s level band and each creature\\'s own stat weighting, ' +\n 'not from difficulty scaling.

' +\n '

The reason for that: within the first stage alone you go from 1 STR to ' +\n 'around 265, so anything anchored to a level is either trivial at the end of ' +\n 'a stage or impossible at the start of one.

';\n return h;\n});\n\n/* --- 8. items -------------------------------------------------------------\n The one place where \"list everything\" needs help: 321 of the game's 371\n plain items are the same inventory type, so grouping by type alone leaves a\n single group of 321. Anything past MOD_WIKI_ALPHA_AT gets an alphabetical\n sub-split as well — the oldest index pattern there is, and derived rather\n than invented, which a hand-made taxonomy would not be.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_ALPHA_AT = 60;\n\n/* The game's own inventory tabs (isort): ALL / WPN / EQP / USE / OTHER. Using\n its categories rather than new ones means a group here is a tab there. */\nvar MOD_WIKI_STYPE = { 2: 'Weapons', 3: 'Equipment', 4: 'Usable', 5: 'Other' };\n\nfunction MOD_wikiItemRow(it) {\n var val = it.v !== undefined ? it.v : (it.val !== undefined ? it.val : null);\n return '
' +\n '

' + MOD_WIKI.safe(it.name) +\n (it.rar > 1 ? 'rarity ' + it.rar + '' : '') +\n (val !== null ? '' + MOD_wikiNum(val) + ' coin' : '') +\n (it.amount > 0 ? '×' + it.amount + '' : '') +\n '

' + MOD_wikiDesc(it.desc) + '
';\n}\n\n/* The letter a name files under. Several items are quoted — the master's\n manuals are literally named \"Sword Saint Manual\" — and filing those under a\n quotation mark is no use to anyone looking for S. */\nfunction MOD_wikiLetter(name) {\n var m = String(name || '').replace(/^[^0-9A-Za-z]+/, '');\n return (m.charAt(0) || '?').toUpperCase();\n}\n\nfunction MOD_wikiByLetter(a, b) {\n var x = MOD_wikiLetter(a.name) + String(a.name || '').toLowerCase();\n var y = MOD_wikiLetter(b.name) + String(b.name || '').toLowerCase();\n return x < y ? -1 : (x > y ? 1 : 0);\n}\n\n/* Flat below the threshold, alphabetically sub-grouped above it. Ranges are cut\n from the entries actually present, so they stay even as content is added. */\nfunction MOD_wikiAlpha(entries, render, level) {\n if (entries.length <= MOD_WIKI_ALPHA_AT) return entries.map(render).join('');\n var per = Math.ceil(entries.length / Math.ceil(entries.length / 40));\n var out = '';\n for (var i = 0; i < entries.length; i += per) {\n var chunk = entries.slice(i, i + per);\n var first = MOD_wikiLetter(chunk[0].name);\n var last = MOD_wikiLetter(chunk[chunk.length - 1].name);\n out += MOD_wikiGroup(first === last ? first : first + '–' + last,\n chunk.length, chunk.map(render).join(''), level || 4);\n }\n return out;\n}\n\nMOD_wikiPage('items', 'Items', function () {\n var h = '

Items

' +\n '

Everything the game defines, whether or not you have seen ' +\n 'it. Grouped the way your inventory groups it. Values are the base sell ' +\n 'price where the game gives one.

';\n\n /* item[] first, split by the inventory tab it appears under. */\n var buckets = {};\n for (var k in item) {\n var it = item[k];\n if (!it || !it.name || it.name === 'dummy') continue;\n var lab = MOD_WIKI_STYPE[it.stype] || 'Uncategorised';\n (buckets[lab] = buckets[lab] || []).push(it);\n }\n ['Usable', 'Other', 'Weapons', 'Equipment', 'Uncategorised'].forEach(function (lab) {\n var list = buckets[lab];\n if (!list || !list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(lab, list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n\n /* and the equipment objects, which live in their own globals */\n [['Weapons', wpn], ['Armour', eqp], ['Shields', sld], ['Accessories', acc]]\n .forEach(function (g) {\n var list = [];\n for (var kk in g[1]) {\n if (g[1][kk] && g[1][kk].name && g[1][kk].name !== 'dummy') list.push(g[1][kk]);\n }\n if (!list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(g[0], list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n return h;\n});\n\n/* --- 9. actions ----------------------------------------------------------- */\nMOD_wikiPage('actions', 'Actions', function () {\n var unlock = {};\n MOD_ACTION_UNLOCKS.forEach(function (u) {\n if (u.act) unlock[u.act.id] = (skl[u.skill] ? skl[u.skill].name : u.skill) +\n ' level ' + u.lv;\n });\n if (typeof act.mod_qi !== 'undefined') {\n unlock[act.mod_qi.id] = 'clearing the dojo\\'s Easiest, Easy and Normal fights';\n }\n\n var h = '

Actions

' +\n '

What you can be doing. One at a time, and only where the ' +\n 'action makes sense — unless \"Unrestricted actions\" is ticked in settings, ' +\n 'which lets several run at once anywhere. Both limits are deliberate; the ' +\n 'switch is there because they are not to everyone\\'s taste.

';\n\n var keys = [];\n for (var k in act) if (act[k] && act[k].name && act[k].name !== 'dummy') keys.push(k);\n keys.sort(function (a, b) { return act[a].name < act[b].name ? -1 : 1; });\n\n keys.forEach(function (k) {\n var a = act[k];\n var how = unlock[a.id];\n h += '
' +\n '

' + MOD_WIKI.safe(a.name) +\n (a.have === true ? 'unlocked'\n : 'locked') +\n (how ? 'from ' + MOD_WIKI.safe(how) + '' : '') +\n '

' + MOD_wikiDesc(a.desc) + '
';\n });\n return h;\n});\n\n/* --- 10. the dojo --------------------------------------------------------- */\nMOD_wikiPage('dojo', 'The dojo', function () {\n var h = '

The dojo

' +\n '

The instructor promises a reward every five levels. The base ' +\n 'game stops delivering at 30; the mod carries the same ladder to 110, on the ' +\n 'same terms.

' +\n '

Only the lowest rung you have not claimed is offered, which is how the ' +\n 'first six behave. Rewards are sized against the character experience curve ' +\n '(4×level3 + level2) — a level at 110 costs 5.3 ' +\n 'million, which is why the base game\\'s best pill is worth 0.3% of one there.

' +\n '' +\n '';\n\n MOD_DOJO_RUNGS.forEach(function (r) {\n var pill = item[r.pill];\n var extra = [];\n if (r.realmPill) extra.push('breakthrough pill for realm ' + r.realmPill);\n if (r.manual) extra.push('a choice of master\\'s manual');\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
LevelRewardCoinAlso
' + r.lv + '' + (pill ? MOD_WIKI.safe(pill.name) + ' ×' + r.n\n : '—') + '' + MOD_wikiNum(r.coin) + '' + (extra.join(', ') || '—') + '' + (global.flags[r.flag] ? 'claimed' : '') +\n '
' +\n\n '

Spirit pills

' +\n '

Character experience in a bottle. The four the mod adds continue the ' +\n 'game\\'s own series up to the new curve.

' +\n '' +\n '';\n MOD_PILLS.forEach(function (p) {\n h += '';\n });\n h += '
PillExperience
' + MOD_WIKI.safe(p[2]) + '' +\n p[3].toLocaleString() + '
' +\n\n '

Master\\'s manuals

' +\n '

A third grade of skillbook, offered at levels 50, 75 and 100 as a choice ' +\n 'the same way the first one was. Each is worth +' +\n Math.round(MOD_MANUAL_RATE * 100) + '% experience in its skill, permanently.

' +\n '' +\n '';\n MOD_MANUALS.forEach(function (m) {\n h += '';\n });\n h += '
ManualSkill
' + MOD_WIKI.safe(m[2]) + '' +\n MOD_WIKI.safe(skl[m[0]] ? (skl[m[0]].bname || skl[m[0]].name) : m[0]) +\n '
';\n return h;\n});\n\n/* --- 11. mechanics -------------------------------------------------------- */\nMOD_wikiPage('mechanics', 'Mechanics', function () {\n return '

Mechanics

' +\n\n '

Damage is subtractive, in both directions

' +\n '

A hit does the attacker\\'s strength minus the defender\\'s strength, plus ' +\n 'the weapon, plus one, before criticals. A defender\\'s STR is their ' +\n 'armour — there is no separate armour stat doing that job.

' +\n '

Two consequences worth carrying around:

' +\n '
    ' +\n '
  • Damage does not taper, it stops. Once a defender\\'s STR passes ' +\n 'yours, your hits do not get small; they land for nothing. Progress against ' +\n 'something too strong is not slow, it is zero.
  • ' +\n '
  • A stat ratio tells you nothing. Comparing your HP to an enemy\\'s ' +\n 'strength ignores the subtraction entirely and reads several times off in ' +\n 'either direction.
  • ' +\n '
' +\n '

This is also the reason enemies are never scaled by raising ' +\n 'their strength: it would raise their offence and their armour together, and ' +\n 'the second effect arrives first.

' +\n\n '

Criticals carry the late game

' +\n '

Critical rate reaches about a third at the top of the ladder and a ' +\n 'critical is roughly eight times a normal swing, so around three quarters of ' +\n 'all damage done at that point is critical damage. Anything that raises ' +\n 'critical rate or critical damage is worth more than it looks; Sharp Eye and ' +\n 'War are the two that do.

' +\n\n '

Hit chance

' +\n '

Landing a hit is a contest of agility. It is a ratio rather than a ' +\n 'subtraction, so agility behaves more gently than strength does — falling ' +\n 'behind on it costs you accuracy, not everything.

' +\n\n '

Skill sections and parents

' +\n '

Each of the ' + Object.keys(MOD_SECTIONS).length + ' sections has a parent ' +\n 'skill. A parent earns ' + Math.round(MOD_PARENT.feed * 100) + '% of everything ' +\n 'its children earn and grants no stats of its own. In return, a child below ' +\n 'the parent\\'s level trains ' + Math.round(MOD_PARENT.perLevel * 100) +\n '% faster per level of lag, up to ' + MOD_PARENT.maxGap + ' levels behind. ' +\n 'Bringing one section up makes every skill in it cheaper to catch up.

' +\n\n '

Saving

' +\n '

Three slots, switched from the bottom bar. The game builds its world once ' +\n 'at startup and cannot unload a save, so switching slots or starting a new ' +\n 'game reloads the page. Exporting a save before running anything unusual is ' +\n 'always the safe move.

';\n});\n\n/* --- the document ---------------------------------------------------------\n One string, no fetches, no dependencies: this has to work from file:// as\n readily as from a server.\n ------------------------------------------------------------------------- */\n\nfunction MOD_wikiCss() {\n return [\n ':root{color-scheme:dark}',\n '*{box-sizing:border-box}',\n /* Menlo and Consolas before the generic: with MS Gothic missing (any Mac)\n \"monospace\" is Courier, which Safari sets wider than Chrome does, and\n table cells wrapped on one and not the other. */\n 'body{margin:0;background:#060a1c;color:#dfe6f5;font:14px/1.6 \"MS Gothic\",' +\n '\"MS Mincho\",Menlo,Consolas,\"DejaVu Sans Mono\",monospace;display:flex;min-height:100vh}',\n 'a{color:#7cb0ff}',\n 'nav{width:210px;flex:0 0 210px;background:#081040;border-right:1px solid #46a;' +\n 'position:sticky;top:0;height:100vh;overflow:auto;padding:14px 0}',\n 'nav h2{font-size:13px;color:#9fb4d8;margin:0 0 10px 14px;letter-spacing:1px}',\n 'nav a{display:block;padding:6px 14px;text-decoration:none;color:#cfe0ff;' +\n 'border-left:3px solid transparent}',\n 'nav a:hover{background:#154080}',\n 'nav a.on{background:#123070;border-left-color:#7cb0ff;color:#fff}',\n 'nav .srch{margin:0 10px 12px;width:calc(100% - 20px);background:#050a20;' +\n 'color:#dfe6f5;border:1px solid #46a;padding:5px;font:inherit}',\n 'nav .xall{display:block;margin:12px 10px 0;color:#8296b8;font-size:12px;' +\n 'cursor:pointer;-webkit-user-select:none;user-select:none}',\n 'nav .xall input{vertical-align:-1px;margin-right:4px}',\n 'details.wk-g{border:1px solid #17224a;background:#0a1130;margin:8px 0}',\n 'details.wk-g>summary{cursor:pointer;padding:7px 12px;list-style:none;' +\n 'background:#0d1740}',\n 'details.wk-g>summary:hover{background:#154080}',\n 'details.wk-g>summary::-webkit-details-marker{display:none}',\n // a caret drawn rather than borrowed, so it is the same in every browser\n 'details.wk-g>summary::before{content:\"\\\\25B8\";color:#7cb0ff;' +\n 'display:inline-block;width:1em;transition:transform .12s}',\n 'details.wk-g[open]>summary::before{transform:rotate(90deg)}',\n 'details.wk-g>summary h2,details.wk-g>summary h3{display:inline;margin:0;' +\n 'font-size:15px;color:#fff}',\n '.wk-gi{padding:2px 12px 10px}',\n 'details.wk-g details.wk-g>summary h4{display:inline;margin:0;font-size:13px;' +\n 'color:#cfe0ff;font-weight:normal}',\n 'details.wk-g details.wk-g>summary{padding:5px 10px;background:#0b1435}',\n 'main{flex:1;min-width:0;padding:22px 30px 80px;max-width:1000px}',\n 'h1{font-size:26px;margin:0 0 6px;color:#fff;border-bottom:1px solid #46a;' +\n 'padding-bottom:8px}',\n 'h2{font-size:19px;color:#bcd4ff;margin:30px 0 8px}',\n 'h3{font-size:15px;color:#fff;margin:0 0 4px}',\n 'p.lede{color:#a8bcdc;margin:0 0 18px}',\n '.dim{color:#8296b8;font-weight:normal}',\n '.flav{font-size:12px;font-style:italic}',\n '.note{background:#0b1638;border-left:3px solid #46a;padding:8px 12px;' +\n 'color:#a8bcdc;margin:14px 0}',\n 'code,pre{background:#0b1638;border:1px solid #24356e;padding:1px 5px;' +\n 'color:#9fd0ff}',\n 'pre{padding:10px;overflow-x:auto}',\n 'dl dt{color:#fff;margin-top:14px}',\n 'dl dd{margin:2px 0 0 0;color:#a8bcdc}',\n 'ul{color:#a8bcdc}',\n 'table.wk-tbl{border-collapse:collapse;width:100%;margin:10px 0;' +\n 'display:block;overflow-x:auto}',\n 'table.wk-tbl th{text-align:left;color:#9fb4d8;font-weight:normal;' +\n 'border-bottom:1px solid #46a;padding:5px 9px;white-space:nowrap}',\n 'table.wk-tbl td{border-bottom:1px solid #17224a;padding:5px 9px;' +\n 'vertical-align:top}',\n 'table.wk-tbl tr.done td{color:#cfe0ff}',\n 'table.wk-tbl tr.here{background:#123070}',\n '.num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}',\n '.wk-e{border:1px solid #17224a;background:#081040;padding:9px 12px;' +\n 'margin:7px 0;border-radius:2px}',\n 'tr.wk-e{border:0;background:none}',\n '.wk-e.done{border-color:#2a4a8c}',\n '.wk-e h3{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}',\n '.lv{margin-left:auto;font-size:12px;color:#8296b8;font-weight:normal}',\n '.lv.capped{color:#ffd24a}',\n '.rank{font-size:12px;color:#8296b8;font-weight:normal}',\n '.desc{color:#a8bcdc;font-size:13px}',\n '.proj{color:#ffd24a;font-size:12px;margin-top:3px}',\n '.tag{font-size:11px;border:1px solid #46a;color:#9fb4d8;padding:0 5px;' +\n 'border-radius:2px;font-weight:normal}',\n '.tag.mod{border-color:#7a4ea8;color:#c07bff}',\n '.tag.done{border-color:#2e7d4f;color:#6fdc9a}',\n '.tag.here{border-color:#ffd24a;color:#ffd24a}',\n '.tag.warn{border-color:#a8562e;color:#ff9a5c}',\n 'table.wk-perks{border-collapse:collapse;margin:6px 0 0;width:100%}',\n 'table.wk-perks td{padding:2px 8px 2px 0;font-size:12px;color:#8296b8;' +\n 'border-bottom:1px solid #101a3c}',\n 'table.wk-perks tr.done td{color:#cfe0ff}',\n 'table.wk-perks td.tick{color:#6fdc9a;width:1em;text-align:right}',\n // fixed columns so the level bands line up between one area and the next\n 'table.wk-pop{table-layout:fixed}',\n 'table.wk-pop td:first-child{width:60%}',\n 'table.wk-pop td:nth-child(2){width:20%}',\n '.wk-title .tname{font-weight:bold}',\n '.wk-now{background:#0b1638;border:1px solid #24356e;padding:10px 14px;' +\n 'margin:0 0 18px;display:inline-block;min-width:320px}',\n '.wk-now h3{color:#9fb4d8;font-weight:normal;font-size:13px;' +\n 'letter-spacing:1px;margin-bottom:6px}',\n '.wk-now td{padding:2px 16px 2px 0}',\n '.wk-now td:last-child{color:#fff;text-align:right}',\n '.pg{display:none}.pg.on{display:block}',\n 'footer{color:#5a6c8c;font-size:12px;border-top:1px solid #17224a;' +\n 'margin-top:40px;padding-top:12px}',\n '@media(max-width:760px){body{display:block}nav{width:auto;height:auto;' +\n 'position:static;flex:none}main{padding:16px}}'\n ].join('\\n');\n}\n\n/* Filtering, page switching and the hash. Small enough to inline, and inlining\n it is the point — the document has to stand on its own. */\nfunction MOD_wikiJs() {\n return [\n 'var pages=[].slice.call(document.querySelectorAll(\".pg\"));',\n 'var links=[].slice.call(document.querySelectorAll(\"nav a[data-p]\"));',\n 'function show(id){',\n ' var found=false;',\n ' pages.forEach(function(p){var on=p.id===\"pg-\"+id;if(on)found=true;',\n ' p.className=\"pg\"+(on?\" on\":\"\")});',\n ' if(!found&&pages.length){pages[0].className=\"pg on\";id=pages[0].id.slice(3)}',\n ' links.forEach(function(a){a.className=a.dataset.p===id?\"on\":\"\"});',\n ' window.scrollTo(0,0);return id;}',\n 'links.forEach(function(a){a.addEventListener(\"click\",function(e){',\n ' e.preventDefault();location.hash=a.dataset.p;show(a.dataset.p)})});',\n 'window.addEventListener(\"hashchange\",function(){show(location.hash.slice(1))});',\n 'show(location.hash.slice(1)||\"start\");',\n // Search filters entries, hides groups left with none, and OPENS the groups\n // that still have some — a match hidden inside a collapsed group reads as\n // no match at all, which is worse than not having search.\n 'var box=document.getElementById(\"srch\");',\n 'var xall=document.getElementById(\"xall\");',\n 'var groups=[].slice.call(document.querySelectorAll(\"details.wk-g\"));',\n 'function filter(){',\n ' var q=box.value.trim().toLowerCase();',\n ' pages.forEach(function(p){',\n ' [].slice.call(p.querySelectorAll(\".wk-e\")).forEach(function(e){',\n ' e.style.display=(!q||e.textContent.toLowerCase().indexOf(q)>=0)?\"\":\"none\"})});',\n // Every page, not just the one on screen: the nav says how many entries\n // match in each, dims the ones with none, and Enter jumps to the first\n // page that has any. A prose page with no entries counts as one if its text\n // contains the query.\n ' links.forEach(function(a){',\n ' var pg=document.getElementById(\"pg-\"+a.dataset.p),n=0;',\n ' if(a.dataset.l===undefined)a.dataset.l=a.textContent;',\n ' if(q&&pg){var es=[].slice.call(pg.querySelectorAll(\".wk-e\"));',\n ' if(es.length)es.forEach(function(e){if(e.textContent.toLowerCase().indexOf(q)>=0)n++});',\n ' else if(pg.textContent.toLowerCase().indexOf(q)>=0)n=1}',\n ' a.dataset.n=n;a.style.opacity=(q&&!n)?\".4\":\"\";',\n ' a.textContent=a.dataset.l+(q&&n?\" (\"+n+\")\":\"\")});',\n ' groups.forEach(function(g){',\n ' var any=[].slice.call(g.querySelectorAll(\".wk-e\")).some(function(e){',\n ' return e.style.display!==\"none\"});',\n ' g.style.display=any?\"\":\"none\";',\n ' g.open=xall.checked||(!!q&&any)}); }',\n 'box.addEventListener(\"input\",filter);',\n 'box.addEventListener(\"keydown\",function(e){',\n ' if(e.key===\"Escape\"){box.value=\"\";filter()}',\n ' if(e.key===\"Enter\"&&box.value.trim()){',\n ' var cur=links.filter(function(a){return a.className===\"on\"})[0];',\n ' if(cur&&+cur.dataset.n)return;',\n ' var hit=links.filter(function(a){return +a.dataset.n>0})[0];',\n ' if(hit){location.hash=hit.dataset.p;show(hit.dataset.p)}}});',\n // Everything is collapsed by default because three of these pages run past\n // 40,000px open. This is the way back to one long document, which is what\n // the browser's own find needs.\n 'xall.addEventListener(\"change\",filter);'\n ].join('\\n');\n}\n\nfunction MOD_wikiBuild() {\n var nav = '', body = '';\n MOD_WIKI_PAGES.forEach(function (pg) {\n nav += '' +\n MOD_WIKI.safe(pg.label) + '';\n var inner;\n /* One page throwing must not take the wiki down with it — the page says so\n and the rest still renders. */\n try { inner = pg.build(); }\n catch (e) {\n inner = '

' + MOD_WIKI.safe(pg.label) + '

This page ' +\n 'could not be built: ' + MOD_WIKI.safe(e.message) + '

';\n console.warn('[mod] wiki page \"' + pg.id + '\" failed: ' + e.message);\n }\n body += '
' + inner + '
';\n });\n\n var stamp = new Date().toLocaleString();\n return '' +\n '' +\n '' + MOD_WIKI.safe(MOD_WIKI.title) + '' +\n '' +\n '' +\n '
' + body +\n '
Generated from the running game at ' + MOD_WIKI.safe(stamp) +\n ' — mod ' + MOD_WIKI.safe(MOD.version) + '. Everything here is read from the ' +\n 'live data, including your own save, so it cannot fall out of step with the ' +\n 'game. The game is truezhangwei\\'s; the mod is not endorsed by the author.' +\n '
' +\n ' inside content closing the block early, so that is the\n one thing scrubbed. */\n safe: function (h) {\n return String(h === undefined || h === null ? '' : h).replace(/<\\/script/gi, '<\\\\/script');\n }\n};\n\n/* Descriptions carry real HTML (coloured spans, the game's separator div), so\n they are passed through rather than escaped — but the separator becomes a\n rule, since a wiki row is not a tooltip. */\nfunction MOD_wikiDesc(d) {\n var s;\n try { s = (typeof d === 'function') ? d() : d; } catch (e) { s = ''; }\n if (!s) return '';\n s = String(s).split('
 
').join('
');\n return MOD_WIKI.safe(s);\n}\n\nfunction MOD_wikiNum(n) {\n if (!isFinite(n)) return '—';\n return Math.round(n).toLocaleString();\n}\n\n/* An effect line at a level the player has not reached yet. The effect\n functions read the skill they are handed, so the level is borrowed and put\n back; anything that throws simply gets no projection. */\nfunction MOD_wikiEffectAt(key, sk, lvl) {\n var eff = MOD_EFFECTS[key];\n if (!eff) return '';\n var was = sk.lvl, out = '';\n try { sk.lvl = lvl; out = eff(sk) || ''; }\n catch (e) { out = ''; }\n finally { sk.lvl = was; }\n return MOD_WIKI.safe(out);\n}\n\n/* --- page builders --------------------------------------------------------\n Each returns the inner HTML of one page. `wk-e` marks a searchable entry and\n `wk-g` a group that hides itself when all its entries are filtered out.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_PAGES = [];\n\nfunction MOD_wikiPage(id, label, build) {\n MOD_WIKI_PAGES.push({ id: id, label: label, build: build });\n}\n\n/* A collapsible group.\n\n Not decoration: with every skill, item and title listed, three of the eleven\n pages ran past 40,000 pixels — a hundred screens of scrolling, which is the\n same as not having the content at all. Collapsed, each of those pages opens\n as an index you can read in one screen and expand where you want.\n\n
rather than a click handler, so it still works with JavaScript off,\n the browser's own find can be made to reach inside it, and the summary is\n keyboard-reachable for free. `level` is the heading level for the summary,\n since a group is sometimes a section of a page and sometimes a subsection. */\nfunction MOD_wikiGroup(heading, count, inner, level) {\n var hn = 'h' + (level || 2);\n return '
<' + hn + '>' + heading +\n (count !== null && count !== undefined\n ? ' (' + count + ')' : '') +\n '
' + inner + '
';\n}\n\n/* --- 1. orientation ------------------------------------------------------- */\nMOD_wikiPage('start', 'Start here', function () {\n var cap = MOD_levelCap();\n var realm = MOD_realm();\n var h = '

proto23

' +\n '

An idle RPG by truezhangwei, plus a mod. You train skills by ' +\n 'doing things, the things you can do open up as the story does, and none of ' +\n 'it is in a hurry.

' +\n\n '

Where you are right now

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Character level' + MOD_wikiNum(you.lvl) + '
Skill level cap' + cap + ' — ' +\n MOD_WIKI.safe(MOD_CAP.name) + '
Cultivation realm' + MOD_WIKI.safe(realm.name) +\n ' (' + realm.n + ' of ' + (MOD_REALMS.length - 1) + ')
Titles held' + MOD_titleCount() + ' — renown ' +\n (skl.rnwn ? skl.rnwn.lvl : 0) + '
Coin' + MOD_wikiNum(you.wealth || 0) + '
' +\n\n '

The four things worth knowing early

' +\n '
' +\n '
Your skill levels are capped by the story, not by grinding.
' +\n '
The cap starts at 10 and ends at 110, and it moves when you reach a new ' +\n 'place — not when you earn enough experience. If a skill has stopped ' +\n 'levelling, you are not doing it wrong; you need to go somewhere. See ' +\n 'Progression.
' +\n\n '
Armour is subtraction, so levels matter more than they look.
' +\n '
A hit does the attacker\\'s STR minus the defender\\'s STR. That means a ' +\n 'small gap in strength is a large gap in damage, and a big enough one takes ' +\n 'your damage to nothing at all rather than merely reducing it. See ' +\n 'Mechanics.
' +\n\n '
Most skills have a parent that pulls them along.
' +\n '
Every section has a parent skill that earns a share of everything its ' +\n 'children do, and any child lagging behind the parent trains faster — up to ' +\n 'twice as fast at ' + MOD_PARENT.maxGap + ' levels behind. A neglected skill is ' +\n 'cheap to bring up later, which is the only reason having this many is ' +\n 'workable.
' +\n\n '
Cultivation is a second ladder, and it is not automatic.
' +\n '
Circulating qi levels a skill; the skill does not raise your realm. ' +\n 'Reaching the level opens a bottleneck, and breaking through it costs a pill ' +\n 'and can fail. See Cultivation.
' +\n '
' +\n\n '

What is the mod and what is the game

' +\n '

The game is index.html and it is entirely truezhangwei\\'s ' +\n 'work. Everything in mod.js is a modification of it and is not ' +\n 'endorsed by the author. Pages here mark added content with ' +\n 'mod where the distinction matters. The full ' +\n 'list of changes is in the changelog, on the bottom bar.

';\n return h;\n});\n\n/* --- 2. progression ------------------------------------------------------- */\nMOD_wikiPage('progress', 'Progression', function () {\n var cap = MOD_levelCap();\n var h = '

Progression

' +\n '

Skill levels are capped by how far the story has gone. Ten ' +\n 'rungs, 10 through 110. The cap applies to every skill at once; Renown is ' +\n 'the single exception, since it is counted rather than trained.

' +\n '' +\n '';\n\n MOD_TIERS.forEach(function (t) {\n var got = null;\n try { got = MOD_tierFlag(t); } catch (e) {}\n var here = (t.cap === cap);\n var need = t.needText ? t.needText\n : (t.flags.length ? t.flags.join(' or ') : 'the start of the game');\n h += '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
CapStageWhat opens it
' + t.cap + '' + MOD_WIKI.safe(t.name) + '' + MOD_WIKI.safe(need) + '' + (here ? 'you are here'\n : got ? 'reached' : '') + '
' +\n\n '

The experience curve

' +\n '

The game\\'s own skill curve is super-exponential — level 109 to 110 ' +\n 'alone costs ' + MOD_wikiNum(MOD_vanillaExpnext(109)) + ' experience, so the top of the ladder ' +\n 'is unreachable by a factor of about ten trillion. But the author only ever ' +\n 'designed skills up to level 15, and below that his curve is fine.

' +\n (MOD_XP.vanillaTo > 0\n ? '

So the mod keeps it. Below level ' + MOD_XP.vanillaTo + ' a skill costs ' +\n 'exactly what the original charges; from there it rises by ×' +\n MOD_XP.ratio.toFixed(4) + ' a level, a ratio solved so the climb to 110 takes ' +\n 'the same time it always did.'\n : '

You have chosen the Mod before 4.2 pacing: a plain geometric curve, ' +\n '×' + MOD_XP.ratio.toFixed(3) + ' a level from level 0, much faster than the ' +\n 'original early on. The Original pacing in settings puts back the ' +\n 'author\\'s own cost below level 10.') +\n ' Character level uses the game\\'s own curve, untouched.

' +\n '' +\n '';\n [1, 5, 9, 10, 15, 25, 50, 75, 90, 109].forEach(function (l) {\n h += '';\n });\n h += '
LevelExperience for the nextOriginal game
' + l +\n '' + MOD_wikiNum(MOD_expnextFor(l)) + '' +\n MOD_wikiNum(MOD_vanillaExpnext(l)) + '
';\n return h;\n});\n\n/* --- 3. skills ------------------------------------------------------------ */\nMOD_wikiPage('skills', 'Skills', function () {\n var cap = MOD_levelCap();\n var bySection = {};\n var keys = [];\n for (var k in skl) {\n var s = skl[k];\n if (!s || typeof s !== 'object' || !s.name) continue;\n keys.push(k);\n }\n keys.forEach(function (k) {\n var s = skl[k];\n var sec = MOD_sectionOf(s);\n (bySection[sec] = bySection[sec] || []).push(k);\n });\n\n var h = '

Skills

' +\n '

' + keys.length + ' skills in ' +\n Object.keys(MOD_SECTIONS).length + ' sections. Every one has perks at ' +\n MOD_LADDER.join(', ') + ', and every one that has milestones grants titles at ' +\n MOD_TITLE_RUNGS.join(', ') + '. Levels and ticks below are yours.

';\n\n Object.keys(MOD_SECTIONS).forEach(function (sec) {\n var list = bySection[sec];\n if (!list || !list.length) return;\n var parentKey = MOD_PARENT_KEY[sec];\n var g = '';\n if (parentKey && skl[parentKey]) {\n g += '

Parent skill: ' +\n MOD_WIKI.safe(skl[parentKey].bname || skl[parentKey].name) +\n ' — earns ' + Math.round(MOD_PARENT.feed * 100) +\n '% of what its children earn, and speeds up any child that lags behind it.

';\n }\n list.sort(function (a, b) { return skl[a].name < skl[b].name ? -1 : 1; });\n list.forEach(function (k) {\n var s = skl[k];\n var lvl = Number(s.lvl) || 0;\n g += '
' +\n '

' + MOD_WIKI.safe(s.bname || s.name) +\n (s.bname ? ' (' + MOD_WIKI.safe(s.name) + ')' : '') +\n '= cap ? ' capped' : '') + '\">lv ' + lvl +\n (lvl >= cap ? ' — at the cap' : '') + '

' +\n '
' + MOD_wikiDesc(s.desc) + '
';\n\n var at110 = MOD_wikiEffectAt(k, s, 110);\n if (at110) g += '
At level 110: ' + at110 + '
';\n\n if (s.mlstn && s.mlstn.length) {\n g += '';\n s.mlstn.forEach(function (m) {\n g += '' +\n '' +\n '' +\n '';\n });\n g += '
' + m.lv + '' + MOD_WIKI.safe(m.p || '—') + '' + (m.g === true ? '✓' : '') + '
';\n }\n g += '
';\n });\n h += MOD_wikiGroup(MOD_WIKI.safe(MOD_SECTIONS[sec]), list.length, g, 2);\n });\n return h;\n});\n\n/* --- 4. cultivation ------------------------------------------------------- */\nMOD_wikiPage('cultivation', 'Cultivation', function () {\n var now = MOD_realm();\n var elig = 0;\n try { elig = MOD_realmEligible().n; } catch (e) {}\n var qic = skl.qic ? skl.qic.lvl : 0;\n\n var h = '

Cultivation

' +\n '

Ten realms above Mortal. Circulating qi levels the Qi ' +\n 'Circulation skill; the skill does not advance the realm. Reaching the level ' +\n 'opens a bottleneck, and breaking through it costs a pill and can fail — the ' +\n 'pill is spent either way.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Realm' + MOD_WIKI.safe(MOD_realmTitle()) + '
Spiritual Root' + MOD_WIKI.safe(MOD_root().name) + '
Qi Circulationlevel ' + qic + '
Insight' + MOD_insight() + '
Bottleneck open' +\n (elig > now.n ? 'yes — ' + MOD_WIKI.safe(MOD_REALMS[now.n + 1].name) +\n ' is within reach' : 'no') + '
Qi Deviation' + (MOD_deviated()\n ? MOD_deviationLeft() + ' in-game minutes left' : 'no') + '
' +\n '

Realm thresholds are the same levels that earn a title rank, so realm N ' +\n 'is the level at which a rank N title arrives. Every realm multiplies all ' +\n 'stats and health, and the multipliers do not stack with each other — the ' +\n 'realm you are in is the one that applies.

' +\n '' +\n '';\n\n MOD_REALMS.forEach(function (r) {\n var pill = r.n >= 1 ? item['mod_bp' + r.n] : null;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmNameQi Circ.StatsHealthPill
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + r.qic + '×' + r.mult + '×' + r.hp + '' + (pill ? MOD_WIKI.safe(pill.name) : '—') +\n '
' +\n\n '

Where the pills come from

' +\n '

Every breakthrough pill has a source in normal play. None of them are ' +\n 'craftable, and none are cheap — a bottleneck you can buy in bulk is not a ' +\n 'bottleneck.

' +\n '' +\n '' +\n '' +\n '' +\n '
RealmsSource
1The dojo instructor, when qi ' +\n 'circulation is first taught
2–5The Herbalist in the village ' +\n '(vendor.pha1), restocked
6–10The dojo\\'s Level Advancement ' +\n 'ladder, and the Pill Tower
' +\n '

Realm 1 is deliberately not sold at the marketplace: ' +\n 'the marketplace is behind a 40%-a-visit chance encounter, and realm 1 opens ' +\n 'long before that is reliable.

' +\n\n '

Breaking through

' +\n '

A bottleneck wants three things, which is what the genre says it wants: ' +\n 'a pill, insight, and enough consolidation to make the ' +\n 'attempt worth making. The pill and the insight are both spent whether the ' +\n 'attempt works or not.

' +\n '

The odds improve the further past the threshold you are, plus your root, ' +\n 'plus time sat in Closed Door Training, and cap short of certain.

' +\n '' +\n '';\n [0, 2, 4, 6, 8].forEach(function (o) {\n var base = Math.min(0.55 + o * 0.05 + MOD_root().odds, 0.95);\n h += '';\n });\n h += '
Levels past the thresholdChanceAfter full seclusion
+' + o + '' +\n Math.round(base * 100) + '%' +\n Math.round(Math.min(base + MOD_CULT.secludeOdds, 0.95) * 100) + '%
' +\n\n '

Insight

' +\n '

Insight is the half of the price that cannot be bought. It comes from ' +\n 'three places, and they are the three the genre names: meditating, fights you ' +\n 'nearly lost, and going somewhere you have not been.

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
SourceRate
Circulate Qi, at a wall' +\n (MOD_CULT.meditateChance * 100).toFixed(1) + '% a second
Closed Door Training' +\n (MOD_CULT.secludeChance * 100).toFixed(1) + '% a second
A kill made under ' +\n Math.round(MOD_CULT.brinkFrac * 100) + '% health — ' +\n (MOD_FIGHTS.mode === 'original'\n ? 'the author\\'s areas rarely come close once you reach them; the three past ' +\n 'the golem arena and the rank duels do, within a few fights, as does an area ' +\n 'you walk into early'\n : 'every fight costs a share of your health, so a few in a row without resting') +\n '' +\n Math.round(MOD_CULT.brinkChance * 100) + '% a kill
Entering an area for the first timealways
' +\n '

The insight a realm asks for is sized to the climb to it: about ' +\n Math.round(MOD_CULT.wallShare * 100) + '% of the Circulate Qi time the climb took, ' +\n 'spent at the wall. Seclusion earns it three times as fast, and full ' +\n 'consolidation takes as long as that on average.

' +\n '' +\n '' +\n '';\n var wkHours = function (sec) {\n return sec >= 7200 ? (sec / 3600).toFixed(sec >= 36000 ? 0 : 1) + ' h' : Math.round(sec / 60) + ' min';\n };\n MOD_REALMS.forEach(function (r) {\n if (!r.n) return;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmClimb (Circulate Qi, 1x)Insight to enterFull seclusionQi Deviation (in-game)
' + MOD_WIKI.safe(r.name) + '' + wkHours(MOD_realmClimbSec(r.n)) + '' + MOD_insightNeed(r.n) + '' + wkHours(MOD_secludeFull(r.n)) + '' + (MOD_devMinutes(r.n) / 1440).toFixed(1) + ' days
' +\n\n '

The nine layers

' +\n '

Each realm is nine layers deep, derived from where Qi Circulation sits ' +\n 'between this realm\\'s threshold and the next one\\'s — so breaking through ' +\n 'puts you at the first layer of the new realm on its own. Layers are worth ' +\n '×' + MOD_CULT.layerMult + ' stats and ×' + MOD_CULT.layerBody +\n ' body each above the first, and the top realm has no rung above it to ' +\n 'measure against, so it stays at its first layer.

' +\n '' +\n '';\n for (var li = 1; li <= MOD_CULT.layers; li++) {\n h += '';\n }\n h += '
LayerCalled
' +\n li + '' + MOD_LAYER_ORD[li] + ' layer, ' +\n MOD_WIKI.safe(MOD_layerStage(li)) + '
' +\n '

At the threshold of the next realm and before breaking ' +\n 'through, you are a half step to it — the genre\\'s own name for the ' +\n 'state this mod calls a bottleneck.

' +\n\n '

Closed Door Training

' +\n '

Seclusion does two things. It consolidates — a full sitting ' +\n '(the table above) is worth +' + Math.round(MOD_CULT.secludeOdds * 100) +\n '% on the attempt — and it shelters: a breakthrough attempted while ' +\n 'you are still sitting in it cannot end in Qi Deviation, only in a lost ' +\n 'pill. It will not start unless you are actually at a wall.

' +\n\n '

Qi Deviation

' +\n '

Failing a breakthrough out in the open leaves your cultivation base ' +\n 'unstable for as long as the seclusion you skipped (the table above, never ' +\n 'under ' + MOD_CULT.devMinutes + ' in-game minutes): stats at ×' +\n MOD_CULT.devStat + ', body at ×' + MOD_CULT.devBody + ', and no second ' +\n 'attempt until it passes. It clears on its own, or at once with a ' +\n 'Qi Settling Pill. It never kills — the death system stays out of it.

' +\n\n '

Heavenly Tribulation

' +\n '

From realm ' + MOD_CULT.tribFrom + ' up, a successful breakthrough is ' +\n 'answered. The cost is a fraction of your maximum health, so it is a gate on ' +\n 'walking in healthy rather than a damage race; the realm bonus you are ' +\n 'standing on is what pays it. Fall to it and the realm does not open.

' +\n '' +\n '';\n MOD_REALMS.forEach(function (r) {\n if (!r.n || r.n < MOD_CULT.tribFrom) return;\n h += '' +\n '' +\n '';\n });\n h += '
RealmBoltsOf your max HP
' + MOD_WIKI.safe(r.name) + '' + MOD_tribulationBolts(r.n) + '' + Math.round(MOD_tribulationCost(r.n) * 100) + '%
' +\n\n '

Spiritual Root

' +\n '

Rolled once, when the dojo finishes teaching you to fight. It is never a ' +\n 'dead end: the worst grade costs cultivation speed, not access, and a ' +\n 'Root Cleansing Pill moves you up a grade.

' +\n '' +\n '';\n var rootTotal = 0;\n MOD_ROOTS.forEach(function (r) { rootTotal += r.weight; });\n MOD_ROOTS.forEach(function (r) {\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
GradeRootChanceCultivationBreakthrough
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + Math.round(r.weight / rootTotal * 100) + '%×' + r.xp + '' + (r.odds >= 0 ? '+' : '') +\n Math.round(r.odds * 100) + '%
';\n return h;\n});\n\n/* --- 5. elemental mastery ------------------------------------------------- */\nMOD_wikiPage('mastery', 'Elemental mastery', function () {\n var h = '

Elemental mastery mod

' +\n '

Six masteries, each with a technique that fires on its own ' +\n 'during a fight. Combat is automatic and there is no ability picker, so a ' +\n 'technique is a chance on each of your swings to become something else — one ' +\n 'that scales with INT and the element rather than with your weapon.

' +\n '

Nothing fires while you are still Mortal: the channels have to be open ' +\n 'first. Chance is ' + (MOD_TECH.perLevel * 100).toFixed(1) + '% per mastery ' +\n 'level plus ' + (MOD_TECH.perRealm * 100).toFixed(1) + '% per realm, capped at ' +\n Math.round(MOD_TECH.cap * 100) + '%; a technique lands for ×' +\n MOD_TECH.power + ' of a normal strike.

' +\n '' +\n '';\n\n var realm = 0;\n try { realm = MOD_realm().n; } catch (e) {}\n MOD_MASTERY.forEach(function (m) {\n var lvl = Number(m.skill.lvl) || 0;\n var ch = realm < 1 ? 0\n : Math.min(lvl * MOD_TECH.perLevel + realm * MOD_TECH.perRealm, MOD_TECH.cap);\n h += '' +\n '' +\n '' +\n '';\n });\n h += '
MasteryTechniqueLevelChance now
' +\n MOD_WIKI.safe(m.skill.name) + '' + MOD_WIKI.safe(m.name) + '' + lvl + '' + (realm < 1 ? 'Mortal'\n : (ch * 100).toFixed(1) + '%') + '
';\n return h;\n});\n\n/* --- 6. titles ------------------------------------------------------------ */\nMOD_wikiPage('titles', 'Titles', function () {\n var perm = MOD_permanentRank();\n var held = MOD_titleCount();\n var total = 0; for (var kk in ttl) if (ttl[kk] && ttl[kk].name) total++;\n\n var h = '

Titles

' +\n '

A title\\'s rank runs 1 to 10 and is derived from the level ' +\n 'that earns it, so a rank is a statement about where in the game you are. ' +\n 'Only the title you are wearing applies — until renown makes the lower ranks ' +\n 'permanent.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '
Titles held' + held + ' of ' + total + '
Renown level' + (skl.rnwn ? skl.rnwn.lvl : 0) +\n ' of ' + MOD_RENOWN_MAX + '
Ranks that apply without wearing' +\n (perm >= 1 ? '1–' + perm : 'none yet') + '
' +\n\n '

Ranks

' +\n '';\n MOD_RANK_AT.forEach(function (lv, i) {\n var st = MOD_rankStyle(i + 1);\n h += '' +\n '';\n });\n h += '
RankEarned from levelColour
' + (i + 1) + '' + lv + '' + '●●●' + '
' +\n\n '

Renown

' +\n '

Renown is not trained. It counts the titles you hold, and each level ' +\n 'multiplies every stat by ' + (1 + MOD_RENOWN_RATE) + ' — compounding, so ' +\n 'level ' + MOD_RENOWN_MAX + ' is ×' +\n (Math.round(MOD_renownMult(MOD_RENOWN_MAX) * 1000) / 1000) + '. At renown N, ' +\n 'every title of rank N or below applies permanently.

' +\n '' +\n '';\n MOD_RENOWN_STEPS.forEach(function (n, i) {\n h += '= n ? 'done' : '') + '\">' +\n '';\n });\n h += '
RenownTitles neededAll statsPassive ranks
' + (i + 1) +\n '' + n + '×' +\n (Math.round(MOD_renownMult(i + 1) * 1000) / 1000) + '1–' + (i + 1) + '
' +\n\n '

Every title

' +\n '

Grouped by the skill that grants it. What is left is either ' +\n 'a ladder of its own — creatures killed, items gathered, the ' +\n 'cultivation realms — or a one-off from the story.

';\n\n var groups = {}, fams = {}, loose = [];\n for (var k in ttl) {\n var t = ttl[k];\n /* Unlike the picker, the wiki KEEPS the author's broken entries and says\n so — same call as area.clg, which is listed and tagged \"nothing spawns\"\n rather than quietly dropped. A reader looking for why a title never\n appears is better served by the row than by its absence. */\n if (!t || t.name === undefined || t.name === null) continue;\n var sk = MOD_TITLE_SKILL[k];\n if (sk && skl[sk]) { (groups[sk] = groups[sk] || []).push(k); continue; }\n var stem = MOD_titleStem(k);\n if (stem) { (fams[stem] = fams[stem] || []).push(k); continue; }\n loose.push(k);\n }\n /* Same rule as the picker: a family of one is not a family. */\n Object.keys(fams).forEach(function (stem) {\n if (fams[stem].length < 2) { loose.push(fams[stem][0]); delete fams[stem]; }\n });\n var order = Object.keys(groups).sort(function (a, b) {\n return skl[a].name < skl[b].name ? -1 : 1;\n });\n\n var row = function (k) {\n var t = ttl[k];\n var st = MOD_rankStyle(t.rar);\n var gen = null;\n for (var i = 0; i < MOD_SKILL_TITLES.length; i++) {\n if (MOD_SKILL_TITLES[i].key === k) { gen = MOD_SKILL_TITLES[i]; break; }\n }\n /* The author left a handful unfinished: ttl.ddcd carries the literal string\n \"null\" for both name and description, and shpt2/shpt3/mone3 are blank.\n None is granted by anything, so none can be earned. */\n var broken = !t.name || t.name === 'null';\n return '
' +\n '' +\n MOD_WIKI.safe(t.name || '(unnamed)') + '' +\n (broken ? 'unfinished' : '') +\n 'rank ' + (t.rar || 1) + '' +\n (gen ? 'lv ' + gen.rung + ' · +' +\n Math.round(gen.xp * 100) + '% ' + MOD_WIKI.safe(skl[gen.skill].name) +\n ' exp' : '') +\n (t.have === true ? 'held' : '') +\n '
' + MOD_wikiDesc(t.desc) + '
';\n };\n\n var byRank = function (a, b) { return (ttl[a].rar || 1) - (ttl[b].rar || 1); };\n order.forEach(function (sk) {\n groups[sk].sort(byRank);\n h += MOD_wikiGroup(MOD_WIKI.safe(skl[sk].bname || skl[sk].name),\n groups[sk].length, groups[sk].map(row).join(''), 3);\n });\n /* The ladders that belong to no skill, by the same derivation the picker\n uses — so the two cannot drift apart. Sorted by the label, since that is\n what the reader scans. */\n var famList = Object.keys(fams)\n .map(function (stem) {\n fams[stem].sort(byRank);\n return { stem: stem,\n label: MOD_titleFamilyLabel(stem, fams[stem].map(function (k) { return ttl[k]; })) };\n })\n .sort(function (a, b) { return a.label < b.label ? -1 : a.label > b.label ? 1 : 0; });\n if (famList.length) {\n /* Nested one level down rather than sitting beside the skills. Eleven more\n top-level summaries cost ~530px on a page that is already the tallest in\n the wiki, and these are a different KIND of thing from a skill anyway. */\n var famInner = '', famTotal = 0;\n famList.forEach(function (f) {\n famTotal += fams[f.stem].length;\n famInner += MOD_wikiGroup(MOD_WIKI.safe(f.label), fams[f.stem].length,\n fams[f.stem].map(row).join(''), 4);\n });\n h += MOD_wikiGroup('Ladders of their own', famTotal, famInner, 3);\n }\n if (loose.length) {\n loose.sort(byRank);\n /* The story titles are the one group with no skill to divide them, and\n there are enough of them to need dividing. Rank rather than the alphabet:\n it is the axis they are already sorted on, and the one a reader cares\n about — a rank is a statement about where in the game a title comes from. */\n var inner;\n if (loose.length <= MOD_WIKI_ALPHA_AT) {\n inner = loose.map(row).join('');\n } else {\n var byRankNo = {};\n loose.forEach(function (k) {\n var r = Math.min(Math.max(Number(ttl[k].rar) || 1, 1), 10);\n (byRankNo[r] = byRankNo[r] || []).push(k);\n });\n inner = '';\n Object.keys(byRankNo).sort(function (a, b) { return a - b; }).forEach(function (r) {\n var st = MOD_rankStyle(r);\n inner += MOD_wikiGroup('Rank ' + r + '', byRankNo[r].length,\n byRankNo[r].map(row).join(''), 4);\n });\n }\n h += MOD_wikiGroup('From the story', loose.length, inner, 3);\n }\n return h;\n});\n\n/* --- 7. areas ------------------------------------------------------------- */\nMOD_wikiPage('areas', 'Areas & enemies', function () {\n var h = '

Areas & enemies

' +\n '

Every area, what lives in it and at what levels, and what it ' +\n 'drops. Enemy strength is not read off these levels — see the note below the ' +\n 'table.

';\n\n /* Two of the game's 21 areas are not places and listing them misleads:\n nwh \"Somewhere\" — where global.current_z parks whenever you are NOT in\n a fight, populated by creature.default (\"Nothing\").\n tst \"Test\" — reached only from chss.tst, which has id -1 and is in no\n sector, so nothing in the game can travel there.\n The test for \"is this a place\" is whether you can get to it, and neither\n can be. Everything else is listed whether or not you have been. */\n var MOD_WIKI_NOT_PLACES = { nwh: 1, tst: 1 };\n\n var keys = [];\n for (var k in area) {\n if (area[k] && area[k].pop && !MOD_WIKI_NOT_PLACES[k]) keys.push(k);\n }\n /* Ordered by the middle of the level band, which is the closest thing the\n game has to a difficulty order. */\n keys.sort(function (a, b) {\n var mid = function (z) {\n if (!z.pop.length) return 0;\n var s = 0; z.pop.forEach(function (e) { s += (e.lvlmin + e.lvlmax) / 2; });\n return s / z.pop.length;\n };\n return mid(area[a]) - mid(area[b]);\n });\n\n keys.forEach(function (k) {\n var z = area[k];\n /* An area whose baked bands are not finite can never spawn anything — see\n the note rendered under its table. Derived rather than listed by name, so\n it stays true if the game changes. */\n var broken = !z.popc || z.popc.length !== z.pop.length ||\n z.popc.some(function (band) { return !band || !isFinite(band[1] - band[0]); });\n h += '

' + MOD_WIKI.safe(z.name) +\n ' ' + k + '' +\n (broken ? 'nothing spawns' : '') +\n '' + (z.size === -1 ? 'endless' : z.size + ' to clear') +\n '

';\n if (z.pop.length) {\n /* The spawn share is NOT pop[i].c. z_bake normalises those weights into\n area.popc — a list of [lo,hi] bands that mon_gen rolls a uniform\n random against — and the weights do not have to sum to 1, so they are\n off wherever they don't. The Southern forest's .35/.45/.25 are really\n 33/43/24%. Read the bands the game actually rolls against. */\n var bands = z.popc || [];\n h += '';\n z.pop.forEach(function (e, i) {\n var c = e.crt || {};\n var band = bands[i];\n var share = band ? band[1] - band[0] : NaN;\n h += '' +\n '' +\n '';\n });\n h += '
' + MOD_WIKI.safe(c.name || '?') + 'lv ' + e.lvlmin +\n (e.lvlmax !== e.lvlmin ? '–' + e.lvlmax : '') + '' + (isFinite(share)\n ? Math.round(share * 100) + '% of spawns'\n : '—') + '
';\n if (broken) {\n h += '
Nothing spawns here. The area\\'s entries carry ' +\n 'no spawn weight, so the table the game rolls against comes out as ' +\n 'NaN and no creature can ever match it. A bug in the base game, left ' +\n 'as it is — nothing in the game travels to this area either.
';\n }\n }\n if (z.drop && z.drop.length) {\n var d = z.drop.map(function (x) {\n return MOD_WIKI.safe((x.item && x.item.name) || '?') +\n ' ' + ((x.c || 0) * 100).toFixed(2) + '%';\n }).join(', ');\n h += '
Drops: ' + d + '
';\n }\n h += '
';\n });\n\n var fightsOrig = MOD_FIGHTS.mode === 'original';\n h += '

How hard an enemy is

' +\n '

It depends on the Fights setting, which is ' +\n (fightsOrig ? 'Original' : 'Scaled') + ' now. On Original (the default) ' +\n 'every area the author wrote is exactly as he wrote it: his creatures, his ' +\n 'level ranges, and you out-grow them as you do in his game. On Scaled every ' +\n 'area is sized to you. The three areas past the golem arena and the rank ' +\n 'duels are sized to you either way.

' +\n '

Sizing to you works like this. Enemies are not fitted to a level. Each spawn is solved against your ' +\n 'actual power at the moment it appears, aiming at roughly ' + MOD_ENEMY.kill +\n ' swings to kill it and ' + MOD_ENEMY.die + ' before it kills you, with a ' +\n 'guaranteed margin between the two so a fight is never a coin flip. Variety ' +\n 'comes from the area\\'s level band and each creature\\'s own stat weighting, ' +\n 'not from difficulty scaling.

' +\n '

The reason for that: within the first stage alone you go from 1 STR to ' +\n 'around 265, so anything anchored to a level is either trivial at the end of ' +\n 'a stage or impossible at the start of one.

';\n return h;\n});\n\n/* --- 8. items -------------------------------------------------------------\n The one place where \"list everything\" needs help: 321 of the game's 371\n plain items are the same inventory type, so grouping by type alone leaves a\n single group of 321. Anything past MOD_WIKI_ALPHA_AT gets an alphabetical\n sub-split as well — the oldest index pattern there is, and derived rather\n than invented, which a hand-made taxonomy would not be.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_ALPHA_AT = 60;\n\n/* The game's own inventory tabs (isort): ALL / WPN / EQP / USE / OTHER. Using\n its categories rather than new ones means a group here is a tab there. */\nvar MOD_WIKI_STYPE = { 2: 'Weapons', 3: 'Equipment', 4: 'Usable', 5: 'Other' };\n\nfunction MOD_wikiItemRow(it) {\n var val = it.v !== undefined ? it.v : (it.val !== undefined ? it.val : null);\n return '
' +\n '

' + MOD_WIKI.safe(it.name) +\n (it.rar > 1 ? 'rarity ' + it.rar + '' : '') +\n (val !== null ? '' + MOD_wikiNum(val) + ' coin' : '') +\n (it.amount > 0 ? '×' + it.amount + '' : '') +\n '

' + MOD_wikiDesc(it.desc) + '
';\n}\n\n/* The letter a name files under. Several items are quoted — the master's\n manuals are literally named \"Sword Saint Manual\" — and filing those under a\n quotation mark is no use to anyone looking for S. */\nfunction MOD_wikiLetter(name) {\n var m = String(name || '').replace(/^[^0-9A-Za-z]+/, '');\n return (m.charAt(0) || '?').toUpperCase();\n}\n\nfunction MOD_wikiByLetter(a, b) {\n var x = MOD_wikiLetter(a.name) + String(a.name || '').toLowerCase();\n var y = MOD_wikiLetter(b.name) + String(b.name || '').toLowerCase();\n return x < y ? -1 : (x > y ? 1 : 0);\n}\n\n/* Flat below the threshold, alphabetically sub-grouped above it. Ranges are cut\n from the entries actually present, so they stay even as content is added. */\nfunction MOD_wikiAlpha(entries, render, level) {\n if (entries.length <= MOD_WIKI_ALPHA_AT) return entries.map(render).join('');\n var per = Math.ceil(entries.length / Math.ceil(entries.length / 40));\n var out = '';\n for (var i = 0; i < entries.length; i += per) {\n var chunk = entries.slice(i, i + per);\n var first = MOD_wikiLetter(chunk[0].name);\n var last = MOD_wikiLetter(chunk[chunk.length - 1].name);\n out += MOD_wikiGroup(first === last ? first : first + '–' + last,\n chunk.length, chunk.map(render).join(''), level || 4);\n }\n return out;\n}\n\nMOD_wikiPage('items', 'Items', function () {\n var h = '

Items

' +\n '

Everything the game defines, whether or not you have seen ' +\n 'it. Grouped the way your inventory groups it. Values are the base sell ' +\n 'price where the game gives one.

';\n\n /* item[] first, split by the inventory tab it appears under. */\n var buckets = {};\n for (var k in item) {\n var it = item[k];\n if (!it || !it.name || it.name === 'dummy') continue;\n var lab = MOD_WIKI_STYPE[it.stype] || 'Uncategorised';\n (buckets[lab] = buckets[lab] || []).push(it);\n }\n ['Usable', 'Other', 'Weapons', 'Equipment', 'Uncategorised'].forEach(function (lab) {\n var list = buckets[lab];\n if (!list || !list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(lab, list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n\n /* and the equipment objects, which live in their own globals */\n [['Weapons', wpn], ['Armour', eqp], ['Shields', sld], ['Accessories', acc]]\n .forEach(function (g) {\n var list = [];\n for (var kk in g[1]) {\n if (g[1][kk] && g[1][kk].name && g[1][kk].name !== 'dummy') list.push(g[1][kk]);\n }\n if (!list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(g[0], list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n return h;\n});\n\n/* --- 9. actions ----------------------------------------------------------- */\nMOD_wikiPage('actions', 'Actions', function () {\n var unlock = {};\n MOD_ACTION_UNLOCKS.forEach(function (u) {\n if (u.act) unlock[u.act.id] = (skl[u.skill] ? skl[u.skill].name : u.skill) +\n ' level ' + u.lv;\n });\n if (typeof act.mod_qi !== 'undefined') {\n unlock[act.mod_qi.id] = 'clearing the dojo\\'s Easiest, Easy and Normal fights';\n }\n\n var h = '

Actions

' +\n '

What you can be doing. One at a time, and only where the ' +\n 'action makes sense — unless \"Unrestricted actions\" is ticked in settings, ' +\n 'which lets several run at once anywhere. Both limits are deliberate; the ' +\n 'switch is there because they are not to everyone\\'s taste.

';\n\n var keys = [];\n for (var k in act) if (act[k] && act[k].name && act[k].name !== 'dummy') keys.push(k);\n keys.sort(function (a, b) { return act[a].name < act[b].name ? -1 : 1; });\n\n keys.forEach(function (k) {\n var a = act[k];\n var how = unlock[a.id];\n h += '
' +\n '

' + MOD_WIKI.safe(a.name) +\n (a.have === true ? 'unlocked'\n : 'locked') +\n (how ? 'from ' + MOD_WIKI.safe(how) + '' : '') +\n '

' + MOD_wikiDesc(a.desc) + '
';\n });\n return h;\n});\n\n/* --- 10. the dojo --------------------------------------------------------- */\nMOD_wikiPage('dojo', 'The dojo', function () {\n var h = '

The dojo

' +\n '

The instructor promises a reward every five levels. The base ' +\n 'game stops delivering at 30; the mod carries the same ladder to 110, on the ' +\n 'same terms.

' +\n '

Only the lowest rung you have not claimed is offered, which is how the ' +\n 'first six behave. Rewards are sized against the character experience curve ' +\n '(4×level3 + level2) — a level at 110 costs 5.3 ' +\n 'million, which is why the base game\\'s best pill is worth 0.3% of one there.

' +\n '' +\n '';\n\n MOD_DOJO_RUNGS.forEach(function (r) {\n var pill = item[r.pill];\n var extra = [];\n if (r.realmPill) extra.push('breakthrough pill for realm ' + r.realmPill);\n if (r.manual) extra.push('a choice of master\\'s manual');\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
LevelRewardCoinAlso
' + r.lv + '' + (pill ? MOD_WIKI.safe(pill.name) + ' ×' + r.n\n : '—') + '' + MOD_wikiNum(r.coin) + '' + (extra.join(', ') || '—') + '' + (global.flags[r.flag] ? 'claimed' : '') +\n '
' +\n\n '

Spirit pills

' +\n '

Character experience in a bottle. The four the mod adds continue the ' +\n 'game\\'s own series up to the new curve.

' +\n '' +\n '';\n MOD_PILLS.forEach(function (p) {\n h += '';\n });\n h += '
PillExperience
' + MOD_WIKI.safe(p[2]) + '' +\n p[3].toLocaleString() + '
' +\n\n '

Master\\'s manuals

' +\n '

A third grade of skillbook, offered at levels 50, 75 and 100 as a choice ' +\n 'the same way the first one was. Each is worth +' +\n Math.round(MOD_MANUAL_RATE * 100) + '% experience in its skill, permanently.

' +\n '' +\n '';\n MOD_MANUALS.forEach(function (m) {\n h += '';\n });\n h += '
ManualSkill
' + MOD_WIKI.safe(m[2]) + '' +\n MOD_WIKI.safe(skl[m[0]] ? (skl[m[0]].bname || skl[m[0]].name) : m[0]) +\n '
';\n return h;\n});\n\n/* --- 11. mechanics -------------------------------------------------------- */\nMOD_wikiPage('mechanics', 'Mechanics', function () {\n return '

Mechanics

' +\n\n '

Damage is subtractive, in both directions

' +\n '

A hit does the attacker\\'s strength minus the defender\\'s strength, plus ' +\n 'the weapon, plus one, before criticals. A defender\\'s STR is their ' +\n 'armour — there is no separate armour stat doing that job.

' +\n '

Two consequences worth carrying around:

' +\n '
    ' +\n '
  • Damage does not taper, it stops. Once a defender\\'s STR passes ' +\n 'yours, your hits do not get small; they land for nothing. Progress against ' +\n 'something too strong is not slow, it is zero.
  • ' +\n '
  • A stat ratio tells you nothing. Comparing your HP to an enemy\\'s ' +\n 'strength ignores the subtraction entirely and reads several times off in ' +\n 'either direction.
  • ' +\n '
' +\n '

This is also the reason enemies are never scaled by raising ' +\n 'their strength: it would raise their offence and their armour together, and ' +\n 'the second effect arrives first.

' +\n\n '

Criticals carry the late game

' +\n '

Critical rate reaches about a third at the top of the ladder and a ' +\n 'critical is roughly eight times a normal swing, so around three quarters of ' +\n 'all damage done at that point is critical damage. Anything that raises ' +\n 'critical rate or critical damage is worth more than it looks; Sharp Eye and ' +\n 'War are the two that do.

' +\n\n '

Hit chance

' +\n '

Landing a hit is a contest of agility. It is a ratio rather than a ' +\n 'subtraction, so agility behaves more gently than strength does — falling ' +\n 'behind on it costs you accuracy, not everything.

' +\n\n '

Skill sections and parents

' +\n '

Each of the ' + Object.keys(MOD_SECTIONS).length + ' sections has a parent ' +\n 'skill. A parent earns ' + Math.round(MOD_PARENT.feed * 100) + '% of everything ' +\n 'its children earn and grants no stats of its own. In return, a child below ' +\n 'the parent\\'s level trains ' + Math.round(MOD_PARENT.perLevel * 100) +\n '% faster per level of lag, up to ' + MOD_PARENT.maxGap + ' levels behind. ' +\n 'Bringing one section up makes every skill in it cheaper to catch up.

' +\n\n '

Saving

' +\n '

Three slots, switched from the bottom bar. The game builds its world once ' +\n 'at startup and cannot unload a save, so switching slots or starting a new ' +\n 'game reloads the page. Exporting a save before running anything unusual is ' +\n 'always the safe move.

';\n});\n\n/* --- the document ---------------------------------------------------------\n One string, no fetches, no dependencies: this has to work from file:// as\n readily as from a server.\n ------------------------------------------------------------------------- */\n\nfunction MOD_wikiCss() {\n return [\n ':root{color-scheme:dark}',\n '*{box-sizing:border-box}',\n /* Menlo and Consolas before the generic: with MS Gothic missing (any Mac)\n \"monospace\" is Courier, which Safari sets wider than Chrome does, and\n table cells wrapped on one and not the other. */\n 'body{margin:0;background:#060a1c;color:#dfe6f5;font:14px/1.6 \"MS Gothic\",' +\n '\"MS Mincho\",Menlo,Consolas,\"DejaVu Sans Mono\",monospace;display:flex;min-height:100vh}',\n 'a{color:#7cb0ff}',\n 'nav{width:210px;flex:0 0 210px;background:#081040;border-right:1px solid #46a;' +\n 'position:sticky;top:0;height:100vh;overflow:auto;padding:14px 0}',\n 'nav h2{font-size:13px;color:#9fb4d8;margin:0 0 10px 14px;letter-spacing:1px}',\n 'nav a{display:block;padding:6px 14px;text-decoration:none;color:#cfe0ff;' +\n 'border-left:3px solid transparent}',\n 'nav a:hover{background:#154080}',\n 'nav a.on{background:#123070;border-left-color:#7cb0ff;color:#fff}',\n 'nav .srch{margin:0 10px 12px;width:calc(100% - 20px);background:#050a20;' +\n 'color:#dfe6f5;border:1px solid #46a;padding:5px;font:inherit}',\n 'nav .xall{display:block;margin:12px 10px 0;color:#8296b8;font-size:12px;' +\n 'cursor:pointer;-webkit-user-select:none;user-select:none}',\n 'nav .xall input{vertical-align:-1px;margin-right:4px}',\n 'details.wk-g{border:1px solid #17224a;background:#0a1130;margin:8px 0}',\n 'details.wk-g>summary{cursor:pointer;padding:7px 12px;list-style:none;' +\n 'background:#0d1740}',\n 'details.wk-g>summary:hover{background:#154080}',\n 'details.wk-g>summary::-webkit-details-marker{display:none}',\n // a caret drawn rather than borrowed, so it is the same in every browser\n 'details.wk-g>summary::before{content:\"\\\\25B8\";color:#7cb0ff;' +\n 'display:inline-block;width:1em;transition:transform .12s}',\n 'details.wk-g[open]>summary::before{transform:rotate(90deg)}',\n 'details.wk-g>summary h2,details.wk-g>summary h3{display:inline;margin:0;' +\n 'font-size:15px;color:#fff}',\n '.wk-gi{padding:2px 12px 10px}',\n 'details.wk-g details.wk-g>summary h4{display:inline;margin:0;font-size:13px;' +\n 'color:#cfe0ff;font-weight:normal}',\n 'details.wk-g details.wk-g>summary{padding:5px 10px;background:#0b1435}',\n 'main{flex:1;min-width:0;padding:22px 30px 80px;max-width:1000px}',\n 'h1{font-size:26px;margin:0 0 6px;color:#fff;border-bottom:1px solid #46a;' +\n 'padding-bottom:8px}',\n 'h2{font-size:19px;color:#bcd4ff;margin:30px 0 8px}',\n 'h3{font-size:15px;color:#fff;margin:0 0 4px}',\n 'p.lede{color:#a8bcdc;margin:0 0 18px}',\n '.dim{color:#8296b8;font-weight:normal}',\n '.flav{font-size:12px;font-style:italic}',\n '.note{background:#0b1638;border-left:3px solid #46a;padding:8px 12px;' +\n 'color:#a8bcdc;margin:14px 0}',\n 'code,pre{background:#0b1638;border:1px solid #24356e;padding:1px 5px;' +\n 'color:#9fd0ff}',\n 'pre{padding:10px;overflow-x:auto}',\n 'dl dt{color:#fff;margin-top:14px}',\n 'dl dd{margin:2px 0 0 0;color:#a8bcdc}',\n 'ul{color:#a8bcdc}',\n 'table.wk-tbl{border-collapse:collapse;width:100%;margin:10px 0;' +\n 'display:block;overflow-x:auto}',\n 'table.wk-tbl th{text-align:left;color:#9fb4d8;font-weight:normal;' +\n 'border-bottom:1px solid #46a;padding:5px 9px;white-space:nowrap}',\n 'table.wk-tbl td{border-bottom:1px solid #17224a;padding:5px 9px;' +\n 'vertical-align:top}',\n 'table.wk-tbl tr.done td{color:#cfe0ff}',\n 'table.wk-tbl tr.here{background:#123070}',\n '.num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}',\n '.wk-e{border:1px solid #17224a;background:#081040;padding:9px 12px;' +\n 'margin:7px 0;border-radius:2px}',\n 'tr.wk-e{border:0;background:none}',\n '.wk-e.done{border-color:#2a4a8c}',\n '.wk-e h3{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}',\n '.lv{margin-left:auto;font-size:12px;color:#8296b8;font-weight:normal}',\n '.lv.capped{color:#ffd24a}',\n '.rank{font-size:12px;color:#8296b8;font-weight:normal}',\n '.desc{color:#a8bcdc;font-size:13px}',\n '.proj{color:#ffd24a;font-size:12px;margin-top:3px}',\n '.tag{font-size:11px;border:1px solid #46a;color:#9fb4d8;padding:0 5px;' +\n 'border-radius:2px;font-weight:normal}',\n '.tag.mod{border-color:#7a4ea8;color:#c07bff}',\n '.tag.done{border-color:#2e7d4f;color:#6fdc9a}',\n '.tag.here{border-color:#ffd24a;color:#ffd24a}',\n '.tag.warn{border-color:#a8562e;color:#ff9a5c}',\n 'table.wk-perks{border-collapse:collapse;margin:6px 0 0;width:100%}',\n 'table.wk-perks td{padding:2px 8px 2px 0;font-size:12px;color:#8296b8;' +\n 'border-bottom:1px solid #101a3c}',\n 'table.wk-perks tr.done td{color:#cfe0ff}',\n 'table.wk-perks td.tick{color:#6fdc9a;width:1em;text-align:right}',\n // fixed columns so the level bands line up between one area and the next\n 'table.wk-pop{table-layout:fixed}',\n 'table.wk-pop td:first-child{width:60%}',\n 'table.wk-pop td:nth-child(2){width:20%}',\n '.wk-title .tname{font-weight:bold}',\n '.wk-now{background:#0b1638;border:1px solid #24356e;padding:10px 14px;' +\n 'margin:0 0 18px;display:inline-block;min-width:320px}',\n '.wk-now h3{color:#9fb4d8;font-weight:normal;font-size:13px;' +\n 'letter-spacing:1px;margin-bottom:6px}',\n '.wk-now td{padding:2px 16px 2px 0}',\n '.wk-now td:last-child{color:#fff;text-align:right}',\n '.pg{display:none}.pg.on{display:block}',\n 'footer{color:#5a6c8c;font-size:12px;border-top:1px solid #17224a;' +\n 'margin-top:40px;padding-top:12px}',\n '@media(max-width:760px){body{display:block}nav{width:auto;height:auto;' +\n 'position:static;flex:none}main{padding:16px}}'\n ].join('\\n');\n}\n\n/* Filtering, page switching and the hash. Small enough to inline, and inlining\n it is the point — the document has to stand on its own. */\nfunction MOD_wikiJs() {\n return [\n 'var pages=[].slice.call(document.querySelectorAll(\".pg\"));',\n 'var links=[].slice.call(document.querySelectorAll(\"nav a[data-p]\"));',\n 'function show(id){',\n ' var found=false;',\n ' pages.forEach(function(p){var on=p.id===\"pg-\"+id;if(on)found=true;',\n ' p.className=\"pg\"+(on?\" on\":\"\")});',\n ' if(!found&&pages.length){pages[0].className=\"pg on\";id=pages[0].id.slice(3)}',\n ' links.forEach(function(a){a.className=a.dataset.p===id?\"on\":\"\"});',\n ' window.scrollTo(0,0);return id;}',\n 'links.forEach(function(a){a.addEventListener(\"click\",function(e){',\n ' e.preventDefault();location.hash=a.dataset.p;show(a.dataset.p)})});',\n 'window.addEventListener(\"hashchange\",function(){show(location.hash.slice(1))});',\n 'show(location.hash.slice(1)||\"start\");',\n // Search filters entries, hides groups left with none, and OPENS the groups\n // that still have some — a match hidden inside a collapsed group reads as\n // no match at all, which is worse than not having search.\n 'var box=document.getElementById(\"srch\");',\n 'var xall=document.getElementById(\"xall\");',\n 'var groups=[].slice.call(document.querySelectorAll(\"details.wk-g\"));',\n 'function filter(){',\n ' var q=box.value.trim().toLowerCase();',\n ' pages.forEach(function(p){',\n ' [].slice.call(p.querySelectorAll(\".wk-e\")).forEach(function(e){',\n ' e.style.display=(!q||e.textContent.toLowerCase().indexOf(q)>=0)?\"\":\"none\"})});',\n // Every page, not just the one on screen: the nav says how many entries\n // match in each, dims the ones with none, and Enter jumps to the first\n // page that has any. A prose page with no entries counts as one if its text\n // contains the query.\n ' links.forEach(function(a){',\n ' var pg=document.getElementById(\"pg-\"+a.dataset.p),n=0;',\n ' if(a.dataset.l===undefined)a.dataset.l=a.textContent;',\n ' if(q&&pg){var es=[].slice.call(pg.querySelectorAll(\".wk-e\"));',\n ' if(es.length)es.forEach(function(e){if(e.textContent.toLowerCase().indexOf(q)>=0)n++});',\n ' else if(pg.textContent.toLowerCase().indexOf(q)>=0)n=1}',\n ' a.dataset.n=n;a.style.opacity=(q&&!n)?\".4\":\"\";',\n ' a.textContent=a.dataset.l+(q&&n?\" (\"+n+\")\":\"\")});',\n ' groups.forEach(function(g){',\n ' var any=[].slice.call(g.querySelectorAll(\".wk-e\")).some(function(e){',\n ' return e.style.display!==\"none\"});',\n ' g.style.display=any?\"\":\"none\";',\n ' g.open=xall.checked||(!!q&&any)}); }',\n 'box.addEventListener(\"input\",filter);',\n 'box.addEventListener(\"keydown\",function(e){',\n ' if(e.key===\"Escape\"){box.value=\"\";filter()}',\n ' if(e.key===\"Enter\"&&box.value.trim()){',\n ' var cur=links.filter(function(a){return a.className===\"on\"})[0];',\n ' if(cur&&+cur.dataset.n)return;',\n ' var hit=links.filter(function(a){return +a.dataset.n>0})[0];',\n ' if(hit){location.hash=hit.dataset.p;show(hit.dataset.p)}}});',\n // Everything is collapsed by default because three of these pages run past\n // 40,000px open. This is the way back to one long document, which is what\n // the browser's own find needs.\n 'xall.addEventListener(\"change\",filter);'\n ].join('\\n');\n}\n\nfunction MOD_wikiBuild() {\n var nav = '', body = '';\n MOD_WIKI_PAGES.forEach(function (pg) {\n nav += '' +\n MOD_WIKI.safe(pg.label) + '';\n var inner;\n /* One page throwing must not take the wiki down with it — the page says so\n and the rest still renders. */\n try { inner = pg.build(); }\n catch (e) {\n inner = '

' + MOD_WIKI.safe(pg.label) + '

This page ' +\n 'could not be built: ' + MOD_WIKI.safe(e.message) + '

';\n console.warn('[mod] wiki page \"' + pg.id + '\" failed: ' + e.message);\n }\n body += '
' + inner + '
';\n });\n\n var stamp = new Date().toLocaleString();\n return '' +\n '' +\n '' + MOD_WIKI.safe(MOD_WIKI.title) + '' +\n '' +\n '' +\n '
' + body +\n '
Generated from the running game at ' + MOD_WIKI.safe(stamp) +\n ' — mod ' + MOD_WIKI.safe(MOD.version) + '. Everything here is read from the ' +\n 'live data, including your own save, so it cannot fall out of step with the ' +\n 'game. The game is truezhangwei\\'s; the mod is not endorsed by the author.' +\n '
' +\n ' inside content closing the block early, so that is the\n one thing scrubbed. */\n safe: function (h) {\n return String(h === undefined || h === null ? '' : h).replace(/<\\/script/gi, '<\\\\/script');\n }\n};\n\n/* Descriptions carry real HTML (coloured spans, the game's separator div), so\n they are passed through rather than escaped — but the separator becomes a\n rule, since a wiki row is not a tooltip. */\nfunction MOD_wikiDesc(d) {\n var s;\n try { s = (typeof d === 'function') ? d() : d; } catch (e) { s = ''; }\n if (!s) return '';\n s = String(s).split('
 
').join('
');\n return MOD_WIKI.safe(s);\n}\n\nfunction MOD_wikiNum(n) {\n if (!isFinite(n)) return '—';\n return Math.round(n).toLocaleString();\n}\n\n/* An effect line at a level the player has not reached yet. The effect\n functions read the skill they are handed, so the level is borrowed and put\n back; anything that throws simply gets no projection. */\nfunction MOD_wikiEffectAt(key, sk, lvl) {\n var eff = MOD_EFFECTS[key];\n if (!eff) return '';\n var was = sk.lvl, out = '';\n try { sk.lvl = lvl; out = eff(sk) || ''; }\n catch (e) { out = ''; }\n finally { sk.lvl = was; }\n return MOD_WIKI.safe(out);\n}\n\n/* --- page builders --------------------------------------------------------\n Each returns the inner HTML of one page. `wk-e` marks a searchable entry and\n `wk-g` a group that hides itself when all its entries are filtered out.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_PAGES = [];\n\nfunction MOD_wikiPage(id, label, build) {\n MOD_WIKI_PAGES.push({ id: id, label: label, build: build });\n}\n\n/* A collapsible group.\n\n Not decoration: with every skill, item and title listed, three of the eleven\n pages ran past 40,000 pixels — a hundred screens of scrolling, which is the\n same as not having the content at all. Collapsed, each of those pages opens\n as an index you can read in one screen and expand where you want.\n\n
rather than a click handler, so it still works with JavaScript off,\n the browser's own find can be made to reach inside it, and the summary is\n keyboard-reachable for free. `level` is the heading level for the summary,\n since a group is sometimes a section of a page and sometimes a subsection. */\nfunction MOD_wikiGroup(heading, count, inner, level) {\n var hn = 'h' + (level || 2);\n return '
<' + hn + '>' + heading +\n (count !== null && count !== undefined\n ? ' (' + count + ')' : '') +\n '
' + inner + '
';\n}\n\n/* --- 1. orientation ------------------------------------------------------- */\nMOD_wikiPage('start', 'Start here', function () {\n var cap = MOD_levelCap();\n var realm = MOD_realm();\n var h = '

proto23

' +\n '

An idle RPG by truezhangwei, plus a mod. You train skills by ' +\n 'doing things, the things you can do open up as the story does, and none of ' +\n 'it is in a hurry.

' +\n\n '

Where you are right now

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Character level' + MOD_wikiNum(you.lvl) + '
Skill level cap' + cap + ' — ' +\n MOD_WIKI.safe(MOD_CAP.name) + '
Cultivation realm' + MOD_WIKI.safe(realm.name) +\n ' (' + realm.n + ' of ' + (MOD_REALMS.length - 1) + ')
Titles held' + MOD_titleCount() + ' — renown ' +\n (skl.rnwn ? skl.rnwn.lvl : 0) + '
Coin' + MOD_wikiNum(you.wealth || 0) + '
' +\n\n '

The four things worth knowing early

' +\n '
' +\n '
Your skill levels are capped by the story, not by grinding.
' +\n '
The cap starts at 10 and ends at 110, and it moves when you reach a new ' +\n 'place — not when you earn enough experience. If a skill has stopped ' +\n 'levelling, you are not doing it wrong; you need to go somewhere. See ' +\n 'Progression.
' +\n\n '
Armour is subtraction, so levels matter more than they look.
' +\n '
A hit does the attacker\\'s STR minus the defender\\'s STR. That means a ' +\n 'small gap in strength is a large gap in damage, and a big enough one takes ' +\n 'your damage to nothing at all rather than merely reducing it. See ' +\n 'Mechanics.
' +\n\n '
Most skills have a parent that pulls them along.
' +\n '
Every section has a parent skill that earns a share of everything its ' +\n 'children do, and any child lagging behind the parent trains faster — up to ' +\n 'twice as fast at ' + MOD_PARENT.maxGap + ' levels behind. A neglected skill is ' +\n 'cheap to bring up later, which is the only reason having this many is ' +\n 'workable.
' +\n\n '
Cultivation is a second ladder, and it is not automatic.
' +\n '
Circulating qi levels a skill; the skill does not raise your realm. ' +\n 'Reaching the level opens a bottleneck, and breaking through it costs a pill ' +\n 'and can fail. See Cultivation.
' +\n '
' +\n\n '

What is the mod and what is the game

' +\n '

The game is index.html and it is entirely truezhangwei\\'s ' +\n 'work. Everything in mod.js is a modification of it and is not ' +\n 'endorsed by the author. Pages here mark added content with ' +\n 'mod where the distinction matters. The full ' +\n 'list of changes is in the changelog, on the bottom bar.

';\n return h;\n});\n\n/* --- 2. progression ------------------------------------------------------- */\nMOD_wikiPage('progress', 'Progression', function () {\n var cap = MOD_levelCap();\n var h = '

Progression

' +\n '

Skill levels are capped by how far the story has gone. Ten ' +\n 'rungs, 10 through 110. The cap applies to every skill at once; Renown is ' +\n 'the single exception, since it is counted rather than trained.

' +\n '' +\n '';\n\n MOD_TIERS.forEach(function (t) {\n var got = null;\n try { got = MOD_tierFlag(t); } catch (e) {}\n var here = (t.cap === cap);\n var need = t.needText ? t.needText\n : (t.flags.length ? t.flags.join(' or ') : 'the start of the game');\n h += '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
CapStageWhat opens it
' + t.cap + '' + MOD_WIKI.safe(t.name) + '' + MOD_WIKI.safe(need) + '' + (here ? 'you are here'\n : got ? 'reached' : '') + '
' +\n\n '

The experience curve

' +\n '

The game\\'s own skill curve is super-exponential — level 109 to 110 ' +\n 'alone costs ' + MOD_wikiNum(MOD_vanillaExpnext(109)) + ' experience, so the top of the ladder ' +\n 'is unreachable by a factor of about ten trillion. But the author only ever ' +\n 'designed skills up to level 15, and below that his curve is fine.

' +\n (MOD_XP.vanillaTo > 0\n ? '

So the mod keeps it. Below level ' + MOD_XP.vanillaTo + ' a skill costs ' +\n 'exactly what the original charges; from there it rises by ×' +\n MOD_XP.ratio.toFixed(4) + ' a level, a ratio solved so the climb to 110 takes ' +\n 'the same time it always did.'\n : '

You have chosen the Mod before 4.2 pacing: a plain geometric curve, ' +\n '×' + MOD_XP.ratio.toFixed(3) + ' a level from level 0, much faster than the ' +\n 'original early on. The Original pacing in settings puts back the ' +\n 'author\\'s own cost below level 10.') +\n ' Character level uses the game\\'s own curve, untouched.

' +\n '' +\n '';\n [1, 5, 9, 10, 15, 25, 50, 75, 90, 109].forEach(function (l) {\n h += '';\n });\n h += '
LevelExperience for the nextOriginal game
' + l +\n '' + MOD_wikiNum(MOD_expnextFor(l)) + '' +\n MOD_wikiNum(MOD_vanillaExpnext(l)) + '
';\n return h;\n});\n\n/* --- 3. skills ------------------------------------------------------------ */\nMOD_wikiPage('skills', 'Skills', function () {\n var cap = MOD_levelCap();\n var bySection = {};\n var keys = [];\n for (var k in skl) {\n var s = skl[k];\n if (!s || typeof s !== 'object' || !s.name) continue;\n keys.push(k);\n }\n keys.forEach(function (k) {\n var s = skl[k];\n var sec = MOD_sectionOf(s);\n (bySection[sec] = bySection[sec] || []).push(k);\n });\n\n var h = '

Skills

' +\n '

' + keys.length + ' skills in ' +\n Object.keys(MOD_SECTIONS).length + ' sections. Every one has perks at ' +\n MOD_LADDER.join(', ') + ', and every one that has milestones grants titles at ' +\n MOD_TITLE_RUNGS.join(', ') + '. Levels and ticks below are yours.

';\n\n Object.keys(MOD_SECTIONS).forEach(function (sec) {\n var list = bySection[sec];\n if (!list || !list.length) return;\n var parentKey = MOD_PARENT_KEY[sec];\n var g = '';\n if (parentKey && skl[parentKey]) {\n g += '

Parent skill: ' +\n MOD_WIKI.safe(skl[parentKey].bname || skl[parentKey].name) +\n ' — earns ' + Math.round(MOD_PARENT.feed * 100) +\n '% of what its children earn, and speeds up any child that lags behind it.

';\n }\n list.sort(function (a, b) { return skl[a].name < skl[b].name ? -1 : 1; });\n list.forEach(function (k) {\n var s = skl[k];\n var lvl = Number(s.lvl) || 0;\n g += '
' +\n '

' + MOD_WIKI.safe(s.bname || s.name) +\n (s.bname ? ' (' + MOD_WIKI.safe(s.name) + ')' : '') +\n '= cap ? ' capped' : '') + '\">lv ' + lvl +\n (lvl >= cap ? ' — at the cap' : '') + '

' +\n '
' + MOD_wikiDesc(s.desc) + '
';\n\n var at110 = MOD_wikiEffectAt(k, s, 110);\n if (at110) g += '
At level 110: ' + at110 + '
';\n\n if (s.mlstn && s.mlstn.length) {\n g += '';\n s.mlstn.forEach(function (m) {\n g += '' +\n '' +\n '' +\n '';\n });\n g += '
' + m.lv + '' + MOD_WIKI.safe(m.p || '—') + '' + (m.g === true ? '✓' : '') + '
';\n }\n g += '
';\n });\n h += MOD_wikiGroup(MOD_WIKI.safe(MOD_SECTIONS[sec]), list.length, g, 2);\n });\n return h;\n});\n\n/* --- 4. cultivation ------------------------------------------------------- */\nMOD_wikiPage('cultivation', 'Cultivation', function () {\n var now = MOD_realm();\n var elig = 0;\n try { elig = MOD_realmEligible().n; } catch (e) {}\n var qic = skl.qic ? skl.qic.lvl : 0;\n\n var h = '

Cultivation

' +\n '

Ten realms above Mortal. Circulating qi levels the Qi ' +\n 'Circulation skill; the skill does not advance the realm. Reaching the level ' +\n 'opens a bottleneck, and breaking through it costs a pill and can fail — the ' +\n 'pill is spent either way.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Realm' + MOD_WIKI.safe(MOD_realmTitle()) + '
Spiritual Root' + MOD_WIKI.safe(MOD_root().name) + '
Qi Circulationlevel ' + qic + '
Insight' + MOD_insight() + '
Bottleneck open' +\n (elig > now.n ? 'yes — ' + MOD_WIKI.safe(MOD_REALMS[now.n + 1].name) +\n ' is within reach' : 'no') + '
Qi Deviation' + (MOD_deviated()\n ? MOD_deviationLeft() + ' in-game minutes left' : 'no') + '
' +\n '

Realm thresholds are the same levels that earn a title rank, so realm N ' +\n 'is the level at which a rank N title arrives. Every realm multiplies all ' +\n 'stats and health, and the multipliers do not stack with each other — the ' +\n 'realm you are in is the one that applies.

' +\n '' +\n '';\n\n MOD_REALMS.forEach(function (r) {\n var pill = r.n >= 1 ? item['mod_bp' + r.n] : null;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmNameQi Circ.StatsHealthPill
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + r.qic + '×' + r.mult + '×' + r.hp + '' + (pill ? MOD_WIKI.safe(pill.name) : '—') +\n '
' +\n\n '

Where the pills come from

' +\n '

Every breakthrough pill has a source in normal play. None of them are ' +\n 'craftable, and none are cheap — a bottleneck you can buy in bulk is not a ' +\n 'bottleneck.

' +\n '' +\n '' +\n '' +\n '' +\n '
RealmsSource
1The dojo instructor, when qi ' +\n 'circulation is first taught
2–5The Herbalist in the village ' +\n '(vendor.pha1), restocked
6–10The dojo\\'s Level Advancement ' +\n 'ladder, and the Pill Tower
' +\n '

Realm 1 is deliberately not sold at the marketplace: ' +\n 'the marketplace is behind a 40%-a-visit chance encounter, and realm 1 opens ' +\n 'long before that is reliable.

' +\n\n '

Breaking through

' +\n '

A bottleneck wants three things, which is what the genre says it wants: ' +\n 'a pill, insight, and enough consolidation to make the ' +\n 'attempt worth making. The pill and the insight are both spent whether the ' +\n 'attempt works or not.

' +\n '

The odds improve the further past the threshold you are, plus your root, ' +\n 'plus time sat in Closed Door Training, and cap short of certain.

' +\n '' +\n '';\n [0, 2, 4, 6, 8].forEach(function (o) {\n var base = Math.min(0.55 + o * 0.05 + MOD_root().odds, 0.95);\n h += '';\n });\n h += '
Levels past the thresholdChanceAfter full seclusion
+' + o + '' +\n Math.round(base * 100) + '%' +\n Math.round(Math.min(base + MOD_CULT.secludeOdds, 0.95) * 100) + '%
' +\n\n '

Insight

' +\n '

Insight is the half of the price that cannot be bought. It comes from ' +\n 'three places, and they are the three the genre names: meditating, fights you ' +\n 'nearly lost, and going somewhere you have not been.

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
SourceRate
Circulate Qi, at a wall' +\n (MOD_CULT.meditateChance * 100).toFixed(1) + '% a second
Closed Door Training' +\n (MOD_CULT.secludeChance * 100).toFixed(1) + '% a second
A kill made under ' +\n Math.round(MOD_CULT.brinkFrac * 100) + '% health — ' +\n (MOD_FIGHTS.mode === 'original'\n ? 'the author\\'s areas rarely come close once you reach them; the three past ' +\n 'the golem arena and the rank duels do, within a few fights, as does an area ' +\n 'you walk into early'\n : 'every fight costs a share of your health, so a few in a row without resting') +\n '' +\n Math.round(MOD_CULT.brinkChance * 100) + '% a kill
Entering an area for the first timealways
' +\n '

The insight a realm asks for is sized to the climb to it: about ' +\n Math.round(MOD_CULT.wallShare * 100) + '% of the Circulate Qi time the climb took, ' +\n 'spent at the wall. Seclusion earns it three times as fast, and full ' +\n 'consolidation takes as long as that on average.

' +\n '' +\n '' +\n '';\n var wkHours = function (sec) {\n return sec >= 7200 ? (sec / 3600).toFixed(sec >= 36000 ? 0 : 1) + ' h' : Math.round(sec / 60) + ' min';\n };\n MOD_REALMS.forEach(function (r) {\n if (!r.n) return;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmClimb (Circulate Qi, 1x)Insight to enterFull seclusionQi Deviation (in-game)
' + MOD_WIKI.safe(r.name) + '' + wkHours(MOD_realmClimbSec(r.n)) + '' + MOD_insightNeed(r.n) + '' + wkHours(MOD_secludeFull(r.n)) + '' + (MOD_devMinutes(r.n) / 1440).toFixed(1) + ' days
' +\n\n '

The nine layers

' +\n '

Each realm is nine layers deep, derived from where Qi Circulation sits ' +\n 'between this realm\\'s threshold and the next one\\'s — so breaking through ' +\n 'puts you at the first layer of the new realm on its own. Layers are worth ' +\n '×' + MOD_CULT.layerMult + ' stats and ×' + MOD_CULT.layerBody +\n ' body each above the first, and the top realm has no rung above it to ' +\n 'measure against, so it stays at its first layer.

' +\n '' +\n '';\n for (var li = 1; li <= MOD_CULT.layers; li++) {\n h += '';\n }\n h += '
LayerCalled
' +\n li + '' + MOD_LAYER_ORD[li] + ' layer, ' +\n MOD_WIKI.safe(MOD_layerStage(li)) + '
' +\n '

At the threshold of the next realm and before breaking ' +\n 'through, you are a half step to it — the genre\\'s own name for the ' +\n 'state this mod calls a bottleneck.

' +\n\n '

Closed Door Training

' +\n '

Seclusion does two things. It consolidates — a full sitting ' +\n '(the table above) is worth +' + Math.round(MOD_CULT.secludeOdds * 100) +\n '% on the attempt — and it shelters: a breakthrough attempted while ' +\n 'you are still sitting in it cannot end in Qi Deviation, only in a lost ' +\n 'pill. It will not start unless you are actually at a wall.

' +\n\n '

Qi Deviation

' +\n '

Failing a breakthrough out in the open leaves your cultivation base ' +\n 'unstable for as long as the seclusion you skipped (the table above, never ' +\n 'under ' + MOD_CULT.devMinutes + ' in-game minutes): stats at ×' +\n MOD_CULT.devStat + ', body at ×' + MOD_CULT.devBody + ', and no second ' +\n 'attempt until it passes. It clears on its own, or at once with a ' +\n 'Qi Settling Pill. It never kills — the death system stays out of it.

' +\n\n '

Heavenly Tribulation

' +\n '

From realm ' + MOD_CULT.tribFrom + ' up, a successful breakthrough is ' +\n 'answered. The cost is a fraction of your maximum health, so it is a gate on ' +\n 'walking in healthy rather than a damage race; the realm bonus you are ' +\n 'standing on is what pays it. Fall to it and the realm does not open.

' +\n '' +\n '';\n MOD_REALMS.forEach(function (r) {\n if (!r.n || r.n < MOD_CULT.tribFrom) return;\n h += '' +\n '' +\n '';\n });\n h += '
RealmBoltsOf your max HP
' + MOD_WIKI.safe(r.name) + '' + MOD_tribulationBolts(r.n) + '' + Math.round(MOD_tribulationCost(r.n) * 100) + '%
' +\n\n '

Spiritual Root

' +\n '

Rolled once, when the dojo finishes teaching you to fight. It is never a ' +\n 'dead end: the worst grade costs cultivation speed, not access, and a ' +\n 'Root Cleansing Pill moves you up a grade.

' +\n '' +\n '';\n var rootTotal = 0;\n MOD_ROOTS.forEach(function (r) { rootTotal += r.weight; });\n MOD_ROOTS.forEach(function (r) {\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
GradeRootChanceCultivationBreakthrough
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + Math.round(r.weight / rootTotal * 100) + '%×' + r.xp + '' + (r.odds >= 0 ? '+' : '') +\n Math.round(r.odds * 100) + '%
';\n return h;\n});\n\n/* --- 5. elemental mastery ------------------------------------------------- */\nMOD_wikiPage('mastery', 'Elemental mastery', function () {\n var h = '

Elemental mastery mod

' +\n '

Six masteries, each with a technique that fires on its own ' +\n 'during a fight. Combat is automatic and there is no ability picker, so a ' +\n 'technique is a chance on each of your swings to become something else — one ' +\n 'that scales with INT and the element rather than with your weapon.

' +\n '

Nothing fires while you are still Mortal: the channels have to be open ' +\n 'first. Chance is ' + (MOD_TECH.perLevel * 100).toFixed(1) + '% per mastery ' +\n 'level plus ' + (MOD_TECH.perRealm * 100).toFixed(1) + '% per realm, capped at ' +\n Math.round(MOD_TECH.cap * 100) + '%; a technique lands for ×' +\n MOD_TECH.power + ' of a normal strike.

' +\n '' +\n '';\n\n var realm = 0;\n try { realm = MOD_realm().n; } catch (e) {}\n MOD_MASTERY.forEach(function (m) {\n var lvl = Number(m.skill.lvl) || 0;\n var ch = realm < 1 ? 0\n : Math.min(lvl * MOD_TECH.perLevel + realm * MOD_TECH.perRealm, MOD_TECH.cap);\n h += '' +\n '' +\n '' +\n '';\n });\n h += '
MasteryTechniqueLevelChance now
' +\n MOD_WIKI.safe(m.skill.name) + '' + MOD_WIKI.safe(m.name) + '' + lvl + '' + (realm < 1 ? 'Mortal'\n : (ch * 100).toFixed(1) + '%') + '
';\n return h;\n});\n\n/* --- 6. titles ------------------------------------------------------------ */\nMOD_wikiPage('titles', 'Titles', function () {\n var perm = MOD_permanentRank();\n var held = MOD_titleCount();\n var total = 0; for (var kk in ttl) if (ttl[kk] && ttl[kk].name) total++;\n\n var h = '

Titles

' +\n '

A title\\'s rank runs 1 to 10 and is derived from the level ' +\n 'that earns it, so a rank is a statement about where in the game you are. ' +\n 'Only the title you are wearing applies — until renown makes the lower ranks ' +\n 'permanent.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '
Titles held' + held + ' of ' + total + '
Renown level' + (skl.rnwn ? skl.rnwn.lvl : 0) +\n ' of ' + MOD_RENOWN_MAX + '
Ranks that apply without wearing' +\n (perm >= 1 ? '1–' + perm : 'none yet') + '
' +\n\n '

Ranks

' +\n '';\n MOD_RANK_AT.forEach(function (lv, i) {\n var st = MOD_rankStyle(i + 1);\n h += '' +\n '';\n });\n h += '
RankEarned from levelColour
' + (i + 1) + '' + lv + '' + '●●●' + '
' +\n\n '

Renown

' +\n '

Renown is not trained. It counts the titles you hold, and each level ' +\n 'multiplies every stat by ' + (1 + MOD_RENOWN_RATE) + ' — compounding, so ' +\n 'level ' + MOD_RENOWN_MAX + ' is ×' +\n (Math.round(MOD_renownMult(MOD_RENOWN_MAX) * 1000) / 1000) + '. At renown N, ' +\n 'every title of rank N or below applies permanently.

' +\n '' +\n '';\n MOD_RENOWN_STEPS.forEach(function (n, i) {\n h += '= n ? 'done' : '') + '\">' +\n '';\n });\n h += '
RenownTitles neededAll statsPassive ranks
' + (i + 1) +\n '' + n + '×' +\n (Math.round(MOD_renownMult(i + 1) * 1000) / 1000) + '1–' + (i + 1) + '
' +\n\n '

Every title

' +\n '

Grouped by the skill that grants it. What is left is either ' +\n 'a ladder of its own — creatures killed, items gathered, the ' +\n 'cultivation realms — or a one-off from the story.

';\n\n var groups = {}, fams = {}, loose = [];\n for (var k in ttl) {\n var t = ttl[k];\n /* Unlike the picker, the wiki KEEPS the author's broken entries and says\n so — same call as area.clg, which is listed and tagged \"nothing spawns\"\n rather than quietly dropped. A reader looking for why a title never\n appears is better served by the row than by its absence. */\n if (!t || t.name === undefined || t.name === null) continue;\n var sk = MOD_TITLE_SKILL[k];\n if (sk && skl[sk]) { (groups[sk] = groups[sk] || []).push(k); continue; }\n var stem = MOD_titleStem(k);\n if (stem) { (fams[stem] = fams[stem] || []).push(k); continue; }\n loose.push(k);\n }\n /* Same rule as the picker: a family of one is not a family. */\n Object.keys(fams).forEach(function (stem) {\n if (fams[stem].length < 2) { loose.push(fams[stem][0]); delete fams[stem]; }\n });\n var order = Object.keys(groups).sort(function (a, b) {\n return skl[a].name < skl[b].name ? -1 : 1;\n });\n\n var row = function (k) {\n var t = ttl[k];\n var st = MOD_rankStyle(t.rar);\n var gen = null;\n for (var i = 0; i < MOD_SKILL_TITLES.length; i++) {\n if (MOD_SKILL_TITLES[i].key === k) { gen = MOD_SKILL_TITLES[i]; break; }\n }\n /* The author left a handful unfinished: ttl.ddcd carries the literal string\n \"null\" for both name and description, and shpt2/shpt3/mone3 are blank.\n None is granted by anything, so none can be earned. */\n var broken = !t.name || t.name === 'null';\n return '
' +\n '' +\n MOD_WIKI.safe(t.name || '(unnamed)') + '' +\n (broken ? 'unfinished' : '') +\n 'rank ' + (t.rar || 1) + '' +\n (gen ? 'lv ' + gen.rung + ' · +' +\n Math.round(gen.xp * 100) + '% ' + MOD_WIKI.safe(skl[gen.skill].name) +\n ' exp' : '') +\n (t.have === true ? 'held' : '') +\n '
' + MOD_wikiDesc(t.desc) + '
';\n };\n\n var byRank = function (a, b) { return (ttl[a].rar || 1) - (ttl[b].rar || 1); };\n order.forEach(function (sk) {\n groups[sk].sort(byRank);\n h += MOD_wikiGroup(MOD_WIKI.safe(skl[sk].bname || skl[sk].name),\n groups[sk].length, groups[sk].map(row).join(''), 3);\n });\n /* The ladders that belong to no skill, by the same derivation the picker\n uses — so the two cannot drift apart. Sorted by the label, since that is\n what the reader scans. */\n var famList = Object.keys(fams)\n .map(function (stem) {\n fams[stem].sort(byRank);\n return { stem: stem,\n label: MOD_titleFamilyLabel(stem, fams[stem].map(function (k) { return ttl[k]; })) };\n })\n .sort(function (a, b) { return a.label < b.label ? -1 : a.label > b.label ? 1 : 0; });\n if (famList.length) {\n /* Nested one level down rather than sitting beside the skills. Eleven more\n top-level summaries cost ~530px on a page that is already the tallest in\n the wiki, and these are a different KIND of thing from a skill anyway. */\n var famInner = '', famTotal = 0;\n famList.forEach(function (f) {\n famTotal += fams[f.stem].length;\n famInner += MOD_wikiGroup(MOD_WIKI.safe(f.label), fams[f.stem].length,\n fams[f.stem].map(row).join(''), 4);\n });\n h += MOD_wikiGroup('Ladders of their own', famTotal, famInner, 3);\n }\n if (loose.length) {\n loose.sort(byRank);\n /* The story titles are the one group with no skill to divide them, and\n there are enough of them to need dividing. Rank rather than the alphabet:\n it is the axis they are already sorted on, and the one a reader cares\n about — a rank is a statement about where in the game a title comes from. */\n var inner;\n if (loose.length <= MOD_WIKI_ALPHA_AT) {\n inner = loose.map(row).join('');\n } else {\n var byRankNo = {};\n loose.forEach(function (k) {\n var r = Math.min(Math.max(Number(ttl[k].rar) || 1, 1), 10);\n (byRankNo[r] = byRankNo[r] || []).push(k);\n });\n inner = '';\n Object.keys(byRankNo).sort(function (a, b) { return a - b; }).forEach(function (r) {\n var st = MOD_rankStyle(r);\n inner += MOD_wikiGroup('Rank ' + r + '', byRankNo[r].length,\n byRankNo[r].map(row).join(''), 4);\n });\n }\n h += MOD_wikiGroup('From the story', loose.length, inner, 3);\n }\n return h;\n});\n\n/* --- 7. areas ------------------------------------------------------------- */\nMOD_wikiPage('areas', 'Areas & enemies', function () {\n var h = '

Areas & enemies

' +\n '

Every area, what lives in it and at what levels, and what it ' +\n 'drops. Enemy strength is not read off these levels — see the note below the ' +\n 'table.

';\n\n /* Two of the game's 21 areas are not places and listing them misleads:\n nwh \"Somewhere\" — where global.current_z parks whenever you are NOT in\n a fight, populated by creature.default (\"Nothing\").\n tst \"Test\" — reached only from chss.tst, which has id -1 and is in no\n sector, so nothing in the game can travel there.\n The test for \"is this a place\" is whether you can get to it, and neither\n can be. Everything else is listed whether or not you have been. */\n var MOD_WIKI_NOT_PLACES = { nwh: 1, tst: 1 };\n\n var keys = [];\n for (var k in area) {\n if (area[k] && area[k].pop && !MOD_WIKI_NOT_PLACES[k]) keys.push(k);\n }\n /* Ordered by the middle of the level band, which is the closest thing the\n game has to a difficulty order. */\n keys.sort(function (a, b) {\n var mid = function (z) {\n if (!z.pop.length) return 0;\n var s = 0; z.pop.forEach(function (e) { s += (e.lvlmin + e.lvlmax) / 2; });\n return s / z.pop.length;\n };\n return mid(area[a]) - mid(area[b]);\n });\n\n keys.forEach(function (k) {\n var z = area[k];\n /* An area whose baked bands are not finite can never spawn anything — see\n the note rendered under its table. Derived rather than listed by name, so\n it stays true if the game changes. */\n var broken = !z.popc || z.popc.length !== z.pop.length ||\n z.popc.some(function (band) { return !band || !isFinite(band[1] - band[0]); });\n h += '

' + MOD_WIKI.safe(z.name) +\n ' ' + k + '' +\n (broken ? 'nothing spawns' : '') +\n '' + (z.size === -1 ? 'endless' : z.size + ' to clear') +\n '

';\n if (z.pop.length) {\n /* The spawn share is NOT pop[i].c. z_bake normalises those weights into\n area.popc — a list of [lo,hi] bands that mon_gen rolls a uniform\n random against — and the weights do not have to sum to 1, so they are\n off wherever they don't. The Southern forest's .35/.45/.25 are really\n 33/43/24%. Read the bands the game actually rolls against. */\n var bands = z.popc || [];\n h += '';\n z.pop.forEach(function (e, i) {\n var c = e.crt || {};\n var band = bands[i];\n var share = band ? band[1] - band[0] : NaN;\n h += '' +\n '' +\n '';\n });\n h += '
' + MOD_WIKI.safe(c.name || '?') + 'lv ' + e.lvlmin +\n (e.lvlmax !== e.lvlmin ? '–' + e.lvlmax : '') + '' + (isFinite(share)\n ? Math.round(share * 100) + '% of spawns'\n : '—') + '
';\n if (broken) {\n h += '
Nothing spawns here. The area\\'s entries carry ' +\n 'no spawn weight, so the table the game rolls against comes out as ' +\n 'NaN and no creature can ever match it. A bug in the base game, left ' +\n 'as it is — nothing in the game travels to this area either.
';\n }\n }\n if (z.drop && z.drop.length) {\n var d = z.drop.map(function (x) {\n return MOD_WIKI.safe((x.item && x.item.name) || '?') +\n ' ' + ((x.c || 0) * 100).toFixed(2) + '%';\n }).join(', ');\n h += '
Drops: ' + d + '
';\n }\n h += '
';\n });\n\n var fightsOrig = MOD_FIGHTS.mode === 'original';\n h += '

How hard an enemy is

' +\n '

It depends on the Fights setting, which is ' +\n (fightsOrig ? 'Original' : 'Scaled') + ' now. On Original (the default) ' +\n 'every area the author wrote is exactly as he wrote it: his creatures, his ' +\n 'level ranges, and you out-grow them as you do in his game. On Scaled every ' +\n 'area is sized to you. The three areas past the golem arena and the rank ' +\n 'duels are sized to you either way.

' +\n '

Sizing to you works like this. Enemies are not fitted to a level. Each spawn is solved against your ' +\n 'actual power at the moment it appears, aiming at roughly ' + MOD_ENEMY.kill +\n ' swings to kill it and ' + MOD_ENEMY.die + ' before it kills you, with a ' +\n 'guaranteed margin between the two so a fight is never a coin flip. Variety ' +\n 'comes from the area\\'s level band and each creature\\'s own stat weighting, ' +\n 'not from difficulty scaling.

' +\n '

The reason for that: within the first stage alone you go from 1 STR to ' +\n 'around 265, so anything anchored to a level is either trivial at the end of ' +\n 'a stage or impossible at the start of one.

';\n return h;\n});\n\n/* --- 8. items -------------------------------------------------------------\n The one place where \"list everything\" needs help: 321 of the game's 371\n plain items are the same inventory type, so grouping by type alone leaves a\n single group of 321. Anything past MOD_WIKI_ALPHA_AT gets an alphabetical\n sub-split as well — the oldest index pattern there is, and derived rather\n than invented, which a hand-made taxonomy would not be.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_ALPHA_AT = 60;\n\n/* The game's own inventory tabs (isort): ALL / WPN / EQP / USE / OTHER. Using\n its categories rather than new ones means a group here is a tab there. */\nvar MOD_WIKI_STYPE = { 2: 'Weapons', 3: 'Equipment', 4: 'Usable', 5: 'Other' };\n\nfunction MOD_wikiItemRow(it) {\n var val = it.v !== undefined ? it.v : (it.val !== undefined ? it.val : null);\n return '
' +\n '

' + MOD_WIKI.safe(it.name) +\n (it.rar > 1 ? 'rarity ' + it.rar + '' : '') +\n (val !== null ? '' + MOD_wikiNum(val) + ' coin' : '') +\n (it.amount > 0 ? '×' + it.amount + '' : '') +\n '

' + MOD_wikiDesc(it.desc) + '
';\n}\n\n/* The letter a name files under. Several items are quoted — the master's\n manuals are literally named \"Sword Saint Manual\" — and filing those under a\n quotation mark is no use to anyone looking for S. */\nfunction MOD_wikiLetter(name) {\n var m = String(name || '').replace(/^[^0-9A-Za-z]+/, '');\n return (m.charAt(0) || '?').toUpperCase();\n}\n\nfunction MOD_wikiByLetter(a, b) {\n var x = MOD_wikiLetter(a.name) + String(a.name || '').toLowerCase();\n var y = MOD_wikiLetter(b.name) + String(b.name || '').toLowerCase();\n return x < y ? -1 : (x > y ? 1 : 0);\n}\n\n/* Flat below the threshold, alphabetically sub-grouped above it. Ranges are cut\n from the entries actually present, so they stay even as content is added. */\nfunction MOD_wikiAlpha(entries, render, level) {\n if (entries.length <= MOD_WIKI_ALPHA_AT) return entries.map(render).join('');\n var per = Math.ceil(entries.length / Math.ceil(entries.length / 40));\n var out = '';\n for (var i = 0; i < entries.length; i += per) {\n var chunk = entries.slice(i, i + per);\n var first = MOD_wikiLetter(chunk[0].name);\n var last = MOD_wikiLetter(chunk[chunk.length - 1].name);\n out += MOD_wikiGroup(first === last ? first : first + '–' + last,\n chunk.length, chunk.map(render).join(''), level || 4);\n }\n return out;\n}\n\nMOD_wikiPage('items', 'Items', function () {\n var h = '

Items

' +\n '

Everything the game defines, whether or not you have seen ' +\n 'it. Grouped the way your inventory groups it. Values are the base sell ' +\n 'price where the game gives one.

';\n\n /* item[] first, split by the inventory tab it appears under. */\n var buckets = {};\n for (var k in item) {\n var it = item[k];\n if (!it || !it.name || it.name === 'dummy') continue;\n var lab = MOD_WIKI_STYPE[it.stype] || 'Uncategorised';\n (buckets[lab] = buckets[lab] || []).push(it);\n }\n ['Usable', 'Other', 'Weapons', 'Equipment', 'Uncategorised'].forEach(function (lab) {\n var list = buckets[lab];\n if (!list || !list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(lab, list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n\n /* and the equipment objects, which live in their own globals */\n [['Weapons', wpn], ['Armour', eqp], ['Shields', sld], ['Accessories', acc]]\n .forEach(function (g) {\n var list = [];\n for (var kk in g[1]) {\n if (g[1][kk] && g[1][kk].name && g[1][kk].name !== 'dummy') list.push(g[1][kk]);\n }\n if (!list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(g[0], list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n return h;\n});\n\n/* --- 9. actions ----------------------------------------------------------- */\nMOD_wikiPage('actions', 'Actions', function () {\n var unlock = {};\n MOD_ACTION_UNLOCKS.forEach(function (u) {\n if (u.act) unlock[u.act.id] = (skl[u.skill] ? skl[u.skill].name : u.skill) +\n ' level ' + u.lv;\n });\n if (typeof act.mod_qi !== 'undefined') {\n unlock[act.mod_qi.id] = 'clearing the dojo\\'s Easiest, Easy and Normal fights';\n }\n\n var h = '

Actions

' +\n '

What you can be doing. One at a time, and only where the ' +\n 'action makes sense — unless \"Unrestricted actions\" is ticked in settings, ' +\n 'which lets several run at once anywhere. Both limits are deliberate; the ' +\n 'switch is there because they are not to everyone\\'s taste.

';\n\n var keys = [];\n for (var k in act) if (act[k] && act[k].name && act[k].name !== 'dummy') keys.push(k);\n keys.sort(function (a, b) { return act[a].name < act[b].name ? -1 : 1; });\n\n keys.forEach(function (k) {\n var a = act[k];\n var how = unlock[a.id];\n h += '
' +\n '

' + MOD_WIKI.safe(a.name) +\n (a.have === true ? 'unlocked'\n : 'locked') +\n (how ? 'from ' + MOD_WIKI.safe(how) + '' : '') +\n '

' + MOD_wikiDesc(a.desc) + '
';\n });\n return h;\n});\n\n/* --- 10. the dojo --------------------------------------------------------- */\nMOD_wikiPage('dojo', 'The dojo', function () {\n var h = '

The dojo

' +\n '

The instructor promises a reward every five levels. The base ' +\n 'game stops delivering at 30; the mod carries the same ladder to 110, on the ' +\n 'same terms.

' +\n '

Only the lowest rung you have not claimed is offered, which is how the ' +\n 'first six behave. Rewards are sized against the character experience curve ' +\n '(4×level3 + level2) — a level at 110 costs 5.3 ' +\n 'million, which is why the base game\\'s best pill is worth 0.3% of one there.

' +\n '' +\n '';\n\n MOD_DOJO_RUNGS.forEach(function (r) {\n var pill = item[r.pill];\n var extra = [];\n if (r.realmPill) extra.push('breakthrough pill for realm ' + r.realmPill);\n if (r.manual) extra.push('a choice of master\\'s manual');\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
LevelRewardCoinAlso
' + r.lv + '' + (pill ? MOD_WIKI.safe(pill.name) + ' ×' + r.n\n : '—') + '' + MOD_wikiNum(r.coin) + '' + (extra.join(', ') || '—') + '' + (global.flags[r.flag] ? 'claimed' : '') +\n '
' +\n\n '

Spirit pills

' +\n '

Character experience in a bottle. The four the mod adds continue the ' +\n 'game\\'s own series up to the new curve.

' +\n '' +\n '';\n MOD_PILLS.forEach(function (p) {\n h += '';\n });\n h += '
PillExperience
' + MOD_WIKI.safe(p[2]) + '' +\n p[3].toLocaleString() + '
' +\n\n '

Master\\'s manuals

' +\n '

A third grade of skillbook, offered at levels 50, 75 and 100 as a choice ' +\n 'the same way the first one was. Each is worth +' +\n Math.round(MOD_MANUAL_RATE * 100) + '% experience in its skill, permanently.

' +\n '' +\n '';\n MOD_MANUALS.forEach(function (m) {\n h += '';\n });\n h += '
ManualSkill
' + MOD_WIKI.safe(m[2]) + '' +\n MOD_WIKI.safe(skl[m[0]] ? (skl[m[0]].bname || skl[m[0]].name) : m[0]) +\n '
';\n return h;\n});\n\n/* --- 11. mechanics -------------------------------------------------------- */\nMOD_wikiPage('mechanics', 'Mechanics', function () {\n return '

Mechanics

' +\n\n '

Damage is subtractive, in both directions

' +\n '

A hit does the attacker\\'s strength minus the defender\\'s strength, plus ' +\n 'the weapon, plus one, before criticals. A defender\\'s STR is their ' +\n 'armour — there is no separate armour stat doing that job.

' +\n '

Two consequences worth carrying around:

' +\n '
    ' +\n '
  • Damage does not taper, it stops. Once a defender\\'s STR passes ' +\n 'yours, your hits do not get small; they land for nothing. Progress against ' +\n 'something too strong is not slow, it is zero.
  • ' +\n '
  • A stat ratio tells you nothing. Comparing your HP to an enemy\\'s ' +\n 'strength ignores the subtraction entirely and reads several times off in ' +\n 'either direction.
  • ' +\n '
' +\n '

This is also the reason enemies are never scaled by raising ' +\n 'their strength: it would raise their offence and their armour together, and ' +\n 'the second effect arrives first.

' +\n\n '

Criticals carry the late game

' +\n '

Critical rate reaches about a third at the top of the ladder and a ' +\n 'critical is roughly eight times a normal swing, so around three quarters of ' +\n 'all damage done at that point is critical damage. Anything that raises ' +\n 'critical rate or critical damage is worth more than it looks; Sharp Eye and ' +\n 'War are the two that do.

' +\n\n '

Hit chance

' +\n '

Landing a hit is a contest of agility. It is a ratio rather than a ' +\n 'subtraction, so agility behaves more gently than strength does — falling ' +\n 'behind on it costs you accuracy, not everything.

' +\n\n '

Skill sections and parents

' +\n '

Each of the ' + Object.keys(MOD_SECTIONS).length + ' sections has a parent ' +\n 'skill. A parent earns ' + Math.round(MOD_PARENT.feed * 100) + '% of everything ' +\n 'its children earn and grants no stats of its own. In return, a child below ' +\n 'the parent\\'s level trains ' + Math.round(MOD_PARENT.perLevel * 100) +\n '% faster per level of lag, up to ' + MOD_PARENT.maxGap + ' levels behind. ' +\n 'Bringing one section up makes every skill in it cheaper to catch up.

' +\n\n '

Saving

' +\n '

Three slots, switched from the bottom bar. The game builds its world once ' +\n 'at startup and cannot unload a save, so switching slots or starting a new ' +\n 'game reloads the page. Exporting a save before running anything unusual is ' +\n 'always the safe move.

';\n});\n\n/* --- the document ---------------------------------------------------------\n One string, no fetches, no dependencies: this has to work from file:// as\n readily as from a server.\n ------------------------------------------------------------------------- */\n\nfunction MOD_wikiCss() {\n return [\n ':root{color-scheme:dark}',\n '*{box-sizing:border-box}',\n /* Menlo and Consolas before the generic: with MS Gothic missing (any Mac)\n \"monospace\" is Courier, which Safari sets wider than Chrome does, and\n table cells wrapped on one and not the other. */\n 'body{margin:0;background:#060a1c;color:#dfe6f5;font:14px/1.6 \"MS Gothic\",' +\n '\"MS Mincho\",Menlo,Consolas,\"DejaVu Sans Mono\",monospace;display:flex;min-height:100vh}',\n 'a{color:#7cb0ff}',\n 'nav{width:210px;flex:0 0 210px;background:#081040;border-right:1px solid #46a;' +\n 'position:sticky;top:0;height:100vh;overflow:auto;padding:14px 0}',\n 'nav h2{font-size:13px;color:#9fb4d8;margin:0 0 10px 14px;letter-spacing:1px}',\n 'nav a{display:block;padding:6px 14px;text-decoration:none;color:#cfe0ff;' +\n 'border-left:3px solid transparent}',\n 'nav a:hover{background:#154080}',\n 'nav a.on{background:#123070;border-left-color:#7cb0ff;color:#fff}',\n 'nav .srch{margin:0 10px 12px;width:calc(100% - 20px);background:#050a20;' +\n 'color:#dfe6f5;border:1px solid #46a;padding:5px;font:inherit}',\n 'nav .xall{display:block;margin:12px 10px 0;color:#8296b8;font-size:12px;' +\n 'cursor:pointer;-webkit-user-select:none;user-select:none}',\n 'nav .xall input{vertical-align:-1px;margin-right:4px}',\n 'details.wk-g{border:1px solid #17224a;background:#0a1130;margin:8px 0}',\n 'details.wk-g>summary{cursor:pointer;padding:7px 12px;list-style:none;' +\n 'background:#0d1740}',\n 'details.wk-g>summary:hover{background:#154080}',\n 'details.wk-g>summary::-webkit-details-marker{display:none}',\n // a caret drawn rather than borrowed, so it is the same in every browser\n 'details.wk-g>summary::before{content:\"\\\\25B8\";color:#7cb0ff;' +\n 'display:inline-block;width:1em;transition:transform .12s}',\n 'details.wk-g[open]>summary::before{transform:rotate(90deg)}',\n 'details.wk-g>summary h2,details.wk-g>summary h3{display:inline;margin:0;' +\n 'font-size:15px;color:#fff}',\n '.wk-gi{padding:2px 12px 10px}',\n 'details.wk-g details.wk-g>summary h4{display:inline;margin:0;font-size:13px;' +\n 'color:#cfe0ff;font-weight:normal}',\n 'details.wk-g details.wk-g>summary{padding:5px 10px;background:#0b1435}',\n 'main{flex:1;min-width:0;padding:22px 30px 80px;max-width:1000px}',\n 'h1{font-size:26px;margin:0 0 6px;color:#fff;border-bottom:1px solid #46a;' +\n 'padding-bottom:8px}',\n 'h2{font-size:19px;color:#bcd4ff;margin:30px 0 8px}',\n 'h3{font-size:15px;color:#fff;margin:0 0 4px}',\n 'p.lede{color:#a8bcdc;margin:0 0 18px}',\n '.dim{color:#8296b8;font-weight:normal}',\n '.flav{font-size:12px;font-style:italic}',\n '.note{background:#0b1638;border-left:3px solid #46a;padding:8px 12px;' +\n 'color:#a8bcdc;margin:14px 0}',\n 'code,pre{background:#0b1638;border:1px solid #24356e;padding:1px 5px;' +\n 'color:#9fd0ff}',\n 'pre{padding:10px;overflow-x:auto}',\n 'dl dt{color:#fff;margin-top:14px}',\n 'dl dd{margin:2px 0 0 0;color:#a8bcdc}',\n 'ul{color:#a8bcdc}',\n 'table.wk-tbl{border-collapse:collapse;width:100%;margin:10px 0;' +\n 'display:block;overflow-x:auto}',\n 'table.wk-tbl th{text-align:left;color:#9fb4d8;font-weight:normal;' +\n 'border-bottom:1px solid #46a;padding:5px 9px;white-space:nowrap}',\n 'table.wk-tbl td{border-bottom:1px solid #17224a;padding:5px 9px;' +\n 'vertical-align:top}',\n 'table.wk-tbl tr.done td{color:#cfe0ff}',\n 'table.wk-tbl tr.here{background:#123070}',\n '.num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}',\n '.wk-e{border:1px solid #17224a;background:#081040;padding:9px 12px;' +\n 'margin:7px 0;border-radius:2px}',\n 'tr.wk-e{border:0;background:none}',\n '.wk-e.done{border-color:#2a4a8c}',\n '.wk-e h3{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}',\n '.lv{margin-left:auto;font-size:12px;color:#8296b8;font-weight:normal}',\n '.lv.capped{color:#ffd24a}',\n '.rank{font-size:12px;color:#8296b8;font-weight:normal}',\n '.desc{color:#a8bcdc;font-size:13px}',\n '.proj{color:#ffd24a;font-size:12px;margin-top:3px}',\n '.tag{font-size:11px;border:1px solid #46a;color:#9fb4d8;padding:0 5px;' +\n 'border-radius:2px;font-weight:normal}',\n '.tag.mod{border-color:#7a4ea8;color:#c07bff}',\n '.tag.done{border-color:#2e7d4f;color:#6fdc9a}',\n '.tag.here{border-color:#ffd24a;color:#ffd24a}',\n '.tag.warn{border-color:#a8562e;color:#ff9a5c}',\n 'table.wk-perks{border-collapse:collapse;margin:6px 0 0;width:100%}',\n 'table.wk-perks td{padding:2px 8px 2px 0;font-size:12px;color:#8296b8;' +\n 'border-bottom:1px solid #101a3c}',\n 'table.wk-perks tr.done td{color:#cfe0ff}',\n 'table.wk-perks td.tick{color:#6fdc9a;width:1em;text-align:right}',\n // fixed columns so the level bands line up between one area and the next\n 'table.wk-pop{table-layout:fixed}',\n 'table.wk-pop td:first-child{width:60%}',\n 'table.wk-pop td:nth-child(2){width:20%}',\n '.wk-title .tname{font-weight:bold}',\n '.wk-now{background:#0b1638;border:1px solid #24356e;padding:10px 14px;' +\n 'margin:0 0 18px;display:inline-block;min-width:320px}',\n '.wk-now h3{color:#9fb4d8;font-weight:normal;font-size:13px;' +\n 'letter-spacing:1px;margin-bottom:6px}',\n '.wk-now td{padding:2px 16px 2px 0}',\n '.wk-now td:last-child{color:#fff;text-align:right}',\n '.pg{display:none}.pg.on{display:block}',\n 'footer{color:#5a6c8c;font-size:12px;border-top:1px solid #17224a;' +\n 'margin-top:40px;padding-top:12px}',\n '@media(max-width:760px){body{display:block}nav{width:auto;height:auto;' +\n 'position:static;flex:none}main{padding:16px}}'\n ].join('\\n');\n}\n\n/* Filtering, page switching and the hash. Small enough to inline, and inlining\n it is the point — the document has to stand on its own. */\nfunction MOD_wikiJs() {\n return [\n 'var pages=[].slice.call(document.querySelectorAll(\".pg\"));',\n 'var links=[].slice.call(document.querySelectorAll(\"nav a[data-p]\"));',\n 'function show(id){',\n ' var found=false;',\n ' pages.forEach(function(p){var on=p.id===\"pg-\"+id;if(on)found=true;',\n ' p.className=\"pg\"+(on?\" on\":\"\")});',\n ' if(!found&&pages.length){pages[0].className=\"pg on\";id=pages[0].id.slice(3)}',\n ' links.forEach(function(a){a.className=a.dataset.p===id?\"on\":\"\"});',\n ' window.scrollTo(0,0);return id;}',\n 'links.forEach(function(a){a.addEventListener(\"click\",function(e){',\n ' e.preventDefault();location.hash=a.dataset.p;show(a.dataset.p)})});',\n 'window.addEventListener(\"hashchange\",function(){show(location.hash.slice(1))});',\n 'show(location.hash.slice(1)||\"start\");',\n // Search filters entries, hides groups left with none, and OPENS the groups\n // that still have some — a match hidden inside a collapsed group reads as\n // no match at all, which is worse than not having search.\n 'var box=document.getElementById(\"srch\");',\n 'var xall=document.getElementById(\"xall\");',\n 'var groups=[].slice.call(document.querySelectorAll(\"details.wk-g\"));',\n 'function filter(){',\n ' var q=box.value.trim().toLowerCase();',\n ' pages.forEach(function(p){',\n ' [].slice.call(p.querySelectorAll(\".wk-e\")).forEach(function(e){',\n ' e.style.display=(!q||e.textContent.toLowerCase().indexOf(q)>=0)?\"\":\"none\"})});',\n // Every page, not just the one on screen: the nav says how many entries\n // match in each, dims the ones with none, and Enter jumps to the first\n // page that has any. A prose page with no entries counts as one if its text\n // contains the query.\n ' links.forEach(function(a){',\n ' var pg=document.getElementById(\"pg-\"+a.dataset.p),n=0;',\n ' if(a.dataset.l===undefined)a.dataset.l=a.textContent;',\n ' if(q&&pg){var es=[].slice.call(pg.querySelectorAll(\".wk-e\"));',\n ' if(es.length)es.forEach(function(e){if(e.textContent.toLowerCase().indexOf(q)>=0)n++});',\n ' else if(pg.textContent.toLowerCase().indexOf(q)>=0)n=1}',\n ' a.dataset.n=n;a.style.opacity=(q&&!n)?\".4\":\"\";',\n ' a.textContent=a.dataset.l+(q&&n?\" (\"+n+\")\":\"\")});',\n ' groups.forEach(function(g){',\n ' var any=[].slice.call(g.querySelectorAll(\".wk-e\")).some(function(e){',\n ' return e.style.display!==\"none\"});',\n ' g.style.display=any?\"\":\"none\";',\n ' g.open=xall.checked||(!!q&&any)}); }',\n 'box.addEventListener(\"input\",filter);',\n 'box.addEventListener(\"keydown\",function(e){',\n ' if(e.key===\"Escape\"){box.value=\"\";filter()}',\n ' if(e.key===\"Enter\"&&box.value.trim()){',\n ' var cur=links.filter(function(a){return a.className===\"on\"})[0];',\n ' if(cur&&+cur.dataset.n)return;',\n ' var hit=links.filter(function(a){return +a.dataset.n>0})[0];',\n ' if(hit){location.hash=hit.dataset.p;show(hit.dataset.p)}}});',\n // Everything is collapsed by default because three of these pages run past\n // 40,000px open. This is the way back to one long document, which is what\n // the browser's own find needs.\n 'xall.addEventListener(\"change\",filter);'\n ].join('\\n');\n}\n\nfunction MOD_wikiBuild() {\n var nav = '', body = '';\n MOD_WIKI_PAGES.forEach(function (pg) {\n nav += '' +\n MOD_WIKI.safe(pg.label) + '';\n var inner;\n /* One page throwing must not take the wiki down with it — the page says so\n and the rest still renders. */\n try { inner = pg.build(); }\n catch (e) {\n inner = '

' + MOD_WIKI.safe(pg.label) + '

This page ' +\n 'could not be built: ' + MOD_WIKI.safe(e.message) + '

';\n console.warn('[mod] wiki page \"' + pg.id + '\" failed: ' + e.message);\n }\n body += '
' + inner + '
';\n });\n\n var stamp = new Date().toLocaleString();\n return '' +\n '' +\n '' + MOD_WIKI.safe(MOD_WIKI.title) + '' +\n '' +\n '' +\n '
' + body +\n '
Generated from the running game at ' + MOD_WIKI.safe(stamp) +\n ' — mod ' + MOD_WIKI.safe(MOD.version) + '. Everything here is read from the ' +\n 'live data, including your own save, so it cannot fall out of step with the ' +\n 'game. The game is truezhangwei\\'s; the mod is not endorsed by the author.' +\n '
' +\n ' inside content closing the block early, so that is the\n one thing scrubbed. */\n safe: function (h) {\n return String(h === undefined || h === null ? '' : h).replace(/<\\/script/gi, '<\\\\/script');\n }\n};\n\n/* Descriptions carry real HTML (coloured spans, the game's separator div), so\n they are passed through rather than escaped — but the separator becomes a\n rule, since a wiki row is not a tooltip. */\nfunction MOD_wikiDesc(d) {\n var s;\n try { s = (typeof d === 'function') ? d() : d; } catch (e) { s = ''; }\n if (!s) return '';\n s = String(s).split('
 
').join('
');\n return MOD_WIKI.safe(s);\n}\n\nfunction MOD_wikiNum(n) {\n if (!isFinite(n)) return '—';\n return Math.round(n).toLocaleString();\n}\n\n/* An effect line at a level the player has not reached yet. The effect\n functions read the skill they are handed, so the level is borrowed and put\n back; anything that throws simply gets no projection. */\nfunction MOD_wikiEffectAt(key, sk, lvl) {\n var eff = MOD_EFFECTS[key];\n if (!eff) return '';\n var was = sk.lvl, out = '';\n try { sk.lvl = lvl; out = eff(sk) || ''; }\n catch (e) { out = ''; }\n finally { sk.lvl = was; }\n return MOD_WIKI.safe(out);\n}\n\n/* --- page builders --------------------------------------------------------\n Each returns the inner HTML of one page. `wk-e` marks a searchable entry and\n `wk-g` a group that hides itself when all its entries are filtered out.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_PAGES = [];\n\nfunction MOD_wikiPage(id, label, build) {\n MOD_WIKI_PAGES.push({ id: id, label: label, build: build });\n}\n\n/* A collapsible group.\n\n Not decoration: with every skill, item and title listed, three of the eleven\n pages ran past 40,000 pixels — a hundred screens of scrolling, which is the\n same as not having the content at all. Collapsed, each of those pages opens\n as an index you can read in one screen and expand where you want.\n\n
rather than a click handler, so it still works with JavaScript off,\n the browser's own find can be made to reach inside it, and the summary is\n keyboard-reachable for free. `level` is the heading level for the summary,\n since a group is sometimes a section of a page and sometimes a subsection. */\nfunction MOD_wikiGroup(heading, count, inner, level) {\n var hn = 'h' + (level || 2);\n return '
<' + hn + '>' + heading +\n (count !== null && count !== undefined\n ? ' (' + count + ')' : '') +\n '
' + inner + '
';\n}\n\n/* --- 1. orientation ------------------------------------------------------- */\nMOD_wikiPage('start', 'Start here', function () {\n var cap = MOD_levelCap();\n var realm = MOD_realm();\n var h = '

proto23

' +\n '

An idle RPG by truezhangwei, plus a mod. You train skills by ' +\n 'doing things, the things you can do open up as the story does, and none of ' +\n 'it is in a hurry.

' +\n\n '

Where you are right now

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Character level' + MOD_wikiNum(you.lvl) + '
Skill level cap' + cap + ' — ' +\n MOD_WIKI.safe(MOD_CAP.name) + '
Cultivation realm' + MOD_WIKI.safe(realm.name) +\n ' (' + realm.n + ' of ' + (MOD_REALMS.length - 1) + ')
Titles held' + MOD_titleCount() + ' — renown ' +\n (skl.rnwn ? skl.rnwn.lvl : 0) + '
Coin' + MOD_wikiNum(you.wealth || 0) + '
' +\n\n '

The four things worth knowing early

' +\n '
' +\n '
Your skill levels are capped by the story, not by grinding.
' +\n '
The cap starts at 10 and ends at 110, and it moves when you reach a new ' +\n 'place — not when you earn enough experience. If a skill has stopped ' +\n 'levelling, you are not doing it wrong; you need to go somewhere. See ' +\n 'Progression.
' +\n\n '
Armour is subtraction, so levels matter more than they look.
' +\n '
A hit does the attacker\\'s STR minus the defender\\'s STR. That means a ' +\n 'small gap in strength is a large gap in damage, and a big enough one takes ' +\n 'your damage to nothing at all rather than merely reducing it. See ' +\n 'Mechanics.
' +\n\n '
Most skills have a parent that pulls them along.
' +\n '
Every section has a parent skill that earns a share of everything its ' +\n 'children do, and any child lagging behind the parent trains faster — up to ' +\n 'twice as fast at ' + MOD_PARENT.maxGap + ' levels behind. A neglected skill is ' +\n 'cheap to bring up later, which is the only reason having this many is ' +\n 'workable.
' +\n\n '
Cultivation is a second ladder, and it is not automatic.
' +\n '
Circulating qi levels a skill; the skill does not raise your realm. ' +\n 'Reaching the level opens a bottleneck, and breaking through it costs a pill ' +\n 'and can fail. See Cultivation.
' +\n '
' +\n\n '

What is the mod and what is the game

' +\n '

The game is index.html and it is entirely truezhangwei\\'s ' +\n 'work. Everything in mod.js is a modification of it and is not ' +\n 'endorsed by the author. Pages here mark added content with ' +\n 'mod where the distinction matters. The full ' +\n 'list of changes is in the changelog, on the bottom bar.

';\n return h;\n});\n\n/* --- 2. progression ------------------------------------------------------- */\nMOD_wikiPage('progress', 'Progression', function () {\n var cap = MOD_levelCap();\n var h = '

Progression

' +\n '

Skill levels are capped by how far the story has gone. Ten ' +\n 'rungs, 10 through 110. The cap applies to every skill at once; Renown is ' +\n 'the single exception, since it is counted rather than trained.

' +\n '' +\n '';\n\n MOD_TIERS.forEach(function (t) {\n var got = null;\n try { got = MOD_tierFlag(t); } catch (e) {}\n var here = (t.cap === cap);\n var need = t.needText ? t.needText\n : (t.flags.length ? t.flags.join(' or ') : 'the start of the game');\n h += '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
CapStageWhat opens it
' + t.cap + '' + MOD_WIKI.safe(t.name) + '' + MOD_WIKI.safe(need) + '' + (here ? 'you are here'\n : got ? 'reached' : '') + '
' +\n\n '

The experience curve

' +\n '

The game\\'s own skill curve is super-exponential — level 109 to 110 ' +\n 'alone costs ' + MOD_wikiNum(MOD_vanillaExpnext(109)) + ' experience, so the top of the ladder ' +\n 'is unreachable by a factor of about ten trillion. But the author only ever ' +\n 'designed skills up to level 15, and below that his curve is fine.

' +\n (MOD_XP.vanillaTo > 0\n ? '

So the mod keeps it. Below level ' + MOD_XP.vanillaTo + ' a skill costs ' +\n 'exactly what the original charges; from there it rises by ×' +\n MOD_XP.ratio.toFixed(4) + ' a level, a ratio solved so the climb to 110 takes ' +\n 'the same time it always did.'\n : '

You have chosen the Mod before 4.2 pacing: a plain geometric curve, ' +\n '×' + MOD_XP.ratio.toFixed(3) + ' a level from level 0, much faster than the ' +\n 'original early on. The Original pacing in settings puts back the ' +\n 'author\\'s own cost below level 10.') +\n ' Character level uses the game\\'s own curve, untouched.

' +\n '' +\n '';\n [1, 5, 9, 10, 15, 25, 50, 75, 90, 109].forEach(function (l) {\n h += '';\n });\n h += '
LevelExperience for the nextOriginal game
' + l +\n '' + MOD_wikiNum(MOD_expnextFor(l)) + '' +\n MOD_wikiNum(MOD_vanillaExpnext(l)) + '
';\n return h;\n});\n\n/* --- 3. skills ------------------------------------------------------------ */\nMOD_wikiPage('skills', 'Skills', function () {\n var cap = MOD_levelCap();\n var bySection = {};\n var keys = [];\n for (var k in skl) {\n var s = skl[k];\n if (!s || typeof s !== 'object' || !s.name) continue;\n keys.push(k);\n }\n keys.forEach(function (k) {\n var s = skl[k];\n var sec = MOD_sectionOf(s);\n (bySection[sec] = bySection[sec] || []).push(k);\n });\n\n var h = '

Skills

' +\n '

' + keys.length + ' skills in ' +\n Object.keys(MOD_SECTIONS).length + ' sections. Every one has perks at ' +\n MOD_LADDER.join(', ') + ', and every one that has milestones grants titles at ' +\n MOD_TITLE_RUNGS.join(', ') + '. Levels and ticks below are yours.

';\n\n Object.keys(MOD_SECTIONS).forEach(function (sec) {\n var list = bySection[sec];\n if (!list || !list.length) return;\n var parentKey = MOD_PARENT_KEY[sec];\n var g = '';\n if (parentKey && skl[parentKey]) {\n g += '

Parent skill: ' +\n MOD_WIKI.safe(skl[parentKey].bname || skl[parentKey].name) +\n ' — earns ' + Math.round(MOD_PARENT.feed * 100) +\n '% of what its children earn, and speeds up any child that lags behind it.

';\n }\n list.sort(function (a, b) { return skl[a].name < skl[b].name ? -1 : 1; });\n list.forEach(function (k) {\n var s = skl[k];\n var lvl = Number(s.lvl) || 0;\n g += '
' +\n '

' + MOD_WIKI.safe(s.bname || s.name) +\n (s.bname ? ' (' + MOD_WIKI.safe(s.name) + ')' : '') +\n '= cap ? ' capped' : '') + '\">lv ' + lvl +\n (lvl >= cap ? ' — at the cap' : '') + '

' +\n '
' + MOD_wikiDesc(s.desc) + '
';\n\n var at110 = MOD_wikiEffectAt(k, s, 110);\n if (at110) g += '
At level 110: ' + at110 + '
';\n\n if (s.mlstn && s.mlstn.length) {\n g += '';\n s.mlstn.forEach(function (m) {\n g += '' +\n '' +\n '' +\n '';\n });\n g += '
' + m.lv + '' + MOD_WIKI.safe(m.p || '—') + '' + (m.g === true ? '✓' : '') + '
';\n }\n g += '
';\n });\n h += MOD_wikiGroup(MOD_WIKI.safe(MOD_SECTIONS[sec]), list.length, g, 2);\n });\n return h;\n});\n\n/* --- 4. cultivation ------------------------------------------------------- */\nMOD_wikiPage('cultivation', 'Cultivation', function () {\n var now = MOD_realm();\n var elig = 0;\n try { elig = MOD_realmEligible().n; } catch (e) {}\n var qic = skl.qic ? skl.qic.lvl : 0;\n\n var h = '

Cultivation

' +\n '

Ten realms above Mortal. Circulating qi levels the Qi ' +\n 'Circulation skill; the skill does not advance the realm. Reaching the level ' +\n 'opens a bottleneck, and breaking through it costs a pill and can fail — the ' +\n 'pill is spent either way.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Realm' + MOD_WIKI.safe(MOD_realmTitle()) + '
Spiritual Root' + MOD_WIKI.safe(MOD_root().name) + '
Qi Circulationlevel ' + qic + '
Insight' + MOD_insight() + '
Bottleneck open' +\n (elig > now.n ? 'yes — ' + MOD_WIKI.safe(MOD_REALMS[now.n + 1].name) +\n ' is within reach' : 'no') + '
Qi Deviation' + (MOD_deviated()\n ? MOD_deviationLeft() + ' in-game minutes left' : 'no') + '
' +\n '

Realm thresholds are the same levels that earn a title rank, so realm N ' +\n 'is the level at which a rank N title arrives. Every realm multiplies all ' +\n 'stats and health, and the multipliers do not stack with each other — the ' +\n 'realm you are in is the one that applies.

' +\n '' +\n '';\n\n MOD_REALMS.forEach(function (r) {\n var pill = r.n >= 1 ? item['mod_bp' + r.n] : null;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmNameQi Circ.StatsHealthPill
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + r.qic + '×' + r.mult + '×' + r.hp + '' + (pill ? MOD_WIKI.safe(pill.name) : '—') +\n '
' +\n\n '

Where the pills come from

' +\n '

Every breakthrough pill has a source in normal play. None of them are ' +\n 'craftable, and none are cheap — a bottleneck you can buy in bulk is not a ' +\n 'bottleneck.

' +\n '' +\n '' +\n '' +\n '' +\n '
RealmsSource
1The dojo instructor, when qi ' +\n 'circulation is first taught
2–5The Herbalist in the village ' +\n '(vendor.pha1), restocked
6–10The dojo\\'s Level Advancement ' +\n 'ladder, and the Pill Tower
' +\n '

Realm 1 is deliberately not sold at the marketplace: ' +\n 'the marketplace is behind a 40%-a-visit chance encounter, and realm 1 opens ' +\n 'long before that is reliable.

' +\n\n '

Breaking through

' +\n '

A bottleneck wants three things, which is what the genre says it wants: ' +\n 'a pill, insight, and enough consolidation to make the ' +\n 'attempt worth making. The pill and the insight are both spent whether the ' +\n 'attempt works or not.

' +\n '

The odds improve the further past the threshold you are, plus your root, ' +\n 'plus time sat in Closed Door Training, and cap short of certain.

' +\n '' +\n '';\n [0, 2, 4, 6, 8].forEach(function (o) {\n var base = Math.min(0.55 + o * 0.05 + MOD_root().odds, 0.95);\n h += '';\n });\n h += '
Levels past the thresholdChanceAfter full seclusion
+' + o + '' +\n Math.round(base * 100) + '%' +\n Math.round(Math.min(base + MOD_CULT.secludeOdds, 0.95) * 100) + '%
' +\n\n '

Insight

' +\n '

Insight is the half of the price that cannot be bought. It comes from ' +\n 'three places, and they are the three the genre names: meditating, fights you ' +\n 'nearly lost, and going somewhere you have not been.

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
SourceRate
Circulate Qi, at a wall' +\n (MOD_CULT.meditateChance * 100).toFixed(1) + '% a second
Closed Door Training' +\n (MOD_CULT.secludeChance * 100).toFixed(1) + '% a second
A kill made under ' +\n Math.round(MOD_CULT.brinkFrac * 100) + '% health — ' +\n (MOD_FIGHTS.mode === 'original'\n ? 'the author\\'s areas rarely come close once you reach them; the three past ' +\n 'the golem arena and the rank duels do, within a few fights, as does an area ' +\n 'you walk into early'\n : 'every fight costs a share of your health, so a few in a row without resting') +\n '' +\n Math.round(MOD_CULT.brinkChance * 100) + '% a kill
Entering an area for the first timealways
' +\n '

The insight a realm asks for is sized to the climb to it: about ' +\n Math.round(MOD_CULT.wallShare * 100) + '% of the Circulate Qi time the climb took, ' +\n 'spent at the wall. Seclusion earns it three times as fast, and full ' +\n 'consolidation takes as long as that on average.

' +\n '' +\n '' +\n '';\n var wkHours = function (sec) {\n return sec >= 7200 ? (sec / 3600).toFixed(sec >= 36000 ? 0 : 1) + ' h' : Math.round(sec / 60) + ' min';\n };\n MOD_REALMS.forEach(function (r) {\n if (!r.n) return;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmClimb (Circulate Qi, 1x)Insight to enterFull seclusionQi Deviation (in-game)
' + MOD_WIKI.safe(r.name) + '' + wkHours(MOD_realmClimbSec(r.n)) + '' + MOD_insightNeed(r.n) + '' + wkHours(MOD_secludeFull(r.n)) + '' + (MOD_devMinutes(r.n) / 1440).toFixed(1) + ' days
' +\n\n '

The nine layers

' +\n '

Each realm is nine layers deep, derived from where Qi Circulation sits ' +\n 'between this realm\\'s threshold and the next one\\'s — so breaking through ' +\n 'puts you at the first layer of the new realm on its own. Layers are worth ' +\n '×' + MOD_CULT.layerMult + ' stats and ×' + MOD_CULT.layerBody +\n ' body each above the first, and the top realm has no rung above it to ' +\n 'measure against, so it stays at its first layer.

' +\n '' +\n '';\n for (var li = 1; li <= MOD_CULT.layers; li++) {\n h += '';\n }\n h += '
LayerCalled
' +\n li + '' + MOD_LAYER_ORD[li] + ' layer, ' +\n MOD_WIKI.safe(MOD_layerStage(li)) + '
' +\n '

At the threshold of the next realm and before breaking ' +\n 'through, you are a half step to it — the genre\\'s own name for the ' +\n 'state this mod calls a bottleneck.

' +\n\n '

Closed Door Training

' +\n '

Seclusion does two things. It consolidates — a full sitting ' +\n '(the table above) is worth +' + Math.round(MOD_CULT.secludeOdds * 100) +\n '% on the attempt — and it shelters: a breakthrough attempted while ' +\n 'you are still sitting in it cannot end in Qi Deviation, only in a lost ' +\n 'pill. It will not start unless you are actually at a wall.

' +\n\n '

Qi Deviation

' +\n '

Failing a breakthrough out in the open leaves your cultivation base ' +\n 'unstable for as long as the seclusion you skipped (the table above, never ' +\n 'under ' + MOD_CULT.devMinutes + ' in-game minutes): stats at ×' +\n MOD_CULT.devStat + ', body at ×' + MOD_CULT.devBody + ', and no second ' +\n 'attempt until it passes. It clears on its own, or at once with a ' +\n 'Qi Settling Pill. It never kills — the death system stays out of it.

' +\n\n '

Heavenly Tribulation

' +\n '

From realm ' + MOD_CULT.tribFrom + ' up, a successful breakthrough is ' +\n 'answered. The cost is a fraction of your maximum health, so it is a gate on ' +\n 'walking in healthy rather than a damage race; the realm bonus you are ' +\n 'standing on is what pays it. Fall to it and the realm does not open.

' +\n '' +\n '';\n MOD_REALMS.forEach(function (r) {\n if (!r.n || r.n < MOD_CULT.tribFrom) return;\n h += '' +\n '' +\n '';\n });\n h += '
RealmBoltsOf your max HP
' + MOD_WIKI.safe(r.name) + '' + MOD_tribulationBolts(r.n) + '' + Math.round(MOD_tribulationCost(r.n) * 100) + '%
' +\n\n '

Spiritual Root

' +\n '

Rolled once, when the dojo finishes teaching you to fight. It is never a ' +\n 'dead end: the worst grade costs cultivation speed, not access, and a ' +\n 'Root Cleansing Pill moves you up a grade.

' +\n '' +\n '';\n var rootTotal = 0;\n MOD_ROOTS.forEach(function (r) { rootTotal += r.weight; });\n MOD_ROOTS.forEach(function (r) {\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
GradeRootChanceCultivationBreakthrough
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + Math.round(r.weight / rootTotal * 100) + '%×' + r.xp + '' + (r.odds >= 0 ? '+' : '') +\n Math.round(r.odds * 100) + '%
';\n return h;\n});\n\n/* --- 5. elemental mastery ------------------------------------------------- */\nMOD_wikiPage('mastery', 'Elemental mastery', function () {\n var h = '

Elemental mastery mod

' +\n '

Six masteries, each with a technique that fires on its own ' +\n 'during a fight. Combat is automatic and there is no ability picker, so a ' +\n 'technique is a chance on each of your swings to become something else — one ' +\n 'that scales with INT and the element rather than with your weapon.

' +\n '

Nothing fires while you are still Mortal: the channels have to be open ' +\n 'first. Chance is ' + (MOD_TECH.perLevel * 100).toFixed(1) + '% per mastery ' +\n 'level plus ' + (MOD_TECH.perRealm * 100).toFixed(1) + '% per realm, capped at ' +\n Math.round(MOD_TECH.cap * 100) + '%; a technique lands for ×' +\n MOD_TECH.power + ' of a normal strike.

' +\n '' +\n '';\n\n var realm = 0;\n try { realm = MOD_realm().n; } catch (e) {}\n MOD_MASTERY.forEach(function (m) {\n var lvl = Number(m.skill.lvl) || 0;\n var ch = realm < 1 ? 0\n : Math.min(lvl * MOD_TECH.perLevel + realm * MOD_TECH.perRealm, MOD_TECH.cap);\n h += '' +\n '' +\n '' +\n '';\n });\n h += '
MasteryTechniqueLevelChance now
' +\n MOD_WIKI.safe(m.skill.name) + '' + MOD_WIKI.safe(m.name) + '' + lvl + '' + (realm < 1 ? 'Mortal'\n : (ch * 100).toFixed(1) + '%') + '
';\n return h;\n});\n\n/* --- 6. titles ------------------------------------------------------------ */\nMOD_wikiPage('titles', 'Titles', function () {\n var perm = MOD_permanentRank();\n var held = MOD_titleCount();\n var total = 0; for (var kk in ttl) if (ttl[kk] && ttl[kk].name) total++;\n\n var h = '

Titles

' +\n '

A title\\'s rank runs 1 to 10 and is derived from the level ' +\n 'that earns it, so a rank is a statement about where in the game you are. ' +\n 'Only the title you are wearing applies — until renown makes the lower ranks ' +\n 'permanent.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '
Titles held' + held + ' of ' + total + '
Renown level' + (skl.rnwn ? skl.rnwn.lvl : 0) +\n ' of ' + MOD_RENOWN_MAX + '
Ranks that apply without wearing' +\n (perm >= 1 ? '1–' + perm : 'none yet') + '
' +\n\n '

Ranks

' +\n '';\n MOD_RANK_AT.forEach(function (lv, i) {\n var st = MOD_rankStyle(i + 1);\n h += '' +\n '';\n });\n h += '
RankEarned from levelColour
' + (i + 1) + '' + lv + '' + '●●●' + '
' +\n\n '

Renown

' +\n '

Renown is not trained. It counts the titles you hold, and each level ' +\n 'multiplies every stat by ' + (1 + MOD_RENOWN_RATE) + ' — compounding, so ' +\n 'level ' + MOD_RENOWN_MAX + ' is ×' +\n (Math.round(MOD_renownMult(MOD_RENOWN_MAX) * 1000) / 1000) + '. At renown N, ' +\n 'every title of rank N or below applies permanently.

' +\n '' +\n '';\n MOD_RENOWN_STEPS.forEach(function (n, i) {\n h += '= n ? 'done' : '') + '\">' +\n '';\n });\n h += '
RenownTitles neededAll statsPassive ranks
' + (i + 1) +\n '' + n + '×' +\n (Math.round(MOD_renownMult(i + 1) * 1000) / 1000) + '1–' + (i + 1) + '
' +\n\n '

Every title

' +\n '

Grouped by the skill that grants it. What is left is either ' +\n 'a ladder of its own — creatures killed, items gathered, the ' +\n 'cultivation realms — or a one-off from the story.

';\n\n var groups = {}, fams = {}, loose = [];\n for (var k in ttl) {\n var t = ttl[k];\n /* Unlike the picker, the wiki KEEPS the author's broken entries and says\n so — same call as area.clg, which is listed and tagged \"nothing spawns\"\n rather than quietly dropped. A reader looking for why a title never\n appears is better served by the row than by its absence. */\n if (!t || t.name === undefined || t.name === null) continue;\n var sk = MOD_TITLE_SKILL[k];\n if (sk && skl[sk]) { (groups[sk] = groups[sk] || []).push(k); continue; }\n var stem = MOD_titleStem(k);\n if (stem) { (fams[stem] = fams[stem] || []).push(k); continue; }\n loose.push(k);\n }\n /* Same rule as the picker: a family of one is not a family. */\n Object.keys(fams).forEach(function (stem) {\n if (fams[stem].length < 2) { loose.push(fams[stem][0]); delete fams[stem]; }\n });\n var order = Object.keys(groups).sort(function (a, b) {\n return skl[a].name < skl[b].name ? -1 : 1;\n });\n\n var row = function (k) {\n var t = ttl[k];\n var st = MOD_rankStyle(t.rar);\n var gen = null;\n for (var i = 0; i < MOD_SKILL_TITLES.length; i++) {\n if (MOD_SKILL_TITLES[i].key === k) { gen = MOD_SKILL_TITLES[i]; break; }\n }\n /* The author left a handful unfinished: ttl.ddcd carries the literal string\n \"null\" for both name and description, and shpt2/shpt3/mone3 are blank.\n None is granted by anything, so none can be earned. */\n var broken = !t.name || t.name === 'null';\n return '
' +\n '' +\n MOD_WIKI.safe(t.name || '(unnamed)') + '' +\n (broken ? 'unfinished' : '') +\n 'rank ' + (t.rar || 1) + '' +\n (gen ? 'lv ' + gen.rung + ' · +' +\n Math.round(gen.xp * 100) + '% ' + MOD_WIKI.safe(skl[gen.skill].name) +\n ' exp' : '') +\n (t.have === true ? 'held' : '') +\n '
' + MOD_wikiDesc(t.desc) + '
';\n };\n\n var byRank = function (a, b) { return (ttl[a].rar || 1) - (ttl[b].rar || 1); };\n order.forEach(function (sk) {\n groups[sk].sort(byRank);\n h += MOD_wikiGroup(MOD_WIKI.safe(skl[sk].bname || skl[sk].name),\n groups[sk].length, groups[sk].map(row).join(''), 3);\n });\n /* The ladders that belong to no skill, by the same derivation the picker\n uses — so the two cannot drift apart. Sorted by the label, since that is\n what the reader scans. */\n var famList = Object.keys(fams)\n .map(function (stem) {\n fams[stem].sort(byRank);\n return { stem: stem,\n label: MOD_titleFamilyLabel(stem, fams[stem].map(function (k) { return ttl[k]; })) };\n })\n .sort(function (a, b) { return a.label < b.label ? -1 : a.label > b.label ? 1 : 0; });\n if (famList.length) {\n /* Nested one level down rather than sitting beside the skills. Eleven more\n top-level summaries cost ~530px on a page that is already the tallest in\n the wiki, and these are a different KIND of thing from a skill anyway. */\n var famInner = '', famTotal = 0;\n famList.forEach(function (f) {\n famTotal += fams[f.stem].length;\n famInner += MOD_wikiGroup(MOD_WIKI.safe(f.label), fams[f.stem].length,\n fams[f.stem].map(row).join(''), 4);\n });\n h += MOD_wikiGroup('Ladders of their own', famTotal, famInner, 3);\n }\n if (loose.length) {\n loose.sort(byRank);\n /* The story titles are the one group with no skill to divide them, and\n there are enough of them to need dividing. Rank rather than the alphabet:\n it is the axis they are already sorted on, and the one a reader cares\n about — a rank is a statement about where in the game a title comes from. */\n var inner;\n if (loose.length <= MOD_WIKI_ALPHA_AT) {\n inner = loose.map(row).join('');\n } else {\n var byRankNo = {};\n loose.forEach(function (k) {\n var r = Math.min(Math.max(Number(ttl[k].rar) || 1, 1), 10);\n (byRankNo[r] = byRankNo[r] || []).push(k);\n });\n inner = '';\n Object.keys(byRankNo).sort(function (a, b) { return a - b; }).forEach(function (r) {\n var st = MOD_rankStyle(r);\n inner += MOD_wikiGroup('Rank ' + r + '', byRankNo[r].length,\n byRankNo[r].map(row).join(''), 4);\n });\n }\n h += MOD_wikiGroup('From the story', loose.length, inner, 3);\n }\n return h;\n});\n\n/* --- 7. areas ------------------------------------------------------------- */\nMOD_wikiPage('areas', 'Areas & enemies', function () {\n var h = '

Areas & enemies

' +\n '

Every area, what lives in it and at what levels, and what it ' +\n 'drops. Enemy strength is not read off these levels — see the note below the ' +\n 'table.

';\n\n /* Two of the game's 21 areas are not places and listing them misleads:\n nwh \"Somewhere\" — where global.current_z parks whenever you are NOT in\n a fight, populated by creature.default (\"Nothing\").\n tst \"Test\" — reached only from chss.tst, which has id -1 and is in no\n sector, so nothing in the game can travel there.\n The test for \"is this a place\" is whether you can get to it, and neither\n can be. Everything else is listed whether or not you have been. */\n var MOD_WIKI_NOT_PLACES = { nwh: 1, tst: 1 };\n\n var keys = [];\n for (var k in area) {\n if (area[k] && area[k].pop && !MOD_WIKI_NOT_PLACES[k]) keys.push(k);\n }\n /* Ordered by the middle of the level band, which is the closest thing the\n game has to a difficulty order. */\n keys.sort(function (a, b) {\n var mid = function (z) {\n if (!z.pop.length) return 0;\n var s = 0; z.pop.forEach(function (e) { s += (e.lvlmin + e.lvlmax) / 2; });\n return s / z.pop.length;\n };\n return mid(area[a]) - mid(area[b]);\n });\n\n keys.forEach(function (k) {\n var z = area[k];\n /* An area whose baked bands are not finite can never spawn anything — see\n the note rendered under its table. Derived rather than listed by name, so\n it stays true if the game changes. */\n var broken = !z.popc || z.popc.length !== z.pop.length ||\n z.popc.some(function (band) { return !band || !isFinite(band[1] - band[0]); });\n h += '

' + MOD_WIKI.safe(z.name) +\n ' ' + k + '' +\n (broken ? 'nothing spawns' : '') +\n '' + (z.size === -1 ? 'endless' : z.size + ' to clear') +\n '

';\n if (z.pop.length) {\n /* The spawn share is NOT pop[i].c. z_bake normalises those weights into\n area.popc — a list of [lo,hi] bands that mon_gen rolls a uniform\n random against — and the weights do not have to sum to 1, so they are\n off wherever they don't. The Southern forest's .35/.45/.25 are really\n 33/43/24%. Read the bands the game actually rolls against. */\n var bands = z.popc || [];\n h += '';\n z.pop.forEach(function (e, i) {\n var c = e.crt || {};\n var band = bands[i];\n var share = band ? band[1] - band[0] : NaN;\n h += '' +\n '' +\n '';\n });\n h += '
' + MOD_WIKI.safe(c.name || '?') + 'lv ' + e.lvlmin +\n (e.lvlmax !== e.lvlmin ? '–' + e.lvlmax : '') + '' + (isFinite(share)\n ? Math.round(share * 100) + '% of spawns'\n : '—') + '
';\n if (broken) {\n h += '
Nothing spawns here. The area\\'s entries carry ' +\n 'no spawn weight, so the table the game rolls against comes out as ' +\n 'NaN and no creature can ever match it. A bug in the base game, left ' +\n 'as it is — nothing in the game travels to this area either.
';\n }\n }\n if (z.drop && z.drop.length) {\n var d = z.drop.map(function (x) {\n return MOD_WIKI.safe((x.item && x.item.name) || '?') +\n ' ' + ((x.c || 0) * 100).toFixed(2) + '%';\n }).join(', ');\n h += '
Drops: ' + d + '
';\n }\n h += '
';\n });\n\n var fightsOrig = MOD_FIGHTS.mode === 'original';\n h += '

How hard an enemy is

' +\n '

It depends on the Fights setting, which is ' +\n (fightsOrig ? 'Original' : 'Scaled') + ' now. On Original (the default) ' +\n 'every area the author wrote is exactly as he wrote it: his creatures, his ' +\n 'level ranges, and you out-grow them as you do in his game. On Scaled every ' +\n 'area is sized to you. The three areas past the golem arena and the rank ' +\n 'duels are sized to you either way.

' +\n '

Sizing to you works like this. Enemies are not fitted to a level. Each spawn is solved against your ' +\n 'actual power at the moment it appears, aiming at roughly ' + MOD_ENEMY.kill +\n ' swings to kill it and ' + MOD_ENEMY.die + ' before it kills you, with a ' +\n 'guaranteed margin between the two so a fight is never a coin flip. Variety ' +\n 'comes from the area\\'s level band and each creature\\'s own stat weighting, ' +\n 'not from difficulty scaling.

' +\n '

The reason for that: within the first stage alone you go from 1 STR to ' +\n 'around 265, so anything anchored to a level is either trivial at the end of ' +\n 'a stage or impossible at the start of one.

';\n return h;\n});\n\n/* --- 8. items -------------------------------------------------------------\n The one place where \"list everything\" needs help: 321 of the game's 371\n plain items are the same inventory type, so grouping by type alone leaves a\n single group of 321. Anything past MOD_WIKI_ALPHA_AT gets an alphabetical\n sub-split as well — the oldest index pattern there is, and derived rather\n than invented, which a hand-made taxonomy would not be.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_ALPHA_AT = 60;\n\n/* The game's own inventory tabs (isort): ALL / WPN / EQP / USE / OTHER. Using\n its categories rather than new ones means a group here is a tab there. */\nvar MOD_WIKI_STYPE = { 2: 'Weapons', 3: 'Equipment', 4: 'Usable', 5: 'Other' };\n\nfunction MOD_wikiItemRow(it) {\n var val = it.v !== undefined ? it.v : (it.val !== undefined ? it.val : null);\n return '
' +\n '

' + MOD_WIKI.safe(it.name) +\n (it.rar > 1 ? 'rarity ' + it.rar + '' : '') +\n (val !== null ? '' + MOD_wikiNum(val) + ' coin' : '') +\n (it.amount > 0 ? '×' + it.amount + '' : '') +\n '

' + MOD_wikiDesc(it.desc) + '
';\n}\n\n/* The letter a name files under. Several items are quoted — the master's\n manuals are literally named \"Sword Saint Manual\" — and filing those under a\n quotation mark is no use to anyone looking for S. */\nfunction MOD_wikiLetter(name) {\n var m = String(name || '').replace(/^[^0-9A-Za-z]+/, '');\n return (m.charAt(0) || '?').toUpperCase();\n}\n\nfunction MOD_wikiByLetter(a, b) {\n var x = MOD_wikiLetter(a.name) + String(a.name || '').toLowerCase();\n var y = MOD_wikiLetter(b.name) + String(b.name || '').toLowerCase();\n return x < y ? -1 : (x > y ? 1 : 0);\n}\n\n/* Flat below the threshold, alphabetically sub-grouped above it. Ranges are cut\n from the entries actually present, so they stay even as content is added. */\nfunction MOD_wikiAlpha(entries, render, level) {\n if (entries.length <= MOD_WIKI_ALPHA_AT) return entries.map(render).join('');\n var per = Math.ceil(entries.length / Math.ceil(entries.length / 40));\n var out = '';\n for (var i = 0; i < entries.length; i += per) {\n var chunk = entries.slice(i, i + per);\n var first = MOD_wikiLetter(chunk[0].name);\n var last = MOD_wikiLetter(chunk[chunk.length - 1].name);\n out += MOD_wikiGroup(first === last ? first : first + '–' + last,\n chunk.length, chunk.map(render).join(''), level || 4);\n }\n return out;\n}\n\nMOD_wikiPage('items', 'Items', function () {\n var h = '

Items

' +\n '

Everything the game defines, whether or not you have seen ' +\n 'it. Grouped the way your inventory groups it. Values are the base sell ' +\n 'price where the game gives one.

';\n\n /* item[] first, split by the inventory tab it appears under. */\n var buckets = {};\n for (var k in item) {\n var it = item[k];\n if (!it || !it.name || it.name === 'dummy') continue;\n var lab = MOD_WIKI_STYPE[it.stype] || 'Uncategorised';\n (buckets[lab] = buckets[lab] || []).push(it);\n }\n ['Usable', 'Other', 'Weapons', 'Equipment', 'Uncategorised'].forEach(function (lab) {\n var list = buckets[lab];\n if (!list || !list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(lab, list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n\n /* and the equipment objects, which live in their own globals */\n [['Weapons', wpn], ['Armour', eqp], ['Shields', sld], ['Accessories', acc]]\n .forEach(function (g) {\n var list = [];\n for (var kk in g[1]) {\n if (g[1][kk] && g[1][kk].name && g[1][kk].name !== 'dummy') list.push(g[1][kk]);\n }\n if (!list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(g[0], list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n return h;\n});\n\n/* --- 9. actions ----------------------------------------------------------- */\nMOD_wikiPage('actions', 'Actions', function () {\n var unlock = {};\n MOD_ACTION_UNLOCKS.forEach(function (u) {\n if (u.act) unlock[u.act.id] = (skl[u.skill] ? skl[u.skill].name : u.skill) +\n ' level ' + u.lv;\n });\n if (typeof act.mod_qi !== 'undefined') {\n unlock[act.mod_qi.id] = 'clearing the dojo\\'s Easiest, Easy and Normal fights';\n }\n\n var h = '

Actions

' +\n '

What you can be doing. One at a time, and only where the ' +\n 'action makes sense — unless \"Unrestricted actions\" is ticked in settings, ' +\n 'which lets several run at once anywhere. Both limits are deliberate; the ' +\n 'switch is there because they are not to everyone\\'s taste.

';\n\n var keys = [];\n for (var k in act) if (act[k] && act[k].name && act[k].name !== 'dummy') keys.push(k);\n keys.sort(function (a, b) { return act[a].name < act[b].name ? -1 : 1; });\n\n keys.forEach(function (k) {\n var a = act[k];\n var how = unlock[a.id];\n h += '
' +\n '

' + MOD_WIKI.safe(a.name) +\n (a.have === true ? 'unlocked'\n : 'locked') +\n (how ? 'from ' + MOD_WIKI.safe(how) + '' : '') +\n '

' + MOD_wikiDesc(a.desc) + '
';\n });\n return h;\n});\n\n/* --- 10. the dojo --------------------------------------------------------- */\nMOD_wikiPage('dojo', 'The dojo', function () {\n var h = '

The dojo

' +\n '

The instructor promises a reward every five levels. The base ' +\n 'game stops delivering at 30; the mod carries the same ladder to 110, on the ' +\n 'same terms.

' +\n '

Only the lowest rung you have not claimed is offered, which is how the ' +\n 'first six behave. Rewards are sized against the character experience curve ' +\n '(4×level3 + level2) — a level at 110 costs 5.3 ' +\n 'million, which is why the base game\\'s best pill is worth 0.3% of one there.

' +\n '' +\n '';\n\n MOD_DOJO_RUNGS.forEach(function (r) {\n var pill = item[r.pill];\n var extra = [];\n if (r.realmPill) extra.push('breakthrough pill for realm ' + r.realmPill);\n if (r.manual) extra.push('a choice of master\\'s manual');\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
LevelRewardCoinAlso
' + r.lv + '' + (pill ? MOD_WIKI.safe(pill.name) + ' ×' + r.n\n : '—') + '' + MOD_wikiNum(r.coin) + '' + (extra.join(', ') || '—') + '' + (global.flags[r.flag] ? 'claimed' : '') +\n '
' +\n\n '

Spirit pills

' +\n '

Character experience in a bottle. The four the mod adds continue the ' +\n 'game\\'s own series up to the new curve.

' +\n '' +\n '';\n MOD_PILLS.forEach(function (p) {\n h += '';\n });\n h += '
PillExperience
' + MOD_WIKI.safe(p[2]) + '' +\n p[3].toLocaleString() + '
' +\n\n '

Master\\'s manuals

' +\n '

A third grade of skillbook, offered at levels 50, 75 and 100 as a choice ' +\n 'the same way the first one was. Each is worth +' +\n Math.round(MOD_MANUAL_RATE * 100) + '% experience in its skill, permanently.

' +\n '' +\n '';\n MOD_MANUALS.forEach(function (m) {\n h += '';\n });\n h += '
ManualSkill
' + MOD_WIKI.safe(m[2]) + '' +\n MOD_WIKI.safe(skl[m[0]] ? (skl[m[0]].bname || skl[m[0]].name) : m[0]) +\n '
';\n return h;\n});\n\n/* --- 11. mechanics -------------------------------------------------------- */\nMOD_wikiPage('mechanics', 'Mechanics', function () {\n return '

Mechanics

' +\n\n '

Damage is subtractive, in both directions

' +\n '

A hit does the attacker\\'s strength minus the defender\\'s strength, plus ' +\n 'the weapon, plus one, before criticals. A defender\\'s STR is their ' +\n 'armour — there is no separate armour stat doing that job.

' +\n '

Two consequences worth carrying around:

' +\n '
    ' +\n '
  • Damage does not taper, it stops. Once a defender\\'s STR passes ' +\n 'yours, your hits do not get small; they land for nothing. Progress against ' +\n 'something too strong is not slow, it is zero.
  • ' +\n '
  • A stat ratio tells you nothing. Comparing your HP to an enemy\\'s ' +\n 'strength ignores the subtraction entirely and reads several times off in ' +\n 'either direction.
  • ' +\n '
' +\n '

This is also the reason enemies are never scaled by raising ' +\n 'their strength: it would raise their offence and their armour together, and ' +\n 'the second effect arrives first.

' +\n\n '

Criticals carry the late game

' +\n '

Critical rate reaches about a third at the top of the ladder and a ' +\n 'critical is roughly eight times a normal swing, so around three quarters of ' +\n 'all damage done at that point is critical damage. Anything that raises ' +\n 'critical rate or critical damage is worth more than it looks; Sharp Eye and ' +\n 'War are the two that do.

' +\n\n '

Hit chance

' +\n '

Landing a hit is a contest of agility. It is a ratio rather than a ' +\n 'subtraction, so agility behaves more gently than strength does — falling ' +\n 'behind on it costs you accuracy, not everything.

' +\n\n '

Skill sections and parents

' +\n '

Each of the ' + Object.keys(MOD_SECTIONS).length + ' sections has a parent ' +\n 'skill. A parent earns ' + Math.round(MOD_PARENT.feed * 100) + '% of everything ' +\n 'its children earn and grants no stats of its own. In return, a child below ' +\n 'the parent\\'s level trains ' + Math.round(MOD_PARENT.perLevel * 100) +\n '% faster per level of lag, up to ' + MOD_PARENT.maxGap + ' levels behind. ' +\n 'Bringing one section up makes every skill in it cheaper to catch up.

' +\n\n '

Saving

' +\n '

Three slots, switched from the bottom bar. The game builds its world once ' +\n 'at startup and cannot unload a save, so switching slots or starting a new ' +\n 'game reloads the page. Exporting a save before running anything unusual is ' +\n 'always the safe move.

';\n});\n\n/* --- the document ---------------------------------------------------------\n One string, no fetches, no dependencies: this has to work from file:// as\n readily as from a server.\n ------------------------------------------------------------------------- */\n\nfunction MOD_wikiCss() {\n return [\n ':root{color-scheme:dark}',\n '*{box-sizing:border-box}',\n /* Menlo and Consolas before the generic: with MS Gothic missing (any Mac)\n \"monospace\" is Courier, which Safari sets wider than Chrome does, and\n table cells wrapped on one and not the other. */\n 'body{margin:0;background:#060a1c;color:#dfe6f5;font:14px/1.6 \"MS Gothic\",' +\n '\"MS Mincho\",Menlo,Consolas,\"DejaVu Sans Mono\",monospace;display:flex;min-height:100vh}',\n 'a{color:#7cb0ff}',\n 'nav{width:210px;flex:0 0 210px;background:#081040;border-right:1px solid #46a;' +\n 'position:sticky;top:0;height:100vh;overflow:auto;padding:14px 0}',\n 'nav h2{font-size:13px;color:#9fb4d8;margin:0 0 10px 14px;letter-spacing:1px}',\n 'nav a{display:block;padding:6px 14px;text-decoration:none;color:#cfe0ff;' +\n 'border-left:3px solid transparent}',\n 'nav a:hover{background:#154080}',\n 'nav a.on{background:#123070;border-left-color:#7cb0ff;color:#fff}',\n 'nav .srch{margin:0 10px 12px;width:calc(100% - 20px);background:#050a20;' +\n 'color:#dfe6f5;border:1px solid #46a;padding:5px;font:inherit}',\n 'nav .xall{display:block;margin:12px 10px 0;color:#8296b8;font-size:12px;' +\n 'cursor:pointer;-webkit-user-select:none;user-select:none}',\n 'nav .xall input{vertical-align:-1px;margin-right:4px}',\n 'details.wk-g{border:1px solid #17224a;background:#0a1130;margin:8px 0}',\n 'details.wk-g>summary{cursor:pointer;padding:7px 12px;list-style:none;' +\n 'background:#0d1740}',\n 'details.wk-g>summary:hover{background:#154080}',\n 'details.wk-g>summary::-webkit-details-marker{display:none}',\n // a caret drawn rather than borrowed, so it is the same in every browser\n 'details.wk-g>summary::before{content:\"\\\\25B8\";color:#7cb0ff;' +\n 'display:inline-block;width:1em;transition:transform .12s}',\n 'details.wk-g[open]>summary::before{transform:rotate(90deg)}',\n 'details.wk-g>summary h2,details.wk-g>summary h3{display:inline;margin:0;' +\n 'font-size:15px;color:#fff}',\n '.wk-gi{padding:2px 12px 10px}',\n 'details.wk-g details.wk-g>summary h4{display:inline;margin:0;font-size:13px;' +\n 'color:#cfe0ff;font-weight:normal}',\n 'details.wk-g details.wk-g>summary{padding:5px 10px;background:#0b1435}',\n 'main{flex:1;min-width:0;padding:22px 30px 80px;max-width:1000px}',\n 'h1{font-size:26px;margin:0 0 6px;color:#fff;border-bottom:1px solid #46a;' +\n 'padding-bottom:8px}',\n 'h2{font-size:19px;color:#bcd4ff;margin:30px 0 8px}',\n 'h3{font-size:15px;color:#fff;margin:0 0 4px}',\n 'p.lede{color:#a8bcdc;margin:0 0 18px}',\n '.dim{color:#8296b8;font-weight:normal}',\n '.flav{font-size:12px;font-style:italic}',\n '.note{background:#0b1638;border-left:3px solid #46a;padding:8px 12px;' +\n 'color:#a8bcdc;margin:14px 0}',\n 'code,pre{background:#0b1638;border:1px solid #24356e;padding:1px 5px;' +\n 'color:#9fd0ff}',\n 'pre{padding:10px;overflow-x:auto}',\n 'dl dt{color:#fff;margin-top:14px}',\n 'dl dd{margin:2px 0 0 0;color:#a8bcdc}',\n 'ul{color:#a8bcdc}',\n 'table.wk-tbl{border-collapse:collapse;width:100%;margin:10px 0;' +\n 'display:block;overflow-x:auto}',\n 'table.wk-tbl th{text-align:left;color:#9fb4d8;font-weight:normal;' +\n 'border-bottom:1px solid #46a;padding:5px 9px;white-space:nowrap}',\n 'table.wk-tbl td{border-bottom:1px solid #17224a;padding:5px 9px;' +\n 'vertical-align:top}',\n 'table.wk-tbl tr.done td{color:#cfe0ff}',\n 'table.wk-tbl tr.here{background:#123070}',\n '.num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}',\n '.wk-e{border:1px solid #17224a;background:#081040;padding:9px 12px;' +\n 'margin:7px 0;border-radius:2px}',\n 'tr.wk-e{border:0;background:none}',\n '.wk-e.done{border-color:#2a4a8c}',\n '.wk-e h3{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}',\n '.lv{margin-left:auto;font-size:12px;color:#8296b8;font-weight:normal}',\n '.lv.capped{color:#ffd24a}',\n '.rank{font-size:12px;color:#8296b8;font-weight:normal}',\n '.desc{color:#a8bcdc;font-size:13px}',\n '.proj{color:#ffd24a;font-size:12px;margin-top:3px}',\n '.tag{font-size:11px;border:1px solid #46a;color:#9fb4d8;padding:0 5px;' +\n 'border-radius:2px;font-weight:normal}',\n '.tag.mod{border-color:#7a4ea8;color:#c07bff}',\n '.tag.done{border-color:#2e7d4f;color:#6fdc9a}',\n '.tag.here{border-color:#ffd24a;color:#ffd24a}',\n '.tag.warn{border-color:#a8562e;color:#ff9a5c}',\n 'table.wk-perks{border-collapse:collapse;margin:6px 0 0;width:100%}',\n 'table.wk-perks td{padding:2px 8px 2px 0;font-size:12px;color:#8296b8;' +\n 'border-bottom:1px solid #101a3c}',\n 'table.wk-perks tr.done td{color:#cfe0ff}',\n 'table.wk-perks td.tick{color:#6fdc9a;width:1em;text-align:right}',\n // fixed columns so the level bands line up between one area and the next\n 'table.wk-pop{table-layout:fixed}',\n 'table.wk-pop td:first-child{width:60%}',\n 'table.wk-pop td:nth-child(2){width:20%}',\n '.wk-title .tname{font-weight:bold}',\n '.wk-now{background:#0b1638;border:1px solid #24356e;padding:10px 14px;' +\n 'margin:0 0 18px;display:inline-block;min-width:320px}',\n '.wk-now h3{color:#9fb4d8;font-weight:normal;font-size:13px;' +\n 'letter-spacing:1px;margin-bottom:6px}',\n '.wk-now td{padding:2px 16px 2px 0}',\n '.wk-now td:last-child{color:#fff;text-align:right}',\n '.pg{display:none}.pg.on{display:block}',\n 'footer{color:#5a6c8c;font-size:12px;border-top:1px solid #17224a;' +\n 'margin-top:40px;padding-top:12px}',\n '@media(max-width:760px){body{display:block}nav{width:auto;height:auto;' +\n 'position:static;flex:none}main{padding:16px}}'\n ].join('\\n');\n}\n\n/* Filtering, page switching and the hash. Small enough to inline, and inlining\n it is the point — the document has to stand on its own. */\nfunction MOD_wikiJs() {\n return [\n 'var pages=[].slice.call(document.querySelectorAll(\".pg\"));',\n 'var links=[].slice.call(document.querySelectorAll(\"nav a[data-p]\"));',\n 'function show(id){',\n ' var found=false;',\n ' pages.forEach(function(p){var on=p.id===\"pg-\"+id;if(on)found=true;',\n ' p.className=\"pg\"+(on?\" on\":\"\")});',\n ' if(!found&&pages.length){pages[0].className=\"pg on\";id=pages[0].id.slice(3)}',\n ' links.forEach(function(a){a.className=a.dataset.p===id?\"on\":\"\"});',\n ' window.scrollTo(0,0);return id;}',\n 'links.forEach(function(a){a.addEventListener(\"click\",function(e){',\n ' e.preventDefault();location.hash=a.dataset.p;show(a.dataset.p)})});',\n 'window.addEventListener(\"hashchange\",function(){show(location.hash.slice(1))});',\n 'show(location.hash.slice(1)||\"start\");',\n // Search filters entries, hides groups left with none, and OPENS the groups\n // that still have some — a match hidden inside a collapsed group reads as\n // no match at all, which is worse than not having search.\n 'var box=document.getElementById(\"srch\");',\n 'var xall=document.getElementById(\"xall\");',\n 'var groups=[].slice.call(document.querySelectorAll(\"details.wk-g\"));',\n 'function filter(){',\n ' var q=box.value.trim().toLowerCase();',\n ' pages.forEach(function(p){',\n ' [].slice.call(p.querySelectorAll(\".wk-e\")).forEach(function(e){',\n ' e.style.display=(!q||e.textContent.toLowerCase().indexOf(q)>=0)?\"\":\"none\"})});',\n // Every page, not just the one on screen: the nav says how many entries\n // match in each, dims the ones with none, and Enter jumps to the first\n // page that has any. A prose page with no entries counts as one if its text\n // contains the query.\n ' links.forEach(function(a){',\n ' var pg=document.getElementById(\"pg-\"+a.dataset.p),n=0;',\n ' if(a.dataset.l===undefined)a.dataset.l=a.textContent;',\n ' if(q&&pg){var es=[].slice.call(pg.querySelectorAll(\".wk-e\"));',\n ' if(es.length)es.forEach(function(e){if(e.textContent.toLowerCase().indexOf(q)>=0)n++});',\n ' else if(pg.textContent.toLowerCase().indexOf(q)>=0)n=1}',\n ' a.dataset.n=n;a.style.opacity=(q&&!n)?\".4\":\"\";',\n ' a.textContent=a.dataset.l+(q&&n?\" (\"+n+\")\":\"\")});',\n ' groups.forEach(function(g){',\n ' var any=[].slice.call(g.querySelectorAll(\".wk-e\")).some(function(e){',\n ' return e.style.display!==\"none\"});',\n ' g.style.display=any?\"\":\"none\";',\n ' g.open=xall.checked||(!!q&&any)}); }',\n 'box.addEventListener(\"input\",filter);',\n 'box.addEventListener(\"keydown\",function(e){',\n ' if(e.key===\"Escape\"){box.value=\"\";filter()}',\n ' if(e.key===\"Enter\"&&box.value.trim()){',\n ' var cur=links.filter(function(a){return a.className===\"on\"})[0];',\n ' if(cur&&+cur.dataset.n)return;',\n ' var hit=links.filter(function(a){return +a.dataset.n>0})[0];',\n ' if(hit){location.hash=hit.dataset.p;show(hit.dataset.p)}}});',\n // Everything is collapsed by default because three of these pages run past\n // 40,000px open. This is the way back to one long document, which is what\n // the browser's own find needs.\n 'xall.addEventListener(\"change\",filter);'\n ].join('\\n');\n}\n\nfunction MOD_wikiBuild() {\n var nav = '', body = '';\n MOD_WIKI_PAGES.forEach(function (pg) {\n nav += '' +\n MOD_WIKI.safe(pg.label) + '';\n var inner;\n /* One page throwing must not take the wiki down with it — the page says so\n and the rest still renders. */\n try { inner = pg.build(); }\n catch (e) {\n inner = '

' + MOD_WIKI.safe(pg.label) + '

This page ' +\n 'could not be built: ' + MOD_WIKI.safe(e.message) + '

';\n console.warn('[mod] wiki page \"' + pg.id + '\" failed: ' + e.message);\n }\n body += '
' + inner + '
';\n });\n\n var stamp = new Date().toLocaleString();\n return '' +\n '' +\n '' + MOD_WIKI.safe(MOD_WIKI.title) + '' +\n '' +\n '' +\n '
' + body +\n '
Generated from the running game at ' + MOD_WIKI.safe(stamp) +\n ' — mod ' + MOD_WIKI.safe(MOD.version) + '. Everything here is read from the ' +\n 'live data, including your own save, so it cannot fall out of step with the ' +\n 'game. The game is truezhangwei\\'s; the mod is not endorsed by the author.' +\n '
' +\n ' inside content closing the block early, so that is the\n one thing scrubbed. */\n safe: function (h) {\n return String(h === undefined || h === null ? '' : h).replace(/<\\/script/gi, '<\\\\/script');\n }\n};\n\n/* Descriptions carry real HTML (coloured spans, the game's separator div), so\n they are passed through rather than escaped — but the separator becomes a\n rule, since a wiki row is not a tooltip. */\nfunction MOD_wikiDesc(d) {\n var s;\n try { s = (typeof d === 'function') ? d() : d; } catch (e) { s = ''; }\n if (!s) return '';\n s = String(s).split('
 
').join('
');\n return MOD_WIKI.safe(s);\n}\n\nfunction MOD_wikiNum(n) {\n if (!isFinite(n)) return '—';\n return Math.round(n).toLocaleString();\n}\n\n/* An effect line at a level the player has not reached yet. The effect\n functions read the skill they are handed, so the level is borrowed and put\n back; anything that throws simply gets no projection. */\nfunction MOD_wikiEffectAt(key, sk, lvl) {\n var eff = MOD_EFFECTS[key];\n if (!eff) return '';\n var was = sk.lvl, out = '';\n try { sk.lvl = lvl; out = eff(sk) || ''; }\n catch (e) { out = ''; }\n finally { sk.lvl = was; }\n return MOD_WIKI.safe(out);\n}\n\n/* --- page builders --------------------------------------------------------\n Each returns the inner HTML of one page. `wk-e` marks a searchable entry and\n `wk-g` a group that hides itself when all its entries are filtered out.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_PAGES = [];\n\nfunction MOD_wikiPage(id, label, build) {\n MOD_WIKI_PAGES.push({ id: id, label: label, build: build });\n}\n\n/* A collapsible group.\n\n Not decoration: with every skill, item and title listed, three of the eleven\n pages ran past 40,000 pixels — a hundred screens of scrolling, which is the\n same as not having the content at all. Collapsed, each of those pages opens\n as an index you can read in one screen and expand where you want.\n\n
rather than a click handler, so it still works with JavaScript off,\n the browser's own find can be made to reach inside it, and the summary is\n keyboard-reachable for free. `level` is the heading level for the summary,\n since a group is sometimes a section of a page and sometimes a subsection. */\nfunction MOD_wikiGroup(heading, count, inner, level) {\n var hn = 'h' + (level || 2);\n return '
<' + hn + '>' + heading +\n (count !== null && count !== undefined\n ? ' (' + count + ')' : '') +\n '
' + inner + '
';\n}\n\n/* --- 1. orientation ------------------------------------------------------- */\nMOD_wikiPage('start', 'Start here', function () {\n var cap = MOD_levelCap();\n var realm = MOD_realm();\n var h = '

proto23

' +\n '

An idle RPG by truezhangwei, plus a mod. You train skills by ' +\n 'doing things, the things you can do open up as the story does, and none of ' +\n 'it is in a hurry.

' +\n\n '

Where you are right now

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Character level' + MOD_wikiNum(you.lvl) + '
Skill level cap' + cap + ' — ' +\n MOD_WIKI.safe(MOD_CAP.name) + '
Cultivation realm' + MOD_WIKI.safe(realm.name) +\n ' (' + realm.n + ' of ' + (MOD_REALMS.length - 1) + ')
Titles held' + MOD_titleCount() + ' — renown ' +\n (skl.rnwn ? skl.rnwn.lvl : 0) + '
Coin' + MOD_wikiNum(you.wealth || 0) + '
' +\n\n '

The four things worth knowing early

' +\n '
' +\n '
Your skill levels are capped by the story, not by grinding.
' +\n '
The cap starts at 10 and ends at 110, and it moves when you reach a new ' +\n 'place — not when you earn enough experience. If a skill has stopped ' +\n 'levelling, you are not doing it wrong; you need to go somewhere. See ' +\n 'Progression.
' +\n\n '
Armour is subtraction, so levels matter more than they look.
' +\n '
A hit does the attacker\\'s STR minus the defender\\'s STR. That means a ' +\n 'small gap in strength is a large gap in damage, and a big enough one takes ' +\n 'your damage to nothing at all rather than merely reducing it. See ' +\n 'Mechanics.
' +\n\n '
Most skills have a parent that pulls them along.
' +\n '
Every section has a parent skill that earns a share of everything its ' +\n 'children do, and any child lagging behind the parent trains faster — up to ' +\n 'twice as fast at ' + MOD_PARENT.maxGap + ' levels behind. A neglected skill is ' +\n 'cheap to bring up later, which is the only reason having this many is ' +\n 'workable.
' +\n\n '
Cultivation is a second ladder, and it is not automatic.
' +\n '
Circulating qi levels a skill; the skill does not raise your realm. ' +\n 'Reaching the level opens a bottleneck, and breaking through it costs a pill ' +\n 'and can fail. See Cultivation.
' +\n '
' +\n\n '

What is the mod and what is the game

' +\n '

The game is index.html and it is entirely truezhangwei\\'s ' +\n 'work. Everything in mod.js is a modification of it and is not ' +\n 'endorsed by the author. Pages here mark added content with ' +\n 'mod where the distinction matters. The full ' +\n 'list of changes is in the changelog, on the bottom bar.

';\n return h;\n});\n\n/* --- 2. progression ------------------------------------------------------- */\nMOD_wikiPage('progress', 'Progression', function () {\n var cap = MOD_levelCap();\n var h = '

Progression

' +\n '

Skill levels are capped by how far the story has gone. Ten ' +\n 'rungs, 10 through 110. The cap applies to every skill at once; Renown is ' +\n 'the single exception, since it is counted rather than trained.

' +\n '' +\n '';\n\n MOD_TIERS.forEach(function (t) {\n var got = null;\n try { got = MOD_tierFlag(t); } catch (e) {}\n var here = (t.cap === cap);\n var need = t.needText ? t.needText\n : (t.flags.length ? t.flags.join(' or ') : 'the start of the game');\n h += '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
CapStageWhat opens it
' + t.cap + '' + MOD_WIKI.safe(t.name) + '' + MOD_WIKI.safe(need) + '' + (here ? 'you are here'\n : got ? 'reached' : '') + '
' +\n\n '

The experience curve

' +\n '

The game\\'s own skill curve is super-exponential — level 109 to 110 ' +\n 'alone costs ' + MOD_wikiNum(MOD_vanillaExpnext(109)) + ' experience, so the top of the ladder ' +\n 'is unreachable by a factor of about ten trillion. But the author only ever ' +\n 'designed skills up to level 15, and below that his curve is fine.

' +\n (MOD_XP.vanillaTo > 0\n ? '

So the mod keeps it. Below level ' + MOD_XP.vanillaTo + ' a skill costs ' +\n 'exactly what the original charges; from there it rises by ×' +\n MOD_XP.ratio.toFixed(4) + ' a level, a ratio solved so the climb to 110 takes ' +\n 'the same time it always did.'\n : '

You have chosen the Mod before 4.2 pacing: a plain geometric curve, ' +\n '×' + MOD_XP.ratio.toFixed(3) + ' a level from level 0, much faster than the ' +\n 'original early on. The Original pacing in settings puts back the ' +\n 'author\\'s own cost below level 10.') +\n ' Character level uses the game\\'s own curve, untouched.

' +\n '' +\n '';\n [1, 5, 9, 10, 15, 25, 50, 75, 90, 109].forEach(function (l) {\n h += '';\n });\n h += '
LevelExperience for the nextOriginal game
' + l +\n '' + MOD_wikiNum(MOD_expnextFor(l)) + '' +\n MOD_wikiNum(MOD_vanillaExpnext(l)) + '
';\n return h;\n});\n\n/* --- 3. skills ------------------------------------------------------------ */\nMOD_wikiPage('skills', 'Skills', function () {\n var cap = MOD_levelCap();\n var bySection = {};\n var keys = [];\n for (var k in skl) {\n var s = skl[k];\n if (!s || typeof s !== 'object' || !s.name) continue;\n keys.push(k);\n }\n keys.forEach(function (k) {\n var s = skl[k];\n var sec = MOD_sectionOf(s);\n (bySection[sec] = bySection[sec] || []).push(k);\n });\n\n var h = '

Skills

' +\n '

' + keys.length + ' skills in ' +\n Object.keys(MOD_SECTIONS).length + ' sections. Every one has perks at ' +\n MOD_LADDER.join(', ') + ', and every one that has milestones grants titles at ' +\n MOD_TITLE_RUNGS.join(', ') + '. Levels and ticks below are yours.

';\n\n Object.keys(MOD_SECTIONS).forEach(function (sec) {\n var list = bySection[sec];\n if (!list || !list.length) return;\n var parentKey = MOD_PARENT_KEY[sec];\n var g = '';\n if (parentKey && skl[parentKey]) {\n g += '

Parent skill: ' +\n MOD_WIKI.safe(skl[parentKey].bname || skl[parentKey].name) +\n ' — earns ' + Math.round(MOD_PARENT.feed * 100) +\n '% of what its children earn, and speeds up any child that lags behind it.

';\n }\n list.sort(function (a, b) { return skl[a].name < skl[b].name ? -1 : 1; });\n list.forEach(function (k) {\n var s = skl[k];\n var lvl = Number(s.lvl) || 0;\n g += '
' +\n '

' + MOD_WIKI.safe(s.bname || s.name) +\n (s.bname ? ' (' + MOD_WIKI.safe(s.name) + ')' : '') +\n '= cap ? ' capped' : '') + '\">lv ' + lvl +\n (lvl >= cap ? ' — at the cap' : '') + '

' +\n '
' + MOD_wikiDesc(s.desc) + '
';\n\n var at110 = MOD_wikiEffectAt(k, s, 110);\n if (at110) g += '
At level 110: ' + at110 + '
';\n\n if (s.mlstn && s.mlstn.length) {\n g += '';\n s.mlstn.forEach(function (m) {\n g += '' +\n '' +\n '' +\n '';\n });\n g += '
' + m.lv + '' + MOD_WIKI.safe(m.p || '—') + '' + (m.g === true ? '✓' : '') + '
';\n }\n g += '
';\n });\n h += MOD_wikiGroup(MOD_WIKI.safe(MOD_SECTIONS[sec]), list.length, g, 2);\n });\n return h;\n});\n\n/* --- 4. cultivation ------------------------------------------------------- */\nMOD_wikiPage('cultivation', 'Cultivation', function () {\n var now = MOD_realm();\n var elig = 0;\n try { elig = MOD_realmEligible().n; } catch (e) {}\n var qic = skl.qic ? skl.qic.lvl : 0;\n\n var h = '

Cultivation

' +\n '

Ten realms above Mortal. Circulating qi levels the Qi ' +\n 'Circulation skill; the skill does not advance the realm. Reaching the level ' +\n 'opens a bottleneck, and breaking through it costs a pill and can fail — the ' +\n 'pill is spent either way.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Realm' + MOD_WIKI.safe(MOD_realmTitle()) + '
Spiritual Root' + MOD_WIKI.safe(MOD_root().name) + '
Qi Circulationlevel ' + qic + '
Insight' + MOD_insight() + '
Bottleneck open' +\n (elig > now.n ? 'yes — ' + MOD_WIKI.safe(MOD_REALMS[now.n + 1].name) +\n ' is within reach' : 'no') + '
Qi Deviation' + (MOD_deviated()\n ? MOD_deviationLeft() + ' in-game minutes left' : 'no') + '
' +\n '

Realm thresholds are the same levels that earn a title rank, so realm N ' +\n 'is the level at which a rank N title arrives. Every realm multiplies all ' +\n 'stats and health, and the multipliers do not stack with each other — the ' +\n 'realm you are in is the one that applies.

' +\n '' +\n '';\n\n MOD_REALMS.forEach(function (r) {\n var pill = r.n >= 1 ? item['mod_bp' + r.n] : null;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmNameQi Circ.StatsHealthPill
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + r.qic + '×' + r.mult + '×' + r.hp + '' + (pill ? MOD_WIKI.safe(pill.name) : '—') +\n '
' +\n\n '

Where the pills come from

' +\n '

Every breakthrough pill has a source in normal play. None of them are ' +\n 'craftable, and none are cheap — a bottleneck you can buy in bulk is not a ' +\n 'bottleneck.

' +\n '' +\n '' +\n '' +\n '' +\n '
RealmsSource
1The dojo instructor, when qi ' +\n 'circulation is first taught
2–5The Herbalist in the village ' +\n '(vendor.pha1), restocked
6–10The dojo\\'s Level Advancement ' +\n 'ladder, and the Pill Tower
' +\n '

Realm 1 is deliberately not sold at the marketplace: ' +\n 'the marketplace is behind a 40%-a-visit chance encounter, and realm 1 opens ' +\n 'long before that is reliable.

' +\n\n '

Breaking through

' +\n '

A bottleneck wants three things, which is what the genre says it wants: ' +\n 'a pill, insight, and enough consolidation to make the ' +\n 'attempt worth making. The pill and the insight are both spent whether the ' +\n 'attempt works or not.

' +\n '

The odds improve the further past the threshold you are, plus your root, ' +\n 'plus time sat in Closed Door Training, and cap short of certain.

' +\n '' +\n '';\n [0, 2, 4, 6, 8].forEach(function (o) {\n var base = Math.min(0.55 + o * 0.05 + MOD_root().odds, 0.95);\n h += '';\n });\n h += '
Levels past the thresholdChanceAfter full seclusion
+' + o + '' +\n Math.round(base * 100) + '%' +\n Math.round(Math.min(base + MOD_CULT.secludeOdds, 0.95) * 100) + '%
' +\n\n '

Insight

' +\n '

Insight is the half of the price that cannot be bought. It comes from ' +\n 'three places, and they are the three the genre names: meditating, fights you ' +\n 'nearly lost, and going somewhere you have not been.

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
SourceRate
Circulate Qi, at a wall' +\n (MOD_CULT.meditateChance * 100).toFixed(1) + '% a second
Closed Door Training' +\n (MOD_CULT.secludeChance * 100).toFixed(1) + '% a second
A kill made under ' +\n Math.round(MOD_CULT.brinkFrac * 100) + '% health — ' +\n (MOD_FIGHTS.mode === 'original'\n ? 'the author\\'s areas rarely come close once you reach them; the three past ' +\n 'the golem arena and the rank duels do, within a few fights, as does an area ' +\n 'you walk into early'\n : 'every fight costs a share of your health, so a few in a row without resting') +\n '' +\n Math.round(MOD_CULT.brinkChance * 100) + '% a kill
Entering an area for the first timealways
' +\n '

The insight a realm asks for is sized to the climb to it: about ' +\n Math.round(MOD_CULT.wallShare * 100) + '% of the Circulate Qi time the climb took, ' +\n 'spent at the wall. Seclusion earns it three times as fast, and full ' +\n 'consolidation takes as long as that on average.

' +\n '' +\n '' +\n '';\n var wkHours = function (sec) {\n return sec >= 7200 ? (sec / 3600).toFixed(sec >= 36000 ? 0 : 1) + ' h' : Math.round(sec / 60) + ' min';\n };\n MOD_REALMS.forEach(function (r) {\n if (!r.n) return;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmClimb (Circulate Qi, 1x)Insight to enterFull seclusionQi Deviation (in-game)
' + MOD_WIKI.safe(r.name) + '' + wkHours(MOD_realmClimbSec(r.n)) + '' + MOD_insightNeed(r.n) + '' + wkHours(MOD_secludeFull(r.n)) + '' + (MOD_devMinutes(r.n) / 1440).toFixed(1) + ' days
' +\n\n '

The nine layers

' +\n '

Each realm is nine layers deep, derived from where Qi Circulation sits ' +\n 'between this realm\\'s threshold and the next one\\'s — so breaking through ' +\n 'puts you at the first layer of the new realm on its own. Layers are worth ' +\n '×' + MOD_CULT.layerMult + ' stats and ×' + MOD_CULT.layerBody +\n ' body each above the first, and the top realm has no rung above it to ' +\n 'measure against, so it stays at its first layer.

' +\n '' +\n '';\n for (var li = 1; li <= MOD_CULT.layers; li++) {\n h += '';\n }\n h += '
LayerCalled
' +\n li + '' + MOD_LAYER_ORD[li] + ' layer, ' +\n MOD_WIKI.safe(MOD_layerStage(li)) + '
' +\n '

At the threshold of the next realm and before breaking ' +\n 'through, you are a half step to it — the genre\\'s own name for the ' +\n 'state this mod calls a bottleneck.

' +\n\n '

Closed Door Training

' +\n '

Seclusion does two things. It consolidates — a full sitting ' +\n '(the table above) is worth +' + Math.round(MOD_CULT.secludeOdds * 100) +\n '% on the attempt — and it shelters: a breakthrough attempted while ' +\n 'you are still sitting in it cannot end in Qi Deviation, only in a lost ' +\n 'pill. It will not start unless you are actually at a wall.

' +\n\n '

Qi Deviation

' +\n '

Failing a breakthrough out in the open leaves your cultivation base ' +\n 'unstable for as long as the seclusion you skipped (the table above, never ' +\n 'under ' + MOD_CULT.devMinutes + ' in-game minutes): stats at ×' +\n MOD_CULT.devStat + ', body at ×' + MOD_CULT.devBody + ', and no second ' +\n 'attempt until it passes. It clears on its own, or at once with a ' +\n 'Qi Settling Pill. It never kills — the death system stays out of it.

' +\n\n '

Heavenly Tribulation

' +\n '

From realm ' + MOD_CULT.tribFrom + ' up, a successful breakthrough is ' +\n 'answered. The cost is a fraction of your maximum health, so it is a gate on ' +\n 'walking in healthy rather than a damage race; the realm bonus you are ' +\n 'standing on is what pays it. Fall to it and the realm does not open.

' +\n '' +\n '';\n MOD_REALMS.forEach(function (r) {\n if (!r.n || r.n < MOD_CULT.tribFrom) return;\n h += '' +\n '' +\n '';\n });\n h += '
RealmBoltsOf your max HP
' + MOD_WIKI.safe(r.name) + '' + MOD_tribulationBolts(r.n) + '' + Math.round(MOD_tribulationCost(r.n) * 100) + '%
' +\n\n '

Spiritual Root

' +\n '

Rolled once, when the dojo finishes teaching you to fight. It is never a ' +\n 'dead end: the worst grade costs cultivation speed, not access, and a ' +\n 'Root Cleansing Pill moves you up a grade.

' +\n '' +\n '';\n var rootTotal = 0;\n MOD_ROOTS.forEach(function (r) { rootTotal += r.weight; });\n MOD_ROOTS.forEach(function (r) {\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
GradeRootChanceCultivationBreakthrough
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + Math.round(r.weight / rootTotal * 100) + '%×' + r.xp + '' + (r.odds >= 0 ? '+' : '') +\n Math.round(r.odds * 100) + '%
';\n return h;\n});\n\n/* --- 5. elemental mastery ------------------------------------------------- */\nMOD_wikiPage('mastery', 'Elemental mastery', function () {\n var h = '

Elemental mastery mod

' +\n '

Six masteries, each with a technique that fires on its own ' +\n 'during a fight. Combat is automatic and there is no ability picker, so a ' +\n 'technique is a chance on each of your swings to become something else — one ' +\n 'that scales with INT and the element rather than with your weapon.

' +\n '

Nothing fires while you are still Mortal: the channels have to be open ' +\n 'first. Chance is ' + (MOD_TECH.perLevel * 100).toFixed(1) + '% per mastery ' +\n 'level plus ' + (MOD_TECH.perRealm * 100).toFixed(1) + '% per realm, capped at ' +\n Math.round(MOD_TECH.cap * 100) + '%; a technique lands for ×' +\n MOD_TECH.power + ' of a normal strike.

' +\n '' +\n '';\n\n var realm = 0;\n try { realm = MOD_realm().n; } catch (e) {}\n MOD_MASTERY.forEach(function (m) {\n var lvl = Number(m.skill.lvl) || 0;\n var ch = realm < 1 ? 0\n : Math.min(lvl * MOD_TECH.perLevel + realm * MOD_TECH.perRealm, MOD_TECH.cap);\n h += '' +\n '' +\n '' +\n '';\n });\n h += '
MasteryTechniqueLevelChance now
' +\n MOD_WIKI.safe(m.skill.name) + '' + MOD_WIKI.safe(m.name) + '' + lvl + '' + (realm < 1 ? 'Mortal'\n : (ch * 100).toFixed(1) + '%') + '
';\n return h;\n});\n\n/* --- 6. titles ------------------------------------------------------------ */\nMOD_wikiPage('titles', 'Titles', function () {\n var perm = MOD_permanentRank();\n var held = MOD_titleCount();\n var total = 0; for (var kk in ttl) if (ttl[kk] && ttl[kk].name) total++;\n\n var h = '

Titles

' +\n '

A title\\'s rank runs 1 to 10 and is derived from the level ' +\n 'that earns it, so a rank is a statement about where in the game you are. ' +\n 'Only the title you are wearing applies — until renown makes the lower ranks ' +\n 'permanent.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '
Titles held' + held + ' of ' + total + '
Renown level' + (skl.rnwn ? skl.rnwn.lvl : 0) +\n ' of ' + MOD_RENOWN_MAX + '
Ranks that apply without wearing' +\n (perm >= 1 ? '1–' + perm : 'none yet') + '
' +\n\n '

Ranks

' +\n '';\n MOD_RANK_AT.forEach(function (lv, i) {\n var st = MOD_rankStyle(i + 1);\n h += '' +\n '';\n });\n h += '
RankEarned from levelColour
' + (i + 1) + '' + lv + '' + '●●●' + '
' +\n\n '

Renown

' +\n '

Renown is not trained. It counts the titles you hold, and each level ' +\n 'multiplies every stat by ' + (1 + MOD_RENOWN_RATE) + ' — compounding, so ' +\n 'level ' + MOD_RENOWN_MAX + ' is ×' +\n (Math.round(MOD_renownMult(MOD_RENOWN_MAX) * 1000) / 1000) + '. At renown N, ' +\n 'every title of rank N or below applies permanently.

' +\n '' +\n '';\n MOD_RENOWN_STEPS.forEach(function (n, i) {\n h += '= n ? 'done' : '') + '\">' +\n '';\n });\n h += '
RenownTitles neededAll statsPassive ranks
' + (i + 1) +\n '' + n + '×' +\n (Math.round(MOD_renownMult(i + 1) * 1000) / 1000) + '1–' + (i + 1) + '
' +\n\n '

Every title

' +\n '

Grouped by the skill that grants it. What is left is either ' +\n 'a ladder of its own — creatures killed, items gathered, the ' +\n 'cultivation realms — or a one-off from the story.

';\n\n var groups = {}, fams = {}, loose = [];\n for (var k in ttl) {\n var t = ttl[k];\n /* Unlike the picker, the wiki KEEPS the author's broken entries and says\n so — same call as area.clg, which is listed and tagged \"nothing spawns\"\n rather than quietly dropped. A reader looking for why a title never\n appears is better served by the row than by its absence. */\n if (!t || t.name === undefined || t.name === null) continue;\n var sk = MOD_TITLE_SKILL[k];\n if (sk && skl[sk]) { (groups[sk] = groups[sk] || []).push(k); continue; }\n var stem = MOD_titleStem(k);\n if (stem) { (fams[stem] = fams[stem] || []).push(k); continue; }\n loose.push(k);\n }\n /* Same rule as the picker: a family of one is not a family. */\n Object.keys(fams).forEach(function (stem) {\n if (fams[stem].length < 2) { loose.push(fams[stem][0]); delete fams[stem]; }\n });\n var order = Object.keys(groups).sort(function (a, b) {\n return skl[a].name < skl[b].name ? -1 : 1;\n });\n\n var row = function (k) {\n var t = ttl[k];\n var st = MOD_rankStyle(t.rar);\n var gen = null;\n for (var i = 0; i < MOD_SKILL_TITLES.length; i++) {\n if (MOD_SKILL_TITLES[i].key === k) { gen = MOD_SKILL_TITLES[i]; break; }\n }\n /* The author left a handful unfinished: ttl.ddcd carries the literal string\n \"null\" for both name and description, and shpt2/shpt3/mone3 are blank.\n None is granted by anything, so none can be earned. */\n var broken = !t.name || t.name === 'null';\n return '
' +\n '' +\n MOD_WIKI.safe(t.name || '(unnamed)') + '' +\n (broken ? 'unfinished' : '') +\n 'rank ' + (t.rar || 1) + '' +\n (gen ? 'lv ' + gen.rung + ' · +' +\n Math.round(gen.xp * 100) + '% ' + MOD_WIKI.safe(skl[gen.skill].name) +\n ' exp' : '') +\n (t.have === true ? 'held' : '') +\n '
' + MOD_wikiDesc(t.desc) + '
';\n };\n\n var byRank = function (a, b) { return (ttl[a].rar || 1) - (ttl[b].rar || 1); };\n order.forEach(function (sk) {\n groups[sk].sort(byRank);\n h += MOD_wikiGroup(MOD_WIKI.safe(skl[sk].bname || skl[sk].name),\n groups[sk].length, groups[sk].map(row).join(''), 3);\n });\n /* The ladders that belong to no skill, by the same derivation the picker\n uses — so the two cannot drift apart. Sorted by the label, since that is\n what the reader scans. */\n var famList = Object.keys(fams)\n .map(function (stem) {\n fams[stem].sort(byRank);\n return { stem: stem,\n label: MOD_titleFamilyLabel(stem, fams[stem].map(function (k) { return ttl[k]; })) };\n })\n .sort(function (a, b) { return a.label < b.label ? -1 : a.label > b.label ? 1 : 0; });\n if (famList.length) {\n /* Nested one level down rather than sitting beside the skills. Eleven more\n top-level summaries cost ~530px on a page that is already the tallest in\n the wiki, and these are a different KIND of thing from a skill anyway. */\n var famInner = '', famTotal = 0;\n famList.forEach(function (f) {\n famTotal += fams[f.stem].length;\n famInner += MOD_wikiGroup(MOD_WIKI.safe(f.label), fams[f.stem].length,\n fams[f.stem].map(row).join(''), 4);\n });\n h += MOD_wikiGroup('Ladders of their own', famTotal, famInner, 3);\n }\n if (loose.length) {\n loose.sort(byRank);\n /* The story titles are the one group with no skill to divide them, and\n there are enough of them to need dividing. Rank rather than the alphabet:\n it is the axis they are already sorted on, and the one a reader cares\n about — a rank is a statement about where in the game a title comes from. */\n var inner;\n if (loose.length <= MOD_WIKI_ALPHA_AT) {\n inner = loose.map(row).join('');\n } else {\n var byRankNo = {};\n loose.forEach(function (k) {\n var r = Math.min(Math.max(Number(ttl[k].rar) || 1, 1), 10);\n (byRankNo[r] = byRankNo[r] || []).push(k);\n });\n inner = '';\n Object.keys(byRankNo).sort(function (a, b) { return a - b; }).forEach(function (r) {\n var st = MOD_rankStyle(r);\n inner += MOD_wikiGroup('Rank ' + r + '', byRankNo[r].length,\n byRankNo[r].map(row).join(''), 4);\n });\n }\n h += MOD_wikiGroup('From the story', loose.length, inner, 3);\n }\n return h;\n});\n\n/* --- 7. areas ------------------------------------------------------------- */\nMOD_wikiPage('areas', 'Areas & enemies', function () {\n var h = '

Areas & enemies

' +\n '

Every area, what lives in it and at what levels, and what it ' +\n 'drops. Enemy strength is not read off these levels — see the note below the ' +\n 'table.

';\n\n /* Two of the game's 21 areas are not places and listing them misleads:\n nwh \"Somewhere\" — where global.current_z parks whenever you are NOT in\n a fight, populated by creature.default (\"Nothing\").\n tst \"Test\" — reached only from chss.tst, which has id -1 and is in no\n sector, so nothing in the game can travel there.\n The test for \"is this a place\" is whether you can get to it, and neither\n can be. Everything else is listed whether or not you have been. */\n var MOD_WIKI_NOT_PLACES = { nwh: 1, tst: 1 };\n\n var keys = [];\n for (var k in area) {\n if (area[k] && area[k].pop && !MOD_WIKI_NOT_PLACES[k]) keys.push(k);\n }\n /* Ordered by the middle of the level band, which is the closest thing the\n game has to a difficulty order. */\n keys.sort(function (a, b) {\n var mid = function (z) {\n if (!z.pop.length) return 0;\n var s = 0; z.pop.forEach(function (e) { s += (e.lvlmin + e.lvlmax) / 2; });\n return s / z.pop.length;\n };\n return mid(area[a]) - mid(area[b]);\n });\n\n keys.forEach(function (k) {\n var z = area[k];\n /* An area whose baked bands are not finite can never spawn anything — see\n the note rendered under its table. Derived rather than listed by name, so\n it stays true if the game changes. */\n var broken = !z.popc || z.popc.length !== z.pop.length ||\n z.popc.some(function (band) { return !band || !isFinite(band[1] - band[0]); });\n h += '

' + MOD_WIKI.safe(z.name) +\n ' ' + k + '' +\n (broken ? 'nothing spawns' : '') +\n '' + (z.size === -1 ? 'endless' : z.size + ' to clear') +\n '

';\n if (z.pop.length) {\n /* The spawn share is NOT pop[i].c. z_bake normalises those weights into\n area.popc — a list of [lo,hi] bands that mon_gen rolls a uniform\n random against — and the weights do not have to sum to 1, so they are\n off wherever they don't. The Southern forest's .35/.45/.25 are really\n 33/43/24%. Read the bands the game actually rolls against. */\n var bands = z.popc || [];\n h += '';\n z.pop.forEach(function (e, i) {\n var c = e.crt || {};\n var band = bands[i];\n var share = band ? band[1] - band[0] : NaN;\n h += '' +\n '' +\n '';\n });\n h += '
' + MOD_WIKI.safe(c.name || '?') + 'lv ' + e.lvlmin +\n (e.lvlmax !== e.lvlmin ? '–' + e.lvlmax : '') + '' + (isFinite(share)\n ? Math.round(share * 100) + '% of spawns'\n : '—') + '
';\n if (broken) {\n h += '
Nothing spawns here. The area\\'s entries carry ' +\n 'no spawn weight, so the table the game rolls against comes out as ' +\n 'NaN and no creature can ever match it. A bug in the base game, left ' +\n 'as it is — nothing in the game travels to this area either.
';\n }\n }\n if (z.drop && z.drop.length) {\n var d = z.drop.map(function (x) {\n return MOD_WIKI.safe((x.item && x.item.name) || '?') +\n ' ' + ((x.c || 0) * 100).toFixed(2) + '%';\n }).join(', ');\n h += '
Drops: ' + d + '
';\n }\n h += '
';\n });\n\n var fightsOrig = MOD_FIGHTS.mode === 'original';\n h += '

How hard an enemy is

' +\n '

It depends on the Fights setting, which is ' +\n (fightsOrig ? 'Original' : 'Scaled') + ' now. On Original (the default) ' +\n 'every area the author wrote is exactly as he wrote it: his creatures, his ' +\n 'level ranges, and you out-grow them as you do in his game. On Scaled every ' +\n 'area is sized to you. The three areas past the golem arena and the rank ' +\n 'duels are sized to you either way.

' +\n '

Sizing to you works like this. Enemies are not fitted to a level. Each spawn is solved against your ' +\n 'actual power at the moment it appears, aiming at roughly ' + MOD_ENEMY.kill +\n ' swings to kill it and ' + MOD_ENEMY.die + ' before it kills you, with a ' +\n 'guaranteed margin between the two so a fight is never a coin flip. Variety ' +\n 'comes from the area\\'s level band and each creature\\'s own stat weighting, ' +\n 'not from difficulty scaling.

' +\n '

The reason for that: within the first stage alone you go from 1 STR to ' +\n 'around 265, so anything anchored to a level is either trivial at the end of ' +\n 'a stage or impossible at the start of one.

';\n return h;\n});\n\n/* --- 8. items -------------------------------------------------------------\n The one place where \"list everything\" needs help: 321 of the game's 371\n plain items are the same inventory type, so grouping by type alone leaves a\n single group of 321. Anything past MOD_WIKI_ALPHA_AT gets an alphabetical\n sub-split as well — the oldest index pattern there is, and derived rather\n than invented, which a hand-made taxonomy would not be.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_ALPHA_AT = 60;\n\n/* The game's own inventory tabs (isort): ALL / WPN / EQP / USE / OTHER. Using\n its categories rather than new ones means a group here is a tab there. */\nvar MOD_WIKI_STYPE = { 2: 'Weapons', 3: 'Equipment', 4: 'Usable', 5: 'Other' };\n\nfunction MOD_wikiItemRow(it) {\n var val = it.v !== undefined ? it.v : (it.val !== undefined ? it.val : null);\n return '
' +\n '

' + MOD_WIKI.safe(it.name) +\n (it.rar > 1 ? 'rarity ' + it.rar + '' : '') +\n (val !== null ? '' + MOD_wikiNum(val) + ' coin' : '') +\n (it.amount > 0 ? '×' + it.amount + '' : '') +\n '

' + MOD_wikiDesc(it.desc) + '
';\n}\n\n/* The letter a name files under. Several items are quoted — the master's\n manuals are literally named \"Sword Saint Manual\" — and filing those under a\n quotation mark is no use to anyone looking for S. */\nfunction MOD_wikiLetter(name) {\n var m = String(name || '').replace(/^[^0-9A-Za-z]+/, '');\n return (m.charAt(0) || '?').toUpperCase();\n}\n\nfunction MOD_wikiByLetter(a, b) {\n var x = MOD_wikiLetter(a.name) + String(a.name || '').toLowerCase();\n var y = MOD_wikiLetter(b.name) + String(b.name || '').toLowerCase();\n return x < y ? -1 : (x > y ? 1 : 0);\n}\n\n/* Flat below the threshold, alphabetically sub-grouped above it. Ranges are cut\n from the entries actually present, so they stay even as content is added. */\nfunction MOD_wikiAlpha(entries, render, level) {\n if (entries.length <= MOD_WIKI_ALPHA_AT) return entries.map(render).join('');\n var per = Math.ceil(entries.length / Math.ceil(entries.length / 40));\n var out = '';\n for (var i = 0; i < entries.length; i += per) {\n var chunk = entries.slice(i, i + per);\n var first = MOD_wikiLetter(chunk[0].name);\n var last = MOD_wikiLetter(chunk[chunk.length - 1].name);\n out += MOD_wikiGroup(first === last ? first : first + '–' + last,\n chunk.length, chunk.map(render).join(''), level || 4);\n }\n return out;\n}\n\nMOD_wikiPage('items', 'Items', function () {\n var h = '

Items

' +\n '

Everything the game defines, whether or not you have seen ' +\n 'it. Grouped the way your inventory groups it. Values are the base sell ' +\n 'price where the game gives one.

';\n\n /* item[] first, split by the inventory tab it appears under. */\n var buckets = {};\n for (var k in item) {\n var it = item[k];\n if (!it || !it.name || it.name === 'dummy') continue;\n var lab = MOD_WIKI_STYPE[it.stype] || 'Uncategorised';\n (buckets[lab] = buckets[lab] || []).push(it);\n }\n ['Usable', 'Other', 'Weapons', 'Equipment', 'Uncategorised'].forEach(function (lab) {\n var list = buckets[lab];\n if (!list || !list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(lab, list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n\n /* and the equipment objects, which live in their own globals */\n [['Weapons', wpn], ['Armour', eqp], ['Shields', sld], ['Accessories', acc]]\n .forEach(function (g) {\n var list = [];\n for (var kk in g[1]) {\n if (g[1][kk] && g[1][kk].name && g[1][kk].name !== 'dummy') list.push(g[1][kk]);\n }\n if (!list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(g[0], list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n return h;\n});\n\n/* --- 9. actions ----------------------------------------------------------- */\nMOD_wikiPage('actions', 'Actions', function () {\n var unlock = {};\n MOD_ACTION_UNLOCKS.forEach(function (u) {\n if (u.act) unlock[u.act.id] = (skl[u.skill] ? skl[u.skill].name : u.skill) +\n ' level ' + u.lv;\n });\n if (typeof act.mod_qi !== 'undefined') {\n unlock[act.mod_qi.id] = 'clearing the dojo\\'s Easiest, Easy and Normal fights';\n }\n\n var h = '

Actions

' +\n '

What you can be doing. One at a time, and only where the ' +\n 'action makes sense — unless \"Unrestricted actions\" is ticked in settings, ' +\n 'which lets several run at once anywhere. Both limits are deliberate; the ' +\n 'switch is there because they are not to everyone\\'s taste.

';\n\n var keys = [];\n for (var k in act) if (act[k] && act[k].name && act[k].name !== 'dummy') keys.push(k);\n keys.sort(function (a, b) { return act[a].name < act[b].name ? -1 : 1; });\n\n keys.forEach(function (k) {\n var a = act[k];\n var how = unlock[a.id];\n h += '
' +\n '

' + MOD_WIKI.safe(a.name) +\n (a.have === true ? 'unlocked'\n : 'locked') +\n (how ? 'from ' + MOD_WIKI.safe(how) + '' : '') +\n '

' + MOD_wikiDesc(a.desc) + '
';\n });\n return h;\n});\n\n/* --- 10. the dojo --------------------------------------------------------- */\nMOD_wikiPage('dojo', 'The dojo', function () {\n var h = '

The dojo

' +\n '

The instructor promises a reward every five levels. The base ' +\n 'game stops delivering at 30; the mod carries the same ladder to 110, on the ' +\n 'same terms.

' +\n '

Only the lowest rung you have not claimed is offered, which is how the ' +\n 'first six behave. Rewards are sized against the character experience curve ' +\n '(4×level3 + level2) — a level at 110 costs 5.3 ' +\n 'million, which is why the base game\\'s best pill is worth 0.3% of one there.

' +\n '' +\n '';\n\n MOD_DOJO_RUNGS.forEach(function (r) {\n var pill = item[r.pill];\n var extra = [];\n if (r.realmPill) extra.push('breakthrough pill for realm ' + r.realmPill);\n if (r.manual) extra.push('a choice of master\\'s manual');\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
LevelRewardCoinAlso
' + r.lv + '' + (pill ? MOD_WIKI.safe(pill.name) + ' ×' + r.n\n : '—') + '' + MOD_wikiNum(r.coin) + '' + (extra.join(', ') || '—') + '' + (global.flags[r.flag] ? 'claimed' : '') +\n '
' +\n\n '

Spirit pills

' +\n '

Character experience in a bottle. The four the mod adds continue the ' +\n 'game\\'s own series up to the new curve.

' +\n '' +\n '';\n MOD_PILLS.forEach(function (p) {\n h += '';\n });\n h += '
PillExperience
' + MOD_WIKI.safe(p[2]) + '' +\n p[3].toLocaleString() + '
' +\n\n '

Master\\'s manuals

' +\n '

A third grade of skillbook, offered at levels 50, 75 and 100 as a choice ' +\n 'the same way the first one was. Each is worth +' +\n Math.round(MOD_MANUAL_RATE * 100) + '% experience in its skill, permanently.

' +\n '' +\n '';\n MOD_MANUALS.forEach(function (m) {\n h += '';\n });\n h += '
ManualSkill
' + MOD_WIKI.safe(m[2]) + '' +\n MOD_WIKI.safe(skl[m[0]] ? (skl[m[0]].bname || skl[m[0]].name) : m[0]) +\n '
';\n return h;\n});\n\n/* --- 11. mechanics -------------------------------------------------------- */\nMOD_wikiPage('mechanics', 'Mechanics', function () {\n return '

Mechanics

' +\n\n '

Damage is subtractive, in both directions

' +\n '

A hit does the attacker\\'s strength minus the defender\\'s strength, plus ' +\n 'the weapon, plus one, before criticals. A defender\\'s STR is their ' +\n 'armour — there is no separate armour stat doing that job.

' +\n '

Two consequences worth carrying around:

' +\n '
    ' +\n '
  • Damage does not taper, it stops. Once a defender\\'s STR passes ' +\n 'yours, your hits do not get small; they land for nothing. Progress against ' +\n 'something too strong is not slow, it is zero.
  • ' +\n '
  • A stat ratio tells you nothing. Comparing your HP to an enemy\\'s ' +\n 'strength ignores the subtraction entirely and reads several times off in ' +\n 'either direction.
  • ' +\n '
' +\n '

This is also the reason enemies are never scaled by raising ' +\n 'their strength: it would raise their offence and their armour together, and ' +\n 'the second effect arrives first.

' +\n\n '

Criticals carry the late game

' +\n '

Critical rate reaches about a third at the top of the ladder and a ' +\n 'critical is roughly eight times a normal swing, so around three quarters of ' +\n 'all damage done at that point is critical damage. Anything that raises ' +\n 'critical rate or critical damage is worth more than it looks; Sharp Eye and ' +\n 'War are the two that do.

' +\n\n '

Hit chance

' +\n '

Landing a hit is a contest of agility. It is a ratio rather than a ' +\n 'subtraction, so agility behaves more gently than strength does — falling ' +\n 'behind on it costs you accuracy, not everything.

' +\n\n '

Skill sections and parents

' +\n '

Each of the ' + Object.keys(MOD_SECTIONS).length + ' sections has a parent ' +\n 'skill. A parent earns ' + Math.round(MOD_PARENT.feed * 100) + '% of everything ' +\n 'its children earn and grants no stats of its own. In return, a child below ' +\n 'the parent\\'s level trains ' + Math.round(MOD_PARENT.perLevel * 100) +\n '% faster per level of lag, up to ' + MOD_PARENT.maxGap + ' levels behind. ' +\n 'Bringing one section up makes every skill in it cheaper to catch up.

' +\n\n '

Saving

' +\n '

Three slots, switched from the bottom bar. The game builds its world once ' +\n 'at startup and cannot unload a save, so switching slots or starting a new ' +\n 'game reloads the page. Exporting a save before running anything unusual is ' +\n 'always the safe move.

';\n});\n\n/* --- the document ---------------------------------------------------------\n One string, no fetches, no dependencies: this has to work from file:// as\n readily as from a server.\n ------------------------------------------------------------------------- */\n\nfunction MOD_wikiCss() {\n return [\n ':root{color-scheme:dark}',\n '*{box-sizing:border-box}',\n /* Menlo and Consolas before the generic: with MS Gothic missing (any Mac)\n \"monospace\" is Courier, which Safari sets wider than Chrome does, and\n table cells wrapped on one and not the other. */\n 'body{margin:0;background:#060a1c;color:#dfe6f5;font:14px/1.6 \"MS Gothic\",' +\n '\"MS Mincho\",Menlo,Consolas,\"DejaVu Sans Mono\",monospace;display:flex;min-height:100vh}',\n 'a{color:#7cb0ff}',\n 'nav{width:210px;flex:0 0 210px;background:#081040;border-right:1px solid #46a;' +\n 'position:sticky;top:0;height:100vh;overflow:auto;padding:14px 0}',\n 'nav h2{font-size:13px;color:#9fb4d8;margin:0 0 10px 14px;letter-spacing:1px}',\n 'nav a{display:block;padding:6px 14px;text-decoration:none;color:#cfe0ff;' +\n 'border-left:3px solid transparent}',\n 'nav a:hover{background:#154080}',\n 'nav a.on{background:#123070;border-left-color:#7cb0ff;color:#fff}',\n 'nav .srch{margin:0 10px 12px;width:calc(100% - 20px);background:#050a20;' +\n 'color:#dfe6f5;border:1px solid #46a;padding:5px;font:inherit}',\n 'nav .xall{display:block;margin:12px 10px 0;color:#8296b8;font-size:12px;' +\n 'cursor:pointer;-webkit-user-select:none;user-select:none}',\n 'nav .xall input{vertical-align:-1px;margin-right:4px}',\n 'details.wk-g{border:1px solid #17224a;background:#0a1130;margin:8px 0}',\n 'details.wk-g>summary{cursor:pointer;padding:7px 12px;list-style:none;' +\n 'background:#0d1740}',\n 'details.wk-g>summary:hover{background:#154080}',\n 'details.wk-g>summary::-webkit-details-marker{display:none}',\n // a caret drawn rather than borrowed, so it is the same in every browser\n 'details.wk-g>summary::before{content:\"\\\\25B8\";color:#7cb0ff;' +\n 'display:inline-block;width:1em;transition:transform .12s}',\n 'details.wk-g[open]>summary::before{transform:rotate(90deg)}',\n 'details.wk-g>summary h2,details.wk-g>summary h3{display:inline;margin:0;' +\n 'font-size:15px;color:#fff}',\n '.wk-gi{padding:2px 12px 10px}',\n 'details.wk-g details.wk-g>summary h4{display:inline;margin:0;font-size:13px;' +\n 'color:#cfe0ff;font-weight:normal}',\n 'details.wk-g details.wk-g>summary{padding:5px 10px;background:#0b1435}',\n 'main{flex:1;min-width:0;padding:22px 30px 80px;max-width:1000px}',\n 'h1{font-size:26px;margin:0 0 6px;color:#fff;border-bottom:1px solid #46a;' +\n 'padding-bottom:8px}',\n 'h2{font-size:19px;color:#bcd4ff;margin:30px 0 8px}',\n 'h3{font-size:15px;color:#fff;margin:0 0 4px}',\n 'p.lede{color:#a8bcdc;margin:0 0 18px}',\n '.dim{color:#8296b8;font-weight:normal}',\n '.flav{font-size:12px;font-style:italic}',\n '.note{background:#0b1638;border-left:3px solid #46a;padding:8px 12px;' +\n 'color:#a8bcdc;margin:14px 0}',\n 'code,pre{background:#0b1638;border:1px solid #24356e;padding:1px 5px;' +\n 'color:#9fd0ff}',\n 'pre{padding:10px;overflow-x:auto}',\n 'dl dt{color:#fff;margin-top:14px}',\n 'dl dd{margin:2px 0 0 0;color:#a8bcdc}',\n 'ul{color:#a8bcdc}',\n 'table.wk-tbl{border-collapse:collapse;width:100%;margin:10px 0;' +\n 'display:block;overflow-x:auto}',\n 'table.wk-tbl th{text-align:left;color:#9fb4d8;font-weight:normal;' +\n 'border-bottom:1px solid #46a;padding:5px 9px;white-space:nowrap}',\n 'table.wk-tbl td{border-bottom:1px solid #17224a;padding:5px 9px;' +\n 'vertical-align:top}',\n 'table.wk-tbl tr.done td{color:#cfe0ff}',\n 'table.wk-tbl tr.here{background:#123070}',\n '.num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}',\n '.wk-e{border:1px solid #17224a;background:#081040;padding:9px 12px;' +\n 'margin:7px 0;border-radius:2px}',\n 'tr.wk-e{border:0;background:none}',\n '.wk-e.done{border-color:#2a4a8c}',\n '.wk-e h3{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}',\n '.lv{margin-left:auto;font-size:12px;color:#8296b8;font-weight:normal}',\n '.lv.capped{color:#ffd24a}',\n '.rank{font-size:12px;color:#8296b8;font-weight:normal}',\n '.desc{color:#a8bcdc;font-size:13px}',\n '.proj{color:#ffd24a;font-size:12px;margin-top:3px}',\n '.tag{font-size:11px;border:1px solid #46a;color:#9fb4d8;padding:0 5px;' +\n 'border-radius:2px;font-weight:normal}',\n '.tag.mod{border-color:#7a4ea8;color:#c07bff}',\n '.tag.done{border-color:#2e7d4f;color:#6fdc9a}',\n '.tag.here{border-color:#ffd24a;color:#ffd24a}',\n '.tag.warn{border-color:#a8562e;color:#ff9a5c}',\n 'table.wk-perks{border-collapse:collapse;margin:6px 0 0;width:100%}',\n 'table.wk-perks td{padding:2px 8px 2px 0;font-size:12px;color:#8296b8;' +\n 'border-bottom:1px solid #101a3c}',\n 'table.wk-perks tr.done td{color:#cfe0ff}',\n 'table.wk-perks td.tick{color:#6fdc9a;width:1em;text-align:right}',\n // fixed columns so the level bands line up between one area and the next\n 'table.wk-pop{table-layout:fixed}',\n 'table.wk-pop td:first-child{width:60%}',\n 'table.wk-pop td:nth-child(2){width:20%}',\n '.wk-title .tname{font-weight:bold}',\n '.wk-now{background:#0b1638;border:1px solid #24356e;padding:10px 14px;' +\n 'margin:0 0 18px;display:inline-block;min-width:320px}',\n '.wk-now h3{color:#9fb4d8;font-weight:normal;font-size:13px;' +\n 'letter-spacing:1px;margin-bottom:6px}',\n '.wk-now td{padding:2px 16px 2px 0}',\n '.wk-now td:last-child{color:#fff;text-align:right}',\n '.pg{display:none}.pg.on{display:block}',\n 'footer{color:#5a6c8c;font-size:12px;border-top:1px solid #17224a;' +\n 'margin-top:40px;padding-top:12px}',\n '@media(max-width:760px){body{display:block}nav{width:auto;height:auto;' +\n 'position:static;flex:none}main{padding:16px}}'\n ].join('\\n');\n}\n\n/* Filtering, page switching and the hash. Small enough to inline, and inlining\n it is the point — the document has to stand on its own. */\nfunction MOD_wikiJs() {\n return [\n 'var pages=[].slice.call(document.querySelectorAll(\".pg\"));',\n 'var links=[].slice.call(document.querySelectorAll(\"nav a[data-p]\"));',\n 'function show(id){',\n ' var found=false;',\n ' pages.forEach(function(p){var on=p.id===\"pg-\"+id;if(on)found=true;',\n ' p.className=\"pg\"+(on?\" on\":\"\")});',\n ' if(!found&&pages.length){pages[0].className=\"pg on\";id=pages[0].id.slice(3)}',\n ' links.forEach(function(a){a.className=a.dataset.p===id?\"on\":\"\"});',\n ' window.scrollTo(0,0);return id;}',\n 'links.forEach(function(a){a.addEventListener(\"click\",function(e){',\n ' e.preventDefault();location.hash=a.dataset.p;show(a.dataset.p)})});',\n 'window.addEventListener(\"hashchange\",function(){show(location.hash.slice(1))});',\n 'show(location.hash.slice(1)||\"start\");',\n // Search filters entries, hides groups left with none, and OPENS the groups\n // that still have some — a match hidden inside a collapsed group reads as\n // no match at all, which is worse than not having search.\n 'var box=document.getElementById(\"srch\");',\n 'var xall=document.getElementById(\"xall\");',\n 'var groups=[].slice.call(document.querySelectorAll(\"details.wk-g\"));',\n 'function filter(){',\n ' var q=box.value.trim().toLowerCase();',\n ' pages.forEach(function(p){',\n ' [].slice.call(p.querySelectorAll(\".wk-e\")).forEach(function(e){',\n ' e.style.display=(!q||e.textContent.toLowerCase().indexOf(q)>=0)?\"\":\"none\"})});',\n // Every page, not just the one on screen: the nav says how many entries\n // match in each, dims the ones with none, and Enter jumps to the first\n // page that has any. A prose page with no entries counts as one if its text\n // contains the query.\n ' links.forEach(function(a){',\n ' var pg=document.getElementById(\"pg-\"+a.dataset.p),n=0;',\n ' if(a.dataset.l===undefined)a.dataset.l=a.textContent;',\n ' if(q&&pg){var es=[].slice.call(pg.querySelectorAll(\".wk-e\"));',\n ' if(es.length)es.forEach(function(e){if(e.textContent.toLowerCase().indexOf(q)>=0)n++});',\n ' else if(pg.textContent.toLowerCase().indexOf(q)>=0)n=1}',\n ' a.dataset.n=n;a.style.opacity=(q&&!n)?\".4\":\"\";',\n ' a.textContent=a.dataset.l+(q&&n?\" (\"+n+\")\":\"\")});',\n ' groups.forEach(function(g){',\n ' var any=[].slice.call(g.querySelectorAll(\".wk-e\")).some(function(e){',\n ' return e.style.display!==\"none\"});',\n ' g.style.display=any?\"\":\"none\";',\n ' g.open=xall.checked||(!!q&&any)}); }',\n 'box.addEventListener(\"input\",filter);',\n 'box.addEventListener(\"keydown\",function(e){',\n ' if(e.key===\"Escape\"){box.value=\"\";filter()}',\n ' if(e.key===\"Enter\"&&box.value.trim()){',\n ' var cur=links.filter(function(a){return a.className===\"on\"})[0];',\n ' if(cur&&+cur.dataset.n)return;',\n ' var hit=links.filter(function(a){return +a.dataset.n>0})[0];',\n ' if(hit){location.hash=hit.dataset.p;show(hit.dataset.p)}}});',\n // Everything is collapsed by default because three of these pages run past\n // 40,000px open. This is the way back to one long document, which is what\n // the browser's own find needs.\n 'xall.addEventListener(\"change\",filter);'\n ].join('\\n');\n}\n\nfunction MOD_wikiBuild() {\n var nav = '', body = '';\n MOD_WIKI_PAGES.forEach(function (pg) {\n nav += '' +\n MOD_WIKI.safe(pg.label) + '';\n var inner;\n /* One page throwing must not take the wiki down with it — the page says so\n and the rest still renders. */\n try { inner = pg.build(); }\n catch (e) {\n inner = '

' + MOD_WIKI.safe(pg.label) + '

This page ' +\n 'could not be built: ' + MOD_WIKI.safe(e.message) + '

';\n console.warn('[mod] wiki page \"' + pg.id + '\" failed: ' + e.message);\n }\n body += '
' + inner + '
';\n });\n\n var stamp = new Date().toLocaleString();\n return '' +\n '' +\n '' + MOD_WIKI.safe(MOD_WIKI.title) + '' +\n '' +\n '' +\n '
' + body +\n '
Generated from the running game at ' + MOD_WIKI.safe(stamp) +\n ' — mod ' + MOD_WIKI.safe(MOD.version) + '. Everything here is read from the ' +\n 'live data, including your own save, so it cannot fall out of step with the ' +\n 'game. The game is truezhangwei\\'s; the mod is not endorsed by the author.' +\n '
' +\n ' inside content closing the block early, so that is the\n one thing scrubbed. */\n safe: function (h) {\n return String(h === undefined || h === null ? '' : h).replace(/<\\/script/gi, '<\\\\/script');\n }\n};\n\n/* Descriptions carry real HTML (coloured spans, the game's separator div), so\n they are passed through rather than escaped — but the separator becomes a\n rule, since a wiki row is not a tooltip. */\nfunction MOD_wikiDesc(d) {\n var s;\n try { s = (typeof d === 'function') ? d() : d; } catch (e) { s = ''; }\n if (!s) return '';\n s = String(s).split('
 
').join('
');\n return MOD_WIKI.safe(s);\n}\n\nfunction MOD_wikiNum(n) {\n if (!isFinite(n)) return '—';\n return Math.round(n).toLocaleString();\n}\n\n/* An effect line at a level the player has not reached yet. The effect\n functions read the skill they are handed, so the level is borrowed and put\n back; anything that throws simply gets no projection. */\nfunction MOD_wikiEffectAt(key, sk, lvl) {\n var eff = MOD_EFFECTS[key];\n if (!eff) return '';\n var was = sk.lvl, out = '';\n try { sk.lvl = lvl; out = eff(sk) || ''; }\n catch (e) { out = ''; }\n finally { sk.lvl = was; }\n return MOD_WIKI.safe(out);\n}\n\n/* --- page builders --------------------------------------------------------\n Each returns the inner HTML of one page. `wk-e` marks a searchable entry and\n `wk-g` a group that hides itself when all its entries are filtered out.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_PAGES = [];\n\nfunction MOD_wikiPage(id, label, build) {\n MOD_WIKI_PAGES.push({ id: id, label: label, build: build });\n}\n\n/* A collapsible group.\n\n Not decoration: with every skill, item and title listed, three of the eleven\n pages ran past 40,000 pixels — a hundred screens of scrolling, which is the\n same as not having the content at all. Collapsed, each of those pages opens\n as an index you can read in one screen and expand where you want.\n\n
rather than a click handler, so it still works with JavaScript off,\n the browser's own find can be made to reach inside it, and the summary is\n keyboard-reachable for free. `level` is the heading level for the summary,\n since a group is sometimes a section of a page and sometimes a subsection. */\nfunction MOD_wikiGroup(heading, count, inner, level) {\n var hn = 'h' + (level || 2);\n return '
<' + hn + '>' + heading +\n (count !== null && count !== undefined\n ? ' (' + count + ')' : '') +\n '
' + inner + '
';\n}\n\n/* --- 1. orientation ------------------------------------------------------- */\nMOD_wikiPage('start', 'Start here', function () {\n var cap = MOD_levelCap();\n var realm = MOD_realm();\n var h = '

proto23

' +\n '

An idle RPG by truezhangwei, plus a mod. You train skills by ' +\n 'doing things, the things you can do open up as the story does, and none of ' +\n 'it is in a hurry.

' +\n\n '

Where you are right now

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Character level' + MOD_wikiNum(you.lvl) + '
Skill level cap' + cap + ' — ' +\n MOD_WIKI.safe(MOD_CAP.name) + '
Cultivation realm' + MOD_WIKI.safe(realm.name) +\n ' (' + realm.n + ' of ' + (MOD_REALMS.length - 1) + ')
Titles held' + MOD_titleCount() + ' — renown ' +\n (skl.rnwn ? skl.rnwn.lvl : 0) + '
Coin' + MOD_wikiNum(you.wealth || 0) + '
' +\n\n '

The four things worth knowing early

' +\n '
' +\n '
Your skill levels are capped by the story, not by grinding.
' +\n '
The cap starts at 10 and ends at 110, and it moves when you reach a new ' +\n 'place — not when you earn enough experience. If a skill has stopped ' +\n 'levelling, you are not doing it wrong; you need to go somewhere. See ' +\n 'Progression.
' +\n\n '
Armour is subtraction, so levels matter more than they look.
' +\n '
A hit does the attacker\\'s STR minus the defender\\'s STR. That means a ' +\n 'small gap in strength is a large gap in damage, and a big enough one takes ' +\n 'your damage to nothing at all rather than merely reducing it. See ' +\n 'Mechanics.
' +\n\n '
Most skills have a parent that pulls them along.
' +\n '
Every section has a parent skill that earns a share of everything its ' +\n 'children do, and any child lagging behind the parent trains faster — up to ' +\n 'twice as fast at ' + MOD_PARENT.maxGap + ' levels behind. A neglected skill is ' +\n 'cheap to bring up later, which is the only reason having this many is ' +\n 'workable.
' +\n\n '
Cultivation is a second ladder, and it is not automatic.
' +\n '
Circulating qi levels a skill; the skill does not raise your realm. ' +\n 'Reaching the level opens a bottleneck, and breaking through it costs a pill ' +\n 'and can fail. See Cultivation.
' +\n '
' +\n\n '

What is the mod and what is the game

' +\n '

The game is index.html and it is entirely truezhangwei\\'s ' +\n 'work. Everything in mod.js is a modification of it and is not ' +\n 'endorsed by the author. Pages here mark added content with ' +\n 'mod where the distinction matters. The full ' +\n 'list of changes is in the changelog, on the bottom bar.

';\n return h;\n});\n\n/* --- 2. progression ------------------------------------------------------- */\nMOD_wikiPage('progress', 'Progression', function () {\n var cap = MOD_levelCap();\n var h = '

Progression

' +\n '

Skill levels are capped by how far the story has gone. Ten ' +\n 'rungs, 10 through 110. The cap applies to every skill at once; Renown is ' +\n 'the single exception, since it is counted rather than trained.

' +\n '' +\n '';\n\n MOD_TIERS.forEach(function (t) {\n var got = null;\n try { got = MOD_tierFlag(t); } catch (e) {}\n var here = (t.cap === cap);\n var need = t.needText ? t.needText\n : (t.flags.length ? t.flags.join(' or ') : 'the start of the game');\n h += '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
CapStageWhat opens it
' + t.cap + '' + MOD_WIKI.safe(t.name) + '' + MOD_WIKI.safe(need) + '' + (here ? 'you are here'\n : got ? 'reached' : '') + '
' +\n\n '

The experience curve

' +\n '

The game\\'s own skill curve is super-exponential — level 109 to 110 ' +\n 'alone costs ' + MOD_wikiNum(MOD_vanillaExpnext(109)) + ' experience, so the top of the ladder ' +\n 'is unreachable by a factor of about ten trillion. But the author only ever ' +\n 'designed skills up to level 15, and below that his curve is fine.

' +\n (MOD_XP.vanillaTo > 0\n ? '

So the mod keeps it. Below level ' + MOD_XP.vanillaTo + ' a skill costs ' +\n 'exactly what the original charges; from there it rises by ×' +\n MOD_XP.ratio.toFixed(4) + ' a level, a ratio solved so the climb to 110 takes ' +\n 'the same time it always did.'\n : '

You have chosen the Mod before 4.2 pacing: a plain geometric curve, ' +\n '×' + MOD_XP.ratio.toFixed(3) + ' a level from level 0, much faster than the ' +\n 'original early on. The Original pacing in settings puts back the ' +\n 'author\\'s own cost below level 10.') +\n ' Character level uses the game\\'s own curve, untouched.

' +\n '' +\n '';\n [1, 5, 9, 10, 15, 25, 50, 75, 90, 109].forEach(function (l) {\n h += '';\n });\n h += '
LevelExperience for the nextOriginal game
' + l +\n '' + MOD_wikiNum(MOD_expnextFor(l)) + '' +\n MOD_wikiNum(MOD_vanillaExpnext(l)) + '
';\n return h;\n});\n\n/* --- 3. skills ------------------------------------------------------------ */\nMOD_wikiPage('skills', 'Skills', function () {\n var cap = MOD_levelCap();\n var bySection = {};\n var keys = [];\n for (var k in skl) {\n var s = skl[k];\n if (!s || typeof s !== 'object' || !s.name) continue;\n keys.push(k);\n }\n keys.forEach(function (k) {\n var s = skl[k];\n var sec = MOD_sectionOf(s);\n (bySection[sec] = bySection[sec] || []).push(k);\n });\n\n var h = '

Skills

' +\n '

' + keys.length + ' skills in ' +\n Object.keys(MOD_SECTIONS).length + ' sections. Every one has perks at ' +\n MOD_LADDER.join(', ') + ', and every one that has milestones grants titles at ' +\n MOD_TITLE_RUNGS.join(', ') + '. Levels and ticks below are yours.

';\n\n Object.keys(MOD_SECTIONS).forEach(function (sec) {\n var list = bySection[sec];\n if (!list || !list.length) return;\n var parentKey = MOD_PARENT_KEY[sec];\n var g = '';\n if (parentKey && skl[parentKey]) {\n g += '

Parent skill: ' +\n MOD_WIKI.safe(skl[parentKey].bname || skl[parentKey].name) +\n ' — earns ' + Math.round(MOD_PARENT.feed * 100) +\n '% of what its children earn, and speeds up any child that lags behind it.

';\n }\n list.sort(function (a, b) { return skl[a].name < skl[b].name ? -1 : 1; });\n list.forEach(function (k) {\n var s = skl[k];\n var lvl = Number(s.lvl) || 0;\n g += '
' +\n '

' + MOD_WIKI.safe(s.bname || s.name) +\n (s.bname ? ' (' + MOD_WIKI.safe(s.name) + ')' : '') +\n '= cap ? ' capped' : '') + '\">lv ' + lvl +\n (lvl >= cap ? ' — at the cap' : '') + '

' +\n '
' + MOD_wikiDesc(s.desc) + '
';\n\n var at110 = MOD_wikiEffectAt(k, s, 110);\n if (at110) g += '
At level 110: ' + at110 + '
';\n\n if (s.mlstn && s.mlstn.length) {\n g += '';\n s.mlstn.forEach(function (m) {\n g += '' +\n '' +\n '' +\n '';\n });\n g += '
' + m.lv + '' + MOD_WIKI.safe(m.p || '—') + '' + (m.g === true ? '✓' : '') + '
';\n }\n g += '
';\n });\n h += MOD_wikiGroup(MOD_WIKI.safe(MOD_SECTIONS[sec]), list.length, g, 2);\n });\n return h;\n});\n\n/* --- 4. cultivation ------------------------------------------------------- */\nMOD_wikiPage('cultivation', 'Cultivation', function () {\n var now = MOD_realm();\n var elig = 0;\n try { elig = MOD_realmEligible().n; } catch (e) {}\n var qic = skl.qic ? skl.qic.lvl : 0;\n\n var h = '

Cultivation

' +\n '

Ten realms above Mortal. Circulating qi levels the Qi ' +\n 'Circulation skill; the skill does not advance the realm. Reaching the level ' +\n 'opens a bottleneck, and breaking through it costs a pill and can fail — the ' +\n 'pill is spent either way.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Realm' + MOD_WIKI.safe(MOD_realmTitle()) + '
Spiritual Root' + MOD_WIKI.safe(MOD_root().name) + '
Qi Circulationlevel ' + qic + '
Insight' + MOD_insight() + '
Bottleneck open' +\n (elig > now.n ? 'yes — ' + MOD_WIKI.safe(MOD_REALMS[now.n + 1].name) +\n ' is within reach' : 'no') + '
Qi Deviation' + (MOD_deviated()\n ? MOD_deviationLeft() + ' in-game minutes left' : 'no') + '
' +\n '

Realm thresholds are the same levels that earn a title rank, so realm N ' +\n 'is the level at which a rank N title arrives. Every realm multiplies all ' +\n 'stats and health, and the multipliers do not stack with each other — the ' +\n 'realm you are in is the one that applies.

' +\n '' +\n '';\n\n MOD_REALMS.forEach(function (r) {\n var pill = r.n >= 1 ? item['mod_bp' + r.n] : null;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmNameQi Circ.StatsHealthPill
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + r.qic + '×' + r.mult + '×' + r.hp + '' + (pill ? MOD_WIKI.safe(pill.name) : '—') +\n '
' +\n\n '

Where the pills come from

' +\n '

Every breakthrough pill has a source in normal play. None of them are ' +\n 'craftable, and none are cheap — a bottleneck you can buy in bulk is not a ' +\n 'bottleneck.

' +\n '' +\n '' +\n '' +\n '' +\n '
RealmsSource
1The dojo instructor, when qi ' +\n 'circulation is first taught
2–5The Herbalist in the village ' +\n '(vendor.pha1), restocked
6–10The dojo\\'s Level Advancement ' +\n 'ladder, and the Pill Tower
' +\n '

Realm 1 is deliberately not sold at the marketplace: ' +\n 'the marketplace is behind a 40%-a-visit chance encounter, and realm 1 opens ' +\n 'long before that is reliable.

' +\n\n '

Breaking through

' +\n '

A bottleneck wants three things, which is what the genre says it wants: ' +\n 'a pill, insight, and enough consolidation to make the ' +\n 'attempt worth making. The pill and the insight are both spent whether the ' +\n 'attempt works or not.

' +\n '

The odds improve the further past the threshold you are, plus your root, ' +\n 'plus time sat in Closed Door Training, and cap short of certain.

' +\n '' +\n '';\n [0, 2, 4, 6, 8].forEach(function (o) {\n var base = Math.min(0.55 + o * 0.05 + MOD_root().odds, 0.95);\n h += '';\n });\n h += '
Levels past the thresholdChanceAfter full seclusion
+' + o + '' +\n Math.round(base * 100) + '%' +\n Math.round(Math.min(base + MOD_CULT.secludeOdds, 0.95) * 100) + '%
' +\n\n '

Insight

' +\n '

Insight is the half of the price that cannot be bought. It comes from ' +\n 'three places, and they are the three the genre names: meditating, fights you ' +\n 'nearly lost, and going somewhere you have not been.

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
SourceRate
Circulate Qi, at a wall' +\n (MOD_CULT.meditateChance * 100).toFixed(1) + '% a second
Closed Door Training' +\n (MOD_CULT.secludeChance * 100).toFixed(1) + '% a second
A kill made under ' +\n Math.round(MOD_CULT.brinkFrac * 100) + '% health — ' +\n (MOD_FIGHTS.mode === 'original'\n ? 'the author\\'s areas rarely come close once you reach them; the three past ' +\n 'the golem arena and the rank duels do, within a few fights, as does an area ' +\n 'you walk into early'\n : 'every fight costs a share of your health, so a few in a row without resting') +\n '' +\n Math.round(MOD_CULT.brinkChance * 100) + '% a kill
Entering an area for the first timealways
' +\n '

The insight a realm asks for is sized to the climb to it: about ' +\n Math.round(MOD_CULT.wallShare * 100) + '% of the Circulate Qi time the climb took, ' +\n 'spent at the wall. Seclusion earns it three times as fast, and full ' +\n 'consolidation takes as long as that on average.

' +\n '' +\n '' +\n '';\n var wkHours = function (sec) {\n return sec >= 7200 ? (sec / 3600).toFixed(sec >= 36000 ? 0 : 1) + ' h' : Math.round(sec / 60) + ' min';\n };\n MOD_REALMS.forEach(function (r) {\n if (!r.n) return;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmClimb (Circulate Qi, 1x)Insight to enterFull seclusionQi Deviation (in-game)
' + MOD_WIKI.safe(r.name) + '' + wkHours(MOD_realmClimbSec(r.n)) + '' + MOD_insightNeed(r.n) + '' + wkHours(MOD_secludeFull(r.n)) + '' + (MOD_devMinutes(r.n) / 1440).toFixed(1) + ' days
' +\n\n '

The nine layers

' +\n '

Each realm is nine layers deep, derived from where Qi Circulation sits ' +\n 'between this realm\\'s threshold and the next one\\'s — so breaking through ' +\n 'puts you at the first layer of the new realm on its own. Layers are worth ' +\n '×' + MOD_CULT.layerMult + ' stats and ×' + MOD_CULT.layerBody +\n ' body each above the first, and the top realm has no rung above it to ' +\n 'measure against, so it stays at its first layer.

' +\n '' +\n '';\n for (var li = 1; li <= MOD_CULT.layers; li++) {\n h += '';\n }\n h += '
LayerCalled
' +\n li + '' + MOD_LAYER_ORD[li] + ' layer, ' +\n MOD_WIKI.safe(MOD_layerStage(li)) + '
' +\n '

At the threshold of the next realm and before breaking ' +\n 'through, you are a half step to it — the genre\\'s own name for the ' +\n 'state this mod calls a bottleneck.

' +\n\n '

Closed Door Training

' +\n '

Seclusion does two things. It consolidates — a full sitting ' +\n '(the table above) is worth +' + Math.round(MOD_CULT.secludeOdds * 100) +\n '% on the attempt — and it shelters: a breakthrough attempted while ' +\n 'you are still sitting in it cannot end in Qi Deviation, only in a lost ' +\n 'pill. It will not start unless you are actually at a wall.

' +\n\n '

Qi Deviation

' +\n '

Failing a breakthrough out in the open leaves your cultivation base ' +\n 'unstable for as long as the seclusion you skipped (the table above, never ' +\n 'under ' + MOD_CULT.devMinutes + ' in-game minutes): stats at ×' +\n MOD_CULT.devStat + ', body at ×' + MOD_CULT.devBody + ', and no second ' +\n 'attempt until it passes. It clears on its own, or at once with a ' +\n 'Qi Settling Pill. It never kills — the death system stays out of it.

' +\n\n '

Heavenly Tribulation

' +\n '

From realm ' + MOD_CULT.tribFrom + ' up, a successful breakthrough is ' +\n 'answered. The cost is a fraction of your maximum health, so it is a gate on ' +\n 'walking in healthy rather than a damage race; the realm bonus you are ' +\n 'standing on is what pays it. Fall to it and the realm does not open.

' +\n '' +\n '';\n MOD_REALMS.forEach(function (r) {\n if (!r.n || r.n < MOD_CULT.tribFrom) return;\n h += '' +\n '' +\n '';\n });\n h += '
RealmBoltsOf your max HP
' + MOD_WIKI.safe(r.name) + '' + MOD_tribulationBolts(r.n) + '' + Math.round(MOD_tribulationCost(r.n) * 100) + '%
' +\n\n '

Spiritual Root

' +\n '

Rolled once, when the dojo finishes teaching you to fight. It is never a ' +\n 'dead end: the worst grade costs cultivation speed, not access, and a ' +\n 'Root Cleansing Pill moves you up a grade.

' +\n '' +\n '';\n var rootTotal = 0;\n MOD_ROOTS.forEach(function (r) { rootTotal += r.weight; });\n MOD_ROOTS.forEach(function (r) {\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
GradeRootChanceCultivationBreakthrough
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + Math.round(r.weight / rootTotal * 100) + '%×' + r.xp + '' + (r.odds >= 0 ? '+' : '') +\n Math.round(r.odds * 100) + '%
';\n return h;\n});\n\n/* --- 5. elemental mastery ------------------------------------------------- */\nMOD_wikiPage('mastery', 'Elemental mastery', function () {\n var h = '

Elemental mastery mod

' +\n '

Six masteries, each with a technique that fires on its own ' +\n 'during a fight. Combat is automatic and there is no ability picker, so a ' +\n 'technique is a chance on each of your swings to become something else — one ' +\n 'that scales with INT and the element rather than with your weapon.

' +\n '

Nothing fires while you are still Mortal: the channels have to be open ' +\n 'first. Chance is ' + (MOD_TECH.perLevel * 100).toFixed(1) + '% per mastery ' +\n 'level plus ' + (MOD_TECH.perRealm * 100).toFixed(1) + '% per realm, capped at ' +\n Math.round(MOD_TECH.cap * 100) + '%; a technique lands for ×' +\n MOD_TECH.power + ' of a normal strike.

' +\n '' +\n '';\n\n var realm = 0;\n try { realm = MOD_realm().n; } catch (e) {}\n MOD_MASTERY.forEach(function (m) {\n var lvl = Number(m.skill.lvl) || 0;\n var ch = realm < 1 ? 0\n : Math.min(lvl * MOD_TECH.perLevel + realm * MOD_TECH.perRealm, MOD_TECH.cap);\n h += '' +\n '' +\n '' +\n '';\n });\n h += '
MasteryTechniqueLevelChance now
' +\n MOD_WIKI.safe(m.skill.name) + '' + MOD_WIKI.safe(m.name) + '' + lvl + '' + (realm < 1 ? 'Mortal'\n : (ch * 100).toFixed(1) + '%') + '
';\n return h;\n});\n\n/* --- 6. titles ------------------------------------------------------------ */\nMOD_wikiPage('titles', 'Titles', function () {\n var perm = MOD_permanentRank();\n var held = MOD_titleCount();\n var total = 0; for (var kk in ttl) if (ttl[kk] && ttl[kk].name) total++;\n\n var h = '

Titles

' +\n '

A title\\'s rank runs 1 to 10 and is derived from the level ' +\n 'that earns it, so a rank is a statement about where in the game you are. ' +\n 'Only the title you are wearing applies — until renown makes the lower ranks ' +\n 'permanent.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '
Titles held' + held + ' of ' + total + '
Renown level' + (skl.rnwn ? skl.rnwn.lvl : 0) +\n ' of ' + MOD_RENOWN_MAX + '
Ranks that apply without wearing' +\n (perm >= 1 ? '1–' + perm : 'none yet') + '
' +\n\n '

Ranks

' +\n '';\n MOD_RANK_AT.forEach(function (lv, i) {\n var st = MOD_rankStyle(i + 1);\n h += '' +\n '';\n });\n h += '
RankEarned from levelColour
' + (i + 1) + '' + lv + '' + '●●●' + '
' +\n\n '

Renown

' +\n '

Renown is not trained. It counts the titles you hold, and each level ' +\n 'multiplies every stat by ' + (1 + MOD_RENOWN_RATE) + ' — compounding, so ' +\n 'level ' + MOD_RENOWN_MAX + ' is ×' +\n (Math.round(MOD_renownMult(MOD_RENOWN_MAX) * 1000) / 1000) + '. At renown N, ' +\n 'every title of rank N or below applies permanently.

' +\n '' +\n '';\n MOD_RENOWN_STEPS.forEach(function (n, i) {\n h += '= n ? 'done' : '') + '\">' +\n '';\n });\n h += '
RenownTitles neededAll statsPassive ranks
' + (i + 1) +\n '' + n + '×' +\n (Math.round(MOD_renownMult(i + 1) * 1000) / 1000) + '1–' + (i + 1) + '
' +\n\n '

Every title

' +\n '

Grouped by the skill that grants it. What is left is either ' +\n 'a ladder of its own — creatures killed, items gathered, the ' +\n 'cultivation realms — or a one-off from the story.

';\n\n var groups = {}, fams = {}, loose = [];\n for (var k in ttl) {\n var t = ttl[k];\n /* Unlike the picker, the wiki KEEPS the author's broken entries and says\n so — same call as area.clg, which is listed and tagged \"nothing spawns\"\n rather than quietly dropped. A reader looking for why a title never\n appears is better served by the row than by its absence. */\n if (!t || t.name === undefined || t.name === null) continue;\n var sk = MOD_TITLE_SKILL[k];\n if (sk && skl[sk]) { (groups[sk] = groups[sk] || []).push(k); continue; }\n var stem = MOD_titleStem(k);\n if (stem) { (fams[stem] = fams[stem] || []).push(k); continue; }\n loose.push(k);\n }\n /* Same rule as the picker: a family of one is not a family. */\n Object.keys(fams).forEach(function (stem) {\n if (fams[stem].length < 2) { loose.push(fams[stem][0]); delete fams[stem]; }\n });\n var order = Object.keys(groups).sort(function (a, b) {\n return skl[a].name < skl[b].name ? -1 : 1;\n });\n\n var row = function (k) {\n var t = ttl[k];\n var st = MOD_rankStyle(t.rar);\n var gen = null;\n for (var i = 0; i < MOD_SKILL_TITLES.length; i++) {\n if (MOD_SKILL_TITLES[i].key === k) { gen = MOD_SKILL_TITLES[i]; break; }\n }\n /* The author left a handful unfinished: ttl.ddcd carries the literal string\n \"null\" for both name and description, and shpt2/shpt3/mone3 are blank.\n None is granted by anything, so none can be earned. */\n var broken = !t.name || t.name === 'null';\n return '
' +\n '' +\n MOD_WIKI.safe(t.name || '(unnamed)') + '' +\n (broken ? 'unfinished' : '') +\n 'rank ' + (t.rar || 1) + '' +\n (gen ? 'lv ' + gen.rung + ' · +' +\n Math.round(gen.xp * 100) + '% ' + MOD_WIKI.safe(skl[gen.skill].name) +\n ' exp' : '') +\n (t.have === true ? 'held' : '') +\n '
' + MOD_wikiDesc(t.desc) + '
';\n };\n\n var byRank = function (a, b) { return (ttl[a].rar || 1) - (ttl[b].rar || 1); };\n order.forEach(function (sk) {\n groups[sk].sort(byRank);\n h += MOD_wikiGroup(MOD_WIKI.safe(skl[sk].bname || skl[sk].name),\n groups[sk].length, groups[sk].map(row).join(''), 3);\n });\n /* The ladders that belong to no skill, by the same derivation the picker\n uses — so the two cannot drift apart. Sorted by the label, since that is\n what the reader scans. */\n var famList = Object.keys(fams)\n .map(function (stem) {\n fams[stem].sort(byRank);\n return { stem: stem,\n label: MOD_titleFamilyLabel(stem, fams[stem].map(function (k) { return ttl[k]; })) };\n })\n .sort(function (a, b) { return a.label < b.label ? -1 : a.label > b.label ? 1 : 0; });\n if (famList.length) {\n /* Nested one level down rather than sitting beside the skills. Eleven more\n top-level summaries cost ~530px on a page that is already the tallest in\n the wiki, and these are a different KIND of thing from a skill anyway. */\n var famInner = '', famTotal = 0;\n famList.forEach(function (f) {\n famTotal += fams[f.stem].length;\n famInner += MOD_wikiGroup(MOD_WIKI.safe(f.label), fams[f.stem].length,\n fams[f.stem].map(row).join(''), 4);\n });\n h += MOD_wikiGroup('Ladders of their own', famTotal, famInner, 3);\n }\n if (loose.length) {\n loose.sort(byRank);\n /* The story titles are the one group with no skill to divide them, and\n there are enough of them to need dividing. Rank rather than the alphabet:\n it is the axis they are already sorted on, and the one a reader cares\n about — a rank is a statement about where in the game a title comes from. */\n var inner;\n if (loose.length <= MOD_WIKI_ALPHA_AT) {\n inner = loose.map(row).join('');\n } else {\n var byRankNo = {};\n loose.forEach(function (k) {\n var r = Math.min(Math.max(Number(ttl[k].rar) || 1, 1), 10);\n (byRankNo[r] = byRankNo[r] || []).push(k);\n });\n inner = '';\n Object.keys(byRankNo).sort(function (a, b) { return a - b; }).forEach(function (r) {\n var st = MOD_rankStyle(r);\n inner += MOD_wikiGroup('Rank ' + r + '', byRankNo[r].length,\n byRankNo[r].map(row).join(''), 4);\n });\n }\n h += MOD_wikiGroup('From the story', loose.length, inner, 3);\n }\n return h;\n});\n\n/* --- 7. areas ------------------------------------------------------------- */\nMOD_wikiPage('areas', 'Areas & enemies', function () {\n var h = '

Areas & enemies

' +\n '

Every area, what lives in it and at what levels, and what it ' +\n 'drops. Enemy strength is not read off these levels — see the note below the ' +\n 'table.

';\n\n /* Two of the game's 21 areas are not places and listing them misleads:\n nwh \"Somewhere\" — where global.current_z parks whenever you are NOT in\n a fight, populated by creature.default (\"Nothing\").\n tst \"Test\" — reached only from chss.tst, which has id -1 and is in no\n sector, so nothing in the game can travel there.\n The test for \"is this a place\" is whether you can get to it, and neither\n can be. Everything else is listed whether or not you have been. */\n var MOD_WIKI_NOT_PLACES = { nwh: 1, tst: 1 };\n\n var keys = [];\n for (var k in area) {\n if (area[k] && area[k].pop && !MOD_WIKI_NOT_PLACES[k]) keys.push(k);\n }\n /* Ordered by the middle of the level band, which is the closest thing the\n game has to a difficulty order. */\n keys.sort(function (a, b) {\n var mid = function (z) {\n if (!z.pop.length) return 0;\n var s = 0; z.pop.forEach(function (e) { s += (e.lvlmin + e.lvlmax) / 2; });\n return s / z.pop.length;\n };\n return mid(area[a]) - mid(area[b]);\n });\n\n keys.forEach(function (k) {\n var z = area[k];\n /* An area whose baked bands are not finite can never spawn anything — see\n the note rendered under its table. Derived rather than listed by name, so\n it stays true if the game changes. */\n var broken = !z.popc || z.popc.length !== z.pop.length ||\n z.popc.some(function (band) { return !band || !isFinite(band[1] - band[0]); });\n h += '

' + MOD_WIKI.safe(z.name) +\n ' ' + k + '' +\n (broken ? 'nothing spawns' : '') +\n '' + (z.size === -1 ? 'endless' : z.size + ' to clear') +\n '

';\n if (z.pop.length) {\n /* The spawn share is NOT pop[i].c. z_bake normalises those weights into\n area.popc — a list of [lo,hi] bands that mon_gen rolls a uniform\n random against — and the weights do not have to sum to 1, so they are\n off wherever they don't. The Southern forest's .35/.45/.25 are really\n 33/43/24%. Read the bands the game actually rolls against. */\n var bands = z.popc || [];\n h += '';\n z.pop.forEach(function (e, i) {\n var c = e.crt || {};\n var band = bands[i];\n var share = band ? band[1] - band[0] : NaN;\n h += '' +\n '' +\n '';\n });\n h += '
' + MOD_WIKI.safe(c.name || '?') + 'lv ' + e.lvlmin +\n (e.lvlmax !== e.lvlmin ? '–' + e.lvlmax : '') + '' + (isFinite(share)\n ? Math.round(share * 100) + '% of spawns'\n : '—') + '
';\n if (broken) {\n h += '
Nothing spawns here. The area\\'s entries carry ' +\n 'no spawn weight, so the table the game rolls against comes out as ' +\n 'NaN and no creature can ever match it. A bug in the base game, left ' +\n 'as it is — nothing in the game travels to this area either.
';\n }\n }\n if (z.drop && z.drop.length) {\n var d = z.drop.map(function (x) {\n return MOD_WIKI.safe((x.item && x.item.name) || '?') +\n ' ' + ((x.c || 0) * 100).toFixed(2) + '%';\n }).join(', ');\n h += '
Drops: ' + d + '
';\n }\n h += '
';\n });\n\n var fightsOrig = MOD_FIGHTS.mode === 'original';\n h += '

How hard an enemy is

' +\n '

It depends on the Fights setting, which is ' +\n (fightsOrig ? 'Original' : 'Scaled') + ' now. On Original (the default) ' +\n 'every area the author wrote is exactly as he wrote it: his creatures, his ' +\n 'level ranges, and you out-grow them as you do in his game. On Scaled every ' +\n 'area is sized to you. The three areas past the golem arena and the rank ' +\n 'duels are sized to you either way.

' +\n '

Sizing to you works like this. Enemies are not fitted to a level. Each spawn is solved against your ' +\n 'actual power at the moment it appears, aiming at roughly ' + MOD_ENEMY.kill +\n ' swings to kill it and ' + MOD_ENEMY.die + ' before it kills you, with a ' +\n 'guaranteed margin between the two so a fight is never a coin flip. Variety ' +\n 'comes from the area\\'s level band and each creature\\'s own stat weighting, ' +\n 'not from difficulty scaling.

' +\n '

The reason for that: within the first stage alone you go from 1 STR to ' +\n 'around 265, so anything anchored to a level is either trivial at the end of ' +\n 'a stage or impossible at the start of one.

';\n return h;\n});\n\n/* --- 8. items -------------------------------------------------------------\n The one place where \"list everything\" needs help: 321 of the game's 371\n plain items are the same inventory type, so grouping by type alone leaves a\n single group of 321. Anything past MOD_WIKI_ALPHA_AT gets an alphabetical\n sub-split as well — the oldest index pattern there is, and derived rather\n than invented, which a hand-made taxonomy would not be.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_ALPHA_AT = 60;\n\n/* The game's own inventory tabs (isort): ALL / WPN / EQP / USE / OTHER. Using\n its categories rather than new ones means a group here is a tab there. */\nvar MOD_WIKI_STYPE = { 2: 'Weapons', 3: 'Equipment', 4: 'Usable', 5: 'Other' };\n\nfunction MOD_wikiItemRow(it) {\n var val = it.v !== undefined ? it.v : (it.val !== undefined ? it.val : null);\n return '
' +\n '

' + MOD_WIKI.safe(it.name) +\n (it.rar > 1 ? 'rarity ' + it.rar + '' : '') +\n (val !== null ? '' + MOD_wikiNum(val) + ' coin' : '') +\n (it.amount > 0 ? '×' + it.amount + '' : '') +\n '

' + MOD_wikiDesc(it.desc) + '
';\n}\n\n/* The letter a name files under. Several items are quoted — the master's\n manuals are literally named \"Sword Saint Manual\" — and filing those under a\n quotation mark is no use to anyone looking for S. */\nfunction MOD_wikiLetter(name) {\n var m = String(name || '').replace(/^[^0-9A-Za-z]+/, '');\n return (m.charAt(0) || '?').toUpperCase();\n}\n\nfunction MOD_wikiByLetter(a, b) {\n var x = MOD_wikiLetter(a.name) + String(a.name || '').toLowerCase();\n var y = MOD_wikiLetter(b.name) + String(b.name || '').toLowerCase();\n return x < y ? -1 : (x > y ? 1 : 0);\n}\n\n/* Flat below the threshold, alphabetically sub-grouped above it. Ranges are cut\n from the entries actually present, so they stay even as content is added. */\nfunction MOD_wikiAlpha(entries, render, level) {\n if (entries.length <= MOD_WIKI_ALPHA_AT) return entries.map(render).join('');\n var per = Math.ceil(entries.length / Math.ceil(entries.length / 40));\n var out = '';\n for (var i = 0; i < entries.length; i += per) {\n var chunk = entries.slice(i, i + per);\n var first = MOD_wikiLetter(chunk[0].name);\n var last = MOD_wikiLetter(chunk[chunk.length - 1].name);\n out += MOD_wikiGroup(first === last ? first : first + '–' + last,\n chunk.length, chunk.map(render).join(''), level || 4);\n }\n return out;\n}\n\nMOD_wikiPage('items', 'Items', function () {\n var h = '

Items

' +\n '

Everything the game defines, whether or not you have seen ' +\n 'it. Grouped the way your inventory groups it. Values are the base sell ' +\n 'price where the game gives one.

';\n\n /* item[] first, split by the inventory tab it appears under. */\n var buckets = {};\n for (var k in item) {\n var it = item[k];\n if (!it || !it.name || it.name === 'dummy') continue;\n var lab = MOD_WIKI_STYPE[it.stype] || 'Uncategorised';\n (buckets[lab] = buckets[lab] || []).push(it);\n }\n ['Usable', 'Other', 'Weapons', 'Equipment', 'Uncategorised'].forEach(function (lab) {\n var list = buckets[lab];\n if (!list || !list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(lab, list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n\n /* and the equipment objects, which live in their own globals */\n [['Weapons', wpn], ['Armour', eqp], ['Shields', sld], ['Accessories', acc]]\n .forEach(function (g) {\n var list = [];\n for (var kk in g[1]) {\n if (g[1][kk] && g[1][kk].name && g[1][kk].name !== 'dummy') list.push(g[1][kk]);\n }\n if (!list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(g[0], list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n return h;\n});\n\n/* --- 9. actions ----------------------------------------------------------- */\nMOD_wikiPage('actions', 'Actions', function () {\n var unlock = {};\n MOD_ACTION_UNLOCKS.forEach(function (u) {\n if (u.act) unlock[u.act.id] = (skl[u.skill] ? skl[u.skill].name : u.skill) +\n ' level ' + u.lv;\n });\n if (typeof act.mod_qi !== 'undefined') {\n unlock[act.mod_qi.id] = 'clearing the dojo\\'s Easiest, Easy and Normal fights';\n }\n\n var h = '

Actions

' +\n '

What you can be doing. One at a time, and only where the ' +\n 'action makes sense — unless \"Unrestricted actions\" is ticked in settings, ' +\n 'which lets several run at once anywhere. Both limits are deliberate; the ' +\n 'switch is there because they are not to everyone\\'s taste.

';\n\n var keys = [];\n for (var k in act) if (act[k] && act[k].name && act[k].name !== 'dummy') keys.push(k);\n keys.sort(function (a, b) { return act[a].name < act[b].name ? -1 : 1; });\n\n keys.forEach(function (k) {\n var a = act[k];\n var how = unlock[a.id];\n h += '
' +\n '

' + MOD_WIKI.safe(a.name) +\n (a.have === true ? 'unlocked'\n : 'locked') +\n (how ? 'from ' + MOD_WIKI.safe(how) + '' : '') +\n '

' + MOD_wikiDesc(a.desc) + '
';\n });\n return h;\n});\n\n/* --- 10. the dojo --------------------------------------------------------- */\nMOD_wikiPage('dojo', 'The dojo', function () {\n var h = '

The dojo

' +\n '

The instructor promises a reward every five levels. The base ' +\n 'game stops delivering at 30; the mod carries the same ladder to 110, on the ' +\n 'same terms.

' +\n '

Only the lowest rung you have not claimed is offered, which is how the ' +\n 'first six behave. Rewards are sized against the character experience curve ' +\n '(4×level3 + level2) — a level at 110 costs 5.3 ' +\n 'million, which is why the base game\\'s best pill is worth 0.3% of one there.

' +\n '' +\n '';\n\n MOD_DOJO_RUNGS.forEach(function (r) {\n var pill = item[r.pill];\n var extra = [];\n if (r.realmPill) extra.push('breakthrough pill for realm ' + r.realmPill);\n if (r.manual) extra.push('a choice of master\\'s manual');\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
LevelRewardCoinAlso
' + r.lv + '' + (pill ? MOD_WIKI.safe(pill.name) + ' ×' + r.n\n : '—') + '' + MOD_wikiNum(r.coin) + '' + (extra.join(', ') || '—') + '' + (global.flags[r.flag] ? 'claimed' : '') +\n '
' +\n\n '

Spirit pills

' +\n '

Character experience in a bottle. The four the mod adds continue the ' +\n 'game\\'s own series up to the new curve.

' +\n '' +\n '';\n MOD_PILLS.forEach(function (p) {\n h += '';\n });\n h += '
PillExperience
' + MOD_WIKI.safe(p[2]) + '' +\n p[3].toLocaleString() + '
' +\n\n '

Master\\'s manuals

' +\n '

A third grade of skillbook, offered at levels 50, 75 and 100 as a choice ' +\n 'the same way the first one was. Each is worth +' +\n Math.round(MOD_MANUAL_RATE * 100) + '% experience in its skill, permanently.

' +\n '' +\n '';\n MOD_MANUALS.forEach(function (m) {\n h += '';\n });\n h += '
ManualSkill
' + MOD_WIKI.safe(m[2]) + '' +\n MOD_WIKI.safe(skl[m[0]] ? (skl[m[0]].bname || skl[m[0]].name) : m[0]) +\n '
';\n return h;\n});\n\n/* --- 11. mechanics -------------------------------------------------------- */\nMOD_wikiPage('mechanics', 'Mechanics', function () {\n return '

Mechanics

' +\n\n '

Damage is subtractive, in both directions

' +\n '

A hit does the attacker\\'s strength minus the defender\\'s strength, plus ' +\n 'the weapon, plus one, before criticals. A defender\\'s STR is their ' +\n 'armour — there is no separate armour stat doing that job.

' +\n '

Two consequences worth carrying around:

' +\n '
    ' +\n '
  • Damage does not taper, it stops. Once a defender\\'s STR passes ' +\n 'yours, your hits do not get small; they land for nothing. Progress against ' +\n 'something too strong is not slow, it is zero.
  • ' +\n '
  • A stat ratio tells you nothing. Comparing your HP to an enemy\\'s ' +\n 'strength ignores the subtraction entirely and reads several times off in ' +\n 'either direction.
  • ' +\n '
' +\n '

This is also the reason enemies are never scaled by raising ' +\n 'their strength: it would raise their offence and their armour together, and ' +\n 'the second effect arrives first.

' +\n\n '

Criticals carry the late game

' +\n '

Critical rate reaches about a third at the top of the ladder and a ' +\n 'critical is roughly eight times a normal swing, so around three quarters of ' +\n 'all damage done at that point is critical damage. Anything that raises ' +\n 'critical rate or critical damage is worth more than it looks; Sharp Eye and ' +\n 'War are the two that do.

' +\n\n '

Hit chance

' +\n '

Landing a hit is a contest of agility. It is a ratio rather than a ' +\n 'subtraction, so agility behaves more gently than strength does — falling ' +\n 'behind on it costs you accuracy, not everything.

' +\n\n '

Skill sections and parents

' +\n '

Each of the ' + Object.keys(MOD_SECTIONS).length + ' sections has a parent ' +\n 'skill. A parent earns ' + Math.round(MOD_PARENT.feed * 100) + '% of everything ' +\n 'its children earn and grants no stats of its own. In return, a child below ' +\n 'the parent\\'s level trains ' + Math.round(MOD_PARENT.perLevel * 100) +\n '% faster per level of lag, up to ' + MOD_PARENT.maxGap + ' levels behind. ' +\n 'Bringing one section up makes every skill in it cheaper to catch up.

' +\n\n '

Saving

' +\n '

Three slots, switched from the bottom bar. The game builds its world once ' +\n 'at startup and cannot unload a save, so switching slots or starting a new ' +\n 'game reloads the page. Exporting a save before running anything unusual is ' +\n 'always the safe move.

';\n});\n\n/* --- the document ---------------------------------------------------------\n One string, no fetches, no dependencies: this has to work from file:// as\n readily as from a server.\n ------------------------------------------------------------------------- */\n\nfunction MOD_wikiCss() {\n return [\n ':root{color-scheme:dark}',\n '*{box-sizing:border-box}',\n /* Menlo and Consolas before the generic: with MS Gothic missing (any Mac)\n \"monospace\" is Courier, which Safari sets wider than Chrome does, and\n table cells wrapped on one and not the other. */\n 'body{margin:0;background:#060a1c;color:#dfe6f5;font:14px/1.6 \"MS Gothic\",' +\n '\"MS Mincho\",Menlo,Consolas,\"DejaVu Sans Mono\",monospace;display:flex;min-height:100vh}',\n 'a{color:#7cb0ff}',\n 'nav{width:210px;flex:0 0 210px;background:#081040;border-right:1px solid #46a;' +\n 'position:sticky;top:0;height:100vh;overflow:auto;padding:14px 0}',\n 'nav h2{font-size:13px;color:#9fb4d8;margin:0 0 10px 14px;letter-spacing:1px}',\n 'nav a{display:block;padding:6px 14px;text-decoration:none;color:#cfe0ff;' +\n 'border-left:3px solid transparent}',\n 'nav a:hover{background:#154080}',\n 'nav a.on{background:#123070;border-left-color:#7cb0ff;color:#fff}',\n 'nav .srch{margin:0 10px 12px;width:calc(100% - 20px);background:#050a20;' +\n 'color:#dfe6f5;border:1px solid #46a;padding:5px;font:inherit}',\n 'nav .xall{display:block;margin:12px 10px 0;color:#8296b8;font-size:12px;' +\n 'cursor:pointer;-webkit-user-select:none;user-select:none}',\n 'nav .xall input{vertical-align:-1px;margin-right:4px}',\n 'details.wk-g{border:1px solid #17224a;background:#0a1130;margin:8px 0}',\n 'details.wk-g>summary{cursor:pointer;padding:7px 12px;list-style:none;' +\n 'background:#0d1740}',\n 'details.wk-g>summary:hover{background:#154080}',\n 'details.wk-g>summary::-webkit-details-marker{display:none}',\n // a caret drawn rather than borrowed, so it is the same in every browser\n 'details.wk-g>summary::before{content:\"\\\\25B8\";color:#7cb0ff;' +\n 'display:inline-block;width:1em;transition:transform .12s}',\n 'details.wk-g[open]>summary::before{transform:rotate(90deg)}',\n 'details.wk-g>summary h2,details.wk-g>summary h3{display:inline;margin:0;' +\n 'font-size:15px;color:#fff}',\n '.wk-gi{padding:2px 12px 10px}',\n 'details.wk-g details.wk-g>summary h4{display:inline;margin:0;font-size:13px;' +\n 'color:#cfe0ff;font-weight:normal}',\n 'details.wk-g details.wk-g>summary{padding:5px 10px;background:#0b1435}',\n 'main{flex:1;min-width:0;padding:22px 30px 80px;max-width:1000px}',\n 'h1{font-size:26px;margin:0 0 6px;color:#fff;border-bottom:1px solid #46a;' +\n 'padding-bottom:8px}',\n 'h2{font-size:19px;color:#bcd4ff;margin:30px 0 8px}',\n 'h3{font-size:15px;color:#fff;margin:0 0 4px}',\n 'p.lede{color:#a8bcdc;margin:0 0 18px}',\n '.dim{color:#8296b8;font-weight:normal}',\n '.flav{font-size:12px;font-style:italic}',\n '.note{background:#0b1638;border-left:3px solid #46a;padding:8px 12px;' +\n 'color:#a8bcdc;margin:14px 0}',\n 'code,pre{background:#0b1638;border:1px solid #24356e;padding:1px 5px;' +\n 'color:#9fd0ff}',\n 'pre{padding:10px;overflow-x:auto}',\n 'dl dt{color:#fff;margin-top:14px}',\n 'dl dd{margin:2px 0 0 0;color:#a8bcdc}',\n 'ul{color:#a8bcdc}',\n 'table.wk-tbl{border-collapse:collapse;width:100%;margin:10px 0;' +\n 'display:block;overflow-x:auto}',\n 'table.wk-tbl th{text-align:left;color:#9fb4d8;font-weight:normal;' +\n 'border-bottom:1px solid #46a;padding:5px 9px;white-space:nowrap}',\n 'table.wk-tbl td{border-bottom:1px solid #17224a;padding:5px 9px;' +\n 'vertical-align:top}',\n 'table.wk-tbl tr.done td{color:#cfe0ff}',\n 'table.wk-tbl tr.here{background:#123070}',\n '.num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}',\n '.wk-e{border:1px solid #17224a;background:#081040;padding:9px 12px;' +\n 'margin:7px 0;border-radius:2px}',\n 'tr.wk-e{border:0;background:none}',\n '.wk-e.done{border-color:#2a4a8c}',\n '.wk-e h3{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}',\n '.lv{margin-left:auto;font-size:12px;color:#8296b8;font-weight:normal}',\n '.lv.capped{color:#ffd24a}',\n '.rank{font-size:12px;color:#8296b8;font-weight:normal}',\n '.desc{color:#a8bcdc;font-size:13px}',\n '.proj{color:#ffd24a;font-size:12px;margin-top:3px}',\n '.tag{font-size:11px;border:1px solid #46a;color:#9fb4d8;padding:0 5px;' +\n 'border-radius:2px;font-weight:normal}',\n '.tag.mod{border-color:#7a4ea8;color:#c07bff}',\n '.tag.done{border-color:#2e7d4f;color:#6fdc9a}',\n '.tag.here{border-color:#ffd24a;color:#ffd24a}',\n '.tag.warn{border-color:#a8562e;color:#ff9a5c}',\n 'table.wk-perks{border-collapse:collapse;margin:6px 0 0;width:100%}',\n 'table.wk-perks td{padding:2px 8px 2px 0;font-size:12px;color:#8296b8;' +\n 'border-bottom:1px solid #101a3c}',\n 'table.wk-perks tr.done td{color:#cfe0ff}',\n 'table.wk-perks td.tick{color:#6fdc9a;width:1em;text-align:right}',\n // fixed columns so the level bands line up between one area and the next\n 'table.wk-pop{table-layout:fixed}',\n 'table.wk-pop td:first-child{width:60%}',\n 'table.wk-pop td:nth-child(2){width:20%}',\n '.wk-title .tname{font-weight:bold}',\n '.wk-now{background:#0b1638;border:1px solid #24356e;padding:10px 14px;' +\n 'margin:0 0 18px;display:inline-block;min-width:320px}',\n '.wk-now h3{color:#9fb4d8;font-weight:normal;font-size:13px;' +\n 'letter-spacing:1px;margin-bottom:6px}',\n '.wk-now td{padding:2px 16px 2px 0}',\n '.wk-now td:last-child{color:#fff;text-align:right}',\n '.pg{display:none}.pg.on{display:block}',\n 'footer{color:#5a6c8c;font-size:12px;border-top:1px solid #17224a;' +\n 'margin-top:40px;padding-top:12px}',\n '@media(max-width:760px){body{display:block}nav{width:auto;height:auto;' +\n 'position:static;flex:none}main{padding:16px}}'\n ].join('\\n');\n}\n\n/* Filtering, page switching and the hash. Small enough to inline, and inlining\n it is the point — the document has to stand on its own. */\nfunction MOD_wikiJs() {\n return [\n 'var pages=[].slice.call(document.querySelectorAll(\".pg\"));',\n 'var links=[].slice.call(document.querySelectorAll(\"nav a[data-p]\"));',\n 'function show(id){',\n ' var found=false;',\n ' pages.forEach(function(p){var on=p.id===\"pg-\"+id;if(on)found=true;',\n ' p.className=\"pg\"+(on?\" on\":\"\")});',\n ' if(!found&&pages.length){pages[0].className=\"pg on\";id=pages[0].id.slice(3)}',\n ' links.forEach(function(a){a.className=a.dataset.p===id?\"on\":\"\"});',\n ' window.scrollTo(0,0);return id;}',\n 'links.forEach(function(a){a.addEventListener(\"click\",function(e){',\n ' e.preventDefault();location.hash=a.dataset.p;show(a.dataset.p)})});',\n 'window.addEventListener(\"hashchange\",function(){show(location.hash.slice(1))});',\n 'show(location.hash.slice(1)||\"start\");',\n // Search filters entries, hides groups left with none, and OPENS the groups\n // that still have some — a match hidden inside a collapsed group reads as\n // no match at all, which is worse than not having search.\n 'var box=document.getElementById(\"srch\");',\n 'var xall=document.getElementById(\"xall\");',\n 'var groups=[].slice.call(document.querySelectorAll(\"details.wk-g\"));',\n 'function filter(){',\n ' var q=box.value.trim().toLowerCase();',\n ' pages.forEach(function(p){',\n ' [].slice.call(p.querySelectorAll(\".wk-e\")).forEach(function(e){',\n ' e.style.display=(!q||e.textContent.toLowerCase().indexOf(q)>=0)?\"\":\"none\"})});',\n // Every page, not just the one on screen: the nav says how many entries\n // match in each, dims the ones with none, and Enter jumps to the first\n // page that has any. A prose page with no entries counts as one if its text\n // contains the query.\n ' links.forEach(function(a){',\n ' var pg=document.getElementById(\"pg-\"+a.dataset.p),n=0;',\n ' if(a.dataset.l===undefined)a.dataset.l=a.textContent;',\n ' if(q&&pg){var es=[].slice.call(pg.querySelectorAll(\".wk-e\"));',\n ' if(es.length)es.forEach(function(e){if(e.textContent.toLowerCase().indexOf(q)>=0)n++});',\n ' else if(pg.textContent.toLowerCase().indexOf(q)>=0)n=1}',\n ' a.dataset.n=n;a.style.opacity=(q&&!n)?\".4\":\"\";',\n ' a.textContent=a.dataset.l+(q&&n?\" (\"+n+\")\":\"\")});',\n ' groups.forEach(function(g){',\n ' var any=[].slice.call(g.querySelectorAll(\".wk-e\")).some(function(e){',\n ' return e.style.display!==\"none\"});',\n ' g.style.display=any?\"\":\"none\";',\n ' g.open=xall.checked||(!!q&&any)}); }',\n 'box.addEventListener(\"input\",filter);',\n 'box.addEventListener(\"keydown\",function(e){',\n ' if(e.key===\"Escape\"){box.value=\"\";filter()}',\n ' if(e.key===\"Enter\"&&box.value.trim()){',\n ' var cur=links.filter(function(a){return a.className===\"on\"})[0];',\n ' if(cur&&+cur.dataset.n)return;',\n ' var hit=links.filter(function(a){return +a.dataset.n>0})[0];',\n ' if(hit){location.hash=hit.dataset.p;show(hit.dataset.p)}}});',\n // Everything is collapsed by default because three of these pages run past\n // 40,000px open. This is the way back to one long document, which is what\n // the browser's own find needs.\n 'xall.addEventListener(\"change\",filter);'\n ].join('\\n');\n}\n\nfunction MOD_wikiBuild() {\n var nav = '', body = '';\n MOD_WIKI_PAGES.forEach(function (pg) {\n nav += '' +\n MOD_WIKI.safe(pg.label) + '';\n var inner;\n /* One page throwing must not take the wiki down with it — the page says so\n and the rest still renders. */\n try { inner = pg.build(); }\n catch (e) {\n inner = '

' + MOD_WIKI.safe(pg.label) + '

This page ' +\n 'could not be built: ' + MOD_WIKI.safe(e.message) + '

';\n console.warn('[mod] wiki page \"' + pg.id + '\" failed: ' + e.message);\n }\n body += '
' + inner + '
';\n });\n\n var stamp = new Date().toLocaleString();\n return '' +\n '' +\n '' + MOD_WIKI.safe(MOD_WIKI.title) + '' +\n '' +\n '' +\n '
' + body +\n '
Generated from the running game at ' + MOD_WIKI.safe(stamp) +\n ' — mod ' + MOD_WIKI.safe(MOD.version) + '. Everything here is read from the ' +\n 'live data, including your own save, so it cannot fall out of step with the ' +\n 'game. The game is truezhangwei\\'s; the mod is not endorsed by the author.' +\n '
' +\n ' inside content closing the block early, so that is the\n one thing scrubbed. */\n safe: function (h) {\n return String(h === undefined || h === null ? '' : h).replace(/<\\/script/gi, '<\\\\/script');\n }\n};\n\n/* Descriptions carry real HTML (coloured spans, the game's separator div), so\n they are passed through rather than escaped — but the separator becomes a\n rule, since a wiki row is not a tooltip. */\nfunction MOD_wikiDesc(d) {\n var s;\n try { s = (typeof d === 'function') ? d() : d; } catch (e) { s = ''; }\n if (!s) return '';\n s = String(s).split('
 
').join('
');\n return MOD_WIKI.safe(s);\n}\n\nfunction MOD_wikiNum(n) {\n if (!isFinite(n)) return '—';\n return Math.round(n).toLocaleString();\n}\n\n/* An effect line at a level the player has not reached yet. The effect\n functions read the skill they are handed, so the level is borrowed and put\n back; anything that throws simply gets no projection. */\nfunction MOD_wikiEffectAt(key, sk, lvl) {\n var eff = MOD_EFFECTS[key];\n if (!eff) return '';\n var was = sk.lvl, out = '';\n try { sk.lvl = lvl; out = eff(sk) || ''; }\n catch (e) { out = ''; }\n finally { sk.lvl = was; }\n return MOD_WIKI.safe(out);\n}\n\n/* --- page builders --------------------------------------------------------\n Each returns the inner HTML of one page. `wk-e` marks a searchable entry and\n `wk-g` a group that hides itself when all its entries are filtered out.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_PAGES = [];\n\nfunction MOD_wikiPage(id, label, build) {\n MOD_WIKI_PAGES.push({ id: id, label: label, build: build });\n}\n\n/* A collapsible group.\n\n Not decoration: with every skill, item and title listed, three of the eleven\n pages ran past 40,000 pixels — a hundred screens of scrolling, which is the\n same as not having the content at all. Collapsed, each of those pages opens\n as an index you can read in one screen and expand where you want.\n\n
rather than a click handler, so it still works with JavaScript off,\n the browser's own find can be made to reach inside it, and the summary is\n keyboard-reachable for free. `level` is the heading level for the summary,\n since a group is sometimes a section of a page and sometimes a subsection. */\nfunction MOD_wikiGroup(heading, count, inner, level) {\n var hn = 'h' + (level || 2);\n return '
<' + hn + '>' + heading +\n (count !== null && count !== undefined\n ? ' (' + count + ')' : '') +\n '
' + inner + '
';\n}\n\n/* --- 1. orientation ------------------------------------------------------- */\nMOD_wikiPage('start', 'Start here', function () {\n var cap = MOD_levelCap();\n var realm = MOD_realm();\n var h = '

proto23

' +\n '

An idle RPG by truezhangwei, plus a mod. You train skills by ' +\n 'doing things, the things you can do open up as the story does, and none of ' +\n 'it is in a hurry.

' +\n\n '

Where you are right now

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Character level' + MOD_wikiNum(you.lvl) + '
Skill level cap' + cap + ' — ' +\n MOD_WIKI.safe(MOD_CAP.name) + '
Cultivation realm' + MOD_WIKI.safe(realm.name) +\n ' (' + realm.n + ' of ' + (MOD_REALMS.length - 1) + ')
Titles held' + MOD_titleCount() + ' — renown ' +\n (skl.rnwn ? skl.rnwn.lvl : 0) + '
Coin' + MOD_wikiNum(you.wealth || 0) + '
' +\n\n '

The four things worth knowing early

' +\n '
' +\n '
Your skill levels are capped by the story, not by grinding.
' +\n '
The cap starts at 10 and ends at 110, and it moves when you reach a new ' +\n 'place — not when you earn enough experience. If a skill has stopped ' +\n 'levelling, you are not doing it wrong; you need to go somewhere. See ' +\n 'Progression.
' +\n\n '
Armour is subtraction, so levels matter more than they look.
' +\n '
A hit does the attacker\\'s STR minus the defender\\'s STR. That means a ' +\n 'small gap in strength is a large gap in damage, and a big enough one takes ' +\n 'your damage to nothing at all rather than merely reducing it. See ' +\n 'Mechanics.
' +\n\n '
Most skills have a parent that pulls them along.
' +\n '
Every section has a parent skill that earns a share of everything its ' +\n 'children do, and any child lagging behind the parent trains faster — up to ' +\n 'twice as fast at ' + MOD_PARENT.maxGap + ' levels behind. A neglected skill is ' +\n 'cheap to bring up later, which is the only reason having this many is ' +\n 'workable.
' +\n\n '
Cultivation is a second ladder, and it is not automatic.
' +\n '
Circulating qi levels a skill; the skill does not raise your realm. ' +\n 'Reaching the level opens a bottleneck, and breaking through it costs a pill ' +\n 'and can fail. See Cultivation.
' +\n '
' +\n\n '

What is the mod and what is the game

' +\n '

The game is index.html and it is entirely truezhangwei\\'s ' +\n 'work. Everything in mod.js is a modification of it and is not ' +\n 'endorsed by the author. Pages here mark added content with ' +\n 'mod where the distinction matters. The full ' +\n 'list of changes is in the changelog, on the bottom bar.

';\n return h;\n});\n\n/* --- 2. progression ------------------------------------------------------- */\nMOD_wikiPage('progress', 'Progression', function () {\n var cap = MOD_levelCap();\n var h = '

Progression

' +\n '

Skill levels are capped by how far the story has gone. Ten ' +\n 'rungs, 10 through 110. The cap applies to every skill at once; Renown is ' +\n 'the single exception, since it is counted rather than trained.

' +\n '' +\n '';\n\n MOD_TIERS.forEach(function (t) {\n var got = null;\n try { got = MOD_tierFlag(t); } catch (e) {}\n var here = (t.cap === cap);\n var need = t.needText ? t.needText\n : (t.flags.length ? t.flags.join(' or ') : 'the start of the game');\n h += '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
CapStageWhat opens it
' + t.cap + '' + MOD_WIKI.safe(t.name) + '' + MOD_WIKI.safe(need) + '' + (here ? 'you are here'\n : got ? 'reached' : '') + '
' +\n\n '

The experience curve

' +\n '

The game\\'s own skill curve is super-exponential — level 109 to 110 ' +\n 'alone costs ' + MOD_wikiNum(MOD_vanillaExpnext(109)) + ' experience, so the top of the ladder ' +\n 'is unreachable by a factor of about ten trillion. But the author only ever ' +\n 'designed skills up to level 15, and below that his curve is fine.

' +\n (MOD_XP.vanillaTo > 0\n ? '

So the mod keeps it. Below level ' + MOD_XP.vanillaTo + ' a skill costs ' +\n 'exactly what the original charges; from there it rises by ×' +\n MOD_XP.ratio.toFixed(4) + ' a level, a ratio solved so the climb to 110 takes ' +\n 'the same time it always did.'\n : '

You have chosen the Mod before 4.2 pacing: a plain geometric curve, ' +\n '×' + MOD_XP.ratio.toFixed(3) + ' a level from level 0, much faster than the ' +\n 'original early on. The Original pacing in settings puts back the ' +\n 'author\\'s own cost below level 10.') +\n ' Character level uses the game\\'s own curve, untouched.

' +\n '' +\n '';\n [1, 5, 9, 10, 15, 25, 50, 75, 90, 109].forEach(function (l) {\n h += '';\n });\n h += '
LevelExperience for the nextOriginal game
' + l +\n '' + MOD_wikiNum(MOD_expnextFor(l)) + '' +\n MOD_wikiNum(MOD_vanillaExpnext(l)) + '
';\n return h;\n});\n\n/* --- 3. skills ------------------------------------------------------------ */\nMOD_wikiPage('skills', 'Skills', function () {\n var cap = MOD_levelCap();\n var bySection = {};\n var keys = [];\n for (var k in skl) {\n var s = skl[k];\n if (!s || typeof s !== 'object' || !s.name) continue;\n keys.push(k);\n }\n keys.forEach(function (k) {\n var s = skl[k];\n var sec = MOD_sectionOf(s);\n (bySection[sec] = bySection[sec] || []).push(k);\n });\n\n var h = '

Skills

' +\n '

' + keys.length + ' skills in ' +\n Object.keys(MOD_SECTIONS).length + ' sections. Every one has perks at ' +\n MOD_LADDER.join(', ') + ', and every one that has milestones grants titles at ' +\n MOD_TITLE_RUNGS.join(', ') + '. Levels and ticks below are yours.

';\n\n Object.keys(MOD_SECTIONS).forEach(function (sec) {\n var list = bySection[sec];\n if (!list || !list.length) return;\n var parentKey = MOD_PARENT_KEY[sec];\n var g = '';\n if (parentKey && skl[parentKey]) {\n g += '

Parent skill: ' +\n MOD_WIKI.safe(skl[parentKey].bname || skl[parentKey].name) +\n ' — earns ' + Math.round(MOD_PARENT.feed * 100) +\n '% of what its children earn, and speeds up any child that lags behind it.

';\n }\n list.sort(function (a, b) { return skl[a].name < skl[b].name ? -1 : 1; });\n list.forEach(function (k) {\n var s = skl[k];\n var lvl = Number(s.lvl) || 0;\n g += '
' +\n '

' + MOD_WIKI.safe(s.bname || s.name) +\n (s.bname ? ' (' + MOD_WIKI.safe(s.name) + ')' : '') +\n '= cap ? ' capped' : '') + '\">lv ' + lvl +\n (lvl >= cap ? ' — at the cap' : '') + '

' +\n '
' + MOD_wikiDesc(s.desc) + '
';\n\n var at110 = MOD_wikiEffectAt(k, s, 110);\n if (at110) g += '
At level 110: ' + at110 + '
';\n\n if (s.mlstn && s.mlstn.length) {\n g += '';\n s.mlstn.forEach(function (m) {\n g += '' +\n '' +\n '' +\n '';\n });\n g += '
' + m.lv + '' + MOD_WIKI.safe(m.p || '—') + '' + (m.g === true ? '✓' : '') + '
';\n }\n g += '
';\n });\n h += MOD_wikiGroup(MOD_WIKI.safe(MOD_SECTIONS[sec]), list.length, g, 2);\n });\n return h;\n});\n\n/* --- 4. cultivation ------------------------------------------------------- */\nMOD_wikiPage('cultivation', 'Cultivation', function () {\n var now = MOD_realm();\n var elig = 0;\n try { elig = MOD_realmEligible().n; } catch (e) {}\n var qic = skl.qic ? skl.qic.lvl : 0;\n\n var h = '

Cultivation

' +\n '

Ten realms above Mortal. Circulating qi levels the Qi ' +\n 'Circulation skill; the skill does not advance the realm. Reaching the level ' +\n 'opens a bottleneck, and breaking through it costs a pill and can fail — the ' +\n 'pill is spent either way.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Realm' + MOD_WIKI.safe(MOD_realmTitle()) + '
Spiritual Root' + MOD_WIKI.safe(MOD_root().name) + '
Qi Circulationlevel ' + qic + '
Insight' + MOD_insight() + '
Bottleneck open' +\n (elig > now.n ? 'yes — ' + MOD_WIKI.safe(MOD_REALMS[now.n + 1].name) +\n ' is within reach' : 'no') + '
Qi Deviation' + (MOD_deviated()\n ? MOD_deviationLeft() + ' in-game minutes left' : 'no') + '
' +\n '

Realm thresholds are the same levels that earn a title rank, so realm N ' +\n 'is the level at which a rank N title arrives. Every realm multiplies all ' +\n 'stats and health, and the multipliers do not stack with each other — the ' +\n 'realm you are in is the one that applies.

' +\n '' +\n '';\n\n MOD_REALMS.forEach(function (r) {\n var pill = r.n >= 1 ? item['mod_bp' + r.n] : null;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmNameQi Circ.StatsHealthPill
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + r.qic + '×' + r.mult + '×' + r.hp + '' + (pill ? MOD_WIKI.safe(pill.name) : '—') +\n '
' +\n\n '

Where the pills come from

' +\n '

Every breakthrough pill has a source in normal play. None of them are ' +\n 'craftable, and none are cheap — a bottleneck you can buy in bulk is not a ' +\n 'bottleneck.

' +\n '' +\n '' +\n '' +\n '' +\n '
RealmsSource
1The dojo instructor, when qi ' +\n 'circulation is first taught
2–5The Herbalist in the village ' +\n '(vendor.pha1), restocked
6–10The dojo\\'s Level Advancement ' +\n 'ladder, and the Pill Tower
' +\n '

Realm 1 is deliberately not sold at the marketplace: ' +\n 'the marketplace is behind a 40%-a-visit chance encounter, and realm 1 opens ' +\n 'long before that is reliable.

' +\n\n '

Breaking through

' +\n '

A bottleneck wants three things, which is what the genre says it wants: ' +\n 'a pill, insight, and enough consolidation to make the ' +\n 'attempt worth making. The pill and the insight are both spent whether the ' +\n 'attempt works or not.

' +\n '

The odds improve the further past the threshold you are, plus your root, ' +\n 'plus time sat in Closed Door Training, and cap short of certain.

' +\n '' +\n '';\n [0, 2, 4, 6, 8].forEach(function (o) {\n var base = Math.min(0.55 + o * 0.05 + MOD_root().odds, 0.95);\n h += '';\n });\n h += '
Levels past the thresholdChanceAfter full seclusion
+' + o + '' +\n Math.round(base * 100) + '%' +\n Math.round(Math.min(base + MOD_CULT.secludeOdds, 0.95) * 100) + '%
' +\n\n '

Insight

' +\n '

Insight is the half of the price that cannot be bought. It comes from ' +\n 'three places, and they are the three the genre names: meditating, fights you ' +\n 'nearly lost, and going somewhere you have not been.

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
SourceRate
Circulate Qi, at a wall' +\n (MOD_CULT.meditateChance * 100).toFixed(1) + '% a second
Closed Door Training' +\n (MOD_CULT.secludeChance * 100).toFixed(1) + '% a second
A kill made under ' +\n Math.round(MOD_CULT.brinkFrac * 100) + '% health — ' +\n (MOD_FIGHTS.mode === 'original'\n ? 'the author\\'s areas rarely come close once you reach them; the three past ' +\n 'the golem arena and the rank duels do, within a few fights, as does an area ' +\n 'you walk into early'\n : 'every fight costs a share of your health, so a few in a row without resting') +\n '' +\n Math.round(MOD_CULT.brinkChance * 100) + '% a kill
Entering an area for the first timealways
' +\n '

The insight a realm asks for is sized to the climb to it: about ' +\n Math.round(MOD_CULT.wallShare * 100) + '% of the Circulate Qi time the climb took, ' +\n 'spent at the wall. Seclusion earns it three times as fast, and full ' +\n 'consolidation takes as long as that on average.

' +\n '' +\n '' +\n '';\n var wkHours = function (sec) {\n return sec >= 7200 ? (sec / 3600).toFixed(sec >= 36000 ? 0 : 1) + ' h' : Math.round(sec / 60) + ' min';\n };\n MOD_REALMS.forEach(function (r) {\n if (!r.n) return;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmClimb (Circulate Qi, 1x)Insight to enterFull seclusionQi Deviation (in-game)
' + MOD_WIKI.safe(r.name) + '' + wkHours(MOD_realmClimbSec(r.n)) + '' + MOD_insightNeed(r.n) + '' + wkHours(MOD_secludeFull(r.n)) + '' + (MOD_devMinutes(r.n) / 1440).toFixed(1) + ' days
' +\n\n '

The nine layers

' +\n '

Each realm is nine layers deep, derived from where Qi Circulation sits ' +\n 'between this realm\\'s threshold and the next one\\'s — so breaking through ' +\n 'puts you at the first layer of the new realm on its own. Layers are worth ' +\n '×' + MOD_CULT.layerMult + ' stats and ×' + MOD_CULT.layerBody +\n ' body each above the first, and the top realm has no rung above it to ' +\n 'measure against, so it stays at its first layer.

' +\n '' +\n '';\n for (var li = 1; li <= MOD_CULT.layers; li++) {\n h += '';\n }\n h += '
LayerCalled
' +\n li + '' + MOD_LAYER_ORD[li] + ' layer, ' +\n MOD_WIKI.safe(MOD_layerStage(li)) + '
' +\n '

At the threshold of the next realm and before breaking ' +\n 'through, you are a half step to it — the genre\\'s own name for the ' +\n 'state this mod calls a bottleneck.

' +\n\n '

Closed Door Training

' +\n '

Seclusion does two things. It consolidates — a full sitting ' +\n '(the table above) is worth +' + Math.round(MOD_CULT.secludeOdds * 100) +\n '% on the attempt — and it shelters: a breakthrough attempted while ' +\n 'you are still sitting in it cannot end in Qi Deviation, only in a lost ' +\n 'pill. It will not start unless you are actually at a wall.

' +\n\n '

Qi Deviation

' +\n '

Failing a breakthrough out in the open leaves your cultivation base ' +\n 'unstable for as long as the seclusion you skipped (the table above, never ' +\n 'under ' + MOD_CULT.devMinutes + ' in-game minutes): stats at ×' +\n MOD_CULT.devStat + ', body at ×' + MOD_CULT.devBody + ', and no second ' +\n 'attempt until it passes. It clears on its own, or at once with a ' +\n 'Qi Settling Pill. It never kills — the death system stays out of it.

' +\n\n '

Heavenly Tribulation

' +\n '

From realm ' + MOD_CULT.tribFrom + ' up, a successful breakthrough is ' +\n 'answered. The cost is a fraction of your maximum health, so it is a gate on ' +\n 'walking in healthy rather than a damage race; the realm bonus you are ' +\n 'standing on is what pays it. Fall to it and the realm does not open.

' +\n '' +\n '';\n MOD_REALMS.forEach(function (r) {\n if (!r.n || r.n < MOD_CULT.tribFrom) return;\n h += '' +\n '' +\n '';\n });\n h += '
RealmBoltsOf your max HP
' + MOD_WIKI.safe(r.name) + '' + MOD_tribulationBolts(r.n) + '' + Math.round(MOD_tribulationCost(r.n) * 100) + '%
' +\n\n '

Spiritual Root

' +\n '

Rolled once, when the dojo finishes teaching you to fight. It is never a ' +\n 'dead end: the worst grade costs cultivation speed, not access, and a ' +\n 'Root Cleansing Pill moves you up a grade.

' +\n '' +\n '';\n var rootTotal = 0;\n MOD_ROOTS.forEach(function (r) { rootTotal += r.weight; });\n MOD_ROOTS.forEach(function (r) {\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
GradeRootChanceCultivationBreakthrough
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + Math.round(r.weight / rootTotal * 100) + '%×' + r.xp + '' + (r.odds >= 0 ? '+' : '') +\n Math.round(r.odds * 100) + '%
';\n return h;\n});\n\n/* --- 5. elemental mastery ------------------------------------------------- */\nMOD_wikiPage('mastery', 'Elemental mastery', function () {\n var h = '

Elemental mastery mod

' +\n '

Six masteries, each with a technique that fires on its own ' +\n 'during a fight. Combat is automatic and there is no ability picker, so a ' +\n 'technique is a chance on each of your swings to become something else — one ' +\n 'that scales with INT and the element rather than with your weapon.

' +\n '

Nothing fires while you are still Mortal: the channels have to be open ' +\n 'first. Chance is ' + (MOD_TECH.perLevel * 100).toFixed(1) + '% per mastery ' +\n 'level plus ' + (MOD_TECH.perRealm * 100).toFixed(1) + '% per realm, capped at ' +\n Math.round(MOD_TECH.cap * 100) + '%; a technique lands for ×' +\n MOD_TECH.power + ' of a normal strike.

' +\n '' +\n '';\n\n var realm = 0;\n try { realm = MOD_realm().n; } catch (e) {}\n MOD_MASTERY.forEach(function (m) {\n var lvl = Number(m.skill.lvl) || 0;\n var ch = realm < 1 ? 0\n : Math.min(lvl * MOD_TECH.perLevel + realm * MOD_TECH.perRealm, MOD_TECH.cap);\n h += '' +\n '' +\n '' +\n '';\n });\n h += '
MasteryTechniqueLevelChance now
' +\n MOD_WIKI.safe(m.skill.name) + '' + MOD_WIKI.safe(m.name) + '' + lvl + '' + (realm < 1 ? 'Mortal'\n : (ch * 100).toFixed(1) + '%') + '
';\n return h;\n});\n\n/* --- 6. titles ------------------------------------------------------------ */\nMOD_wikiPage('titles', 'Titles', function () {\n var perm = MOD_permanentRank();\n var held = MOD_titleCount();\n var total = 0; for (var kk in ttl) if (ttl[kk] && ttl[kk].name) total++;\n\n var h = '

Titles

' +\n '

A title\\'s rank runs 1 to 10 and is derived from the level ' +\n 'that earns it, so a rank is a statement about where in the game you are. ' +\n 'Only the title you are wearing applies — until renown makes the lower ranks ' +\n 'permanent.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '
Titles held' + held + ' of ' + total + '
Renown level' + (skl.rnwn ? skl.rnwn.lvl : 0) +\n ' of ' + MOD_RENOWN_MAX + '
Ranks that apply without wearing' +\n (perm >= 1 ? '1–' + perm : 'none yet') + '
' +\n\n '

Ranks

' +\n '';\n MOD_RANK_AT.forEach(function (lv, i) {\n var st = MOD_rankStyle(i + 1);\n h += '' +\n '';\n });\n h += '
RankEarned from levelColour
' + (i + 1) + '' + lv + '' + '●●●' + '
' +\n\n '

Renown

' +\n '

Renown is not trained. It counts the titles you hold, and each level ' +\n 'multiplies every stat by ' + (1 + MOD_RENOWN_RATE) + ' — compounding, so ' +\n 'level ' + MOD_RENOWN_MAX + ' is ×' +\n (Math.round(MOD_renownMult(MOD_RENOWN_MAX) * 1000) / 1000) + '. At renown N, ' +\n 'every title of rank N or below applies permanently.

' +\n '' +\n '';\n MOD_RENOWN_STEPS.forEach(function (n, i) {\n h += '= n ? 'done' : '') + '\">' +\n '';\n });\n h += '
RenownTitles neededAll statsPassive ranks
' + (i + 1) +\n '' + n + '×' +\n (Math.round(MOD_renownMult(i + 1) * 1000) / 1000) + '1–' + (i + 1) + '
' +\n\n '

Every title

' +\n '

Grouped by the skill that grants it. What is left is either ' +\n 'a ladder of its own — creatures killed, items gathered, the ' +\n 'cultivation realms — or a one-off from the story.

';\n\n var groups = {}, fams = {}, loose = [];\n for (var k in ttl) {\n var t = ttl[k];\n /* Unlike the picker, the wiki KEEPS the author's broken entries and says\n so — same call as area.clg, which is listed and tagged \"nothing spawns\"\n rather than quietly dropped. A reader looking for why a title never\n appears is better served by the row than by its absence. */\n if (!t || t.name === undefined || t.name === null) continue;\n var sk = MOD_TITLE_SKILL[k];\n if (sk && skl[sk]) { (groups[sk] = groups[sk] || []).push(k); continue; }\n var stem = MOD_titleStem(k);\n if (stem) { (fams[stem] = fams[stem] || []).push(k); continue; }\n loose.push(k);\n }\n /* Same rule as the picker: a family of one is not a family. */\n Object.keys(fams).forEach(function (stem) {\n if (fams[stem].length < 2) { loose.push(fams[stem][0]); delete fams[stem]; }\n });\n var order = Object.keys(groups).sort(function (a, b) {\n return skl[a].name < skl[b].name ? -1 : 1;\n });\n\n var row = function (k) {\n var t = ttl[k];\n var st = MOD_rankStyle(t.rar);\n var gen = null;\n for (var i = 0; i < MOD_SKILL_TITLES.length; i++) {\n if (MOD_SKILL_TITLES[i].key === k) { gen = MOD_SKILL_TITLES[i]; break; }\n }\n /* The author left a handful unfinished: ttl.ddcd carries the literal string\n \"null\" for both name and description, and shpt2/shpt3/mone3 are blank.\n None is granted by anything, so none can be earned. */\n var broken = !t.name || t.name === 'null';\n return '
' +\n '' +\n MOD_WIKI.safe(t.name || '(unnamed)') + '' +\n (broken ? 'unfinished' : '') +\n 'rank ' + (t.rar || 1) + '' +\n (gen ? 'lv ' + gen.rung + ' · +' +\n Math.round(gen.xp * 100) + '% ' + MOD_WIKI.safe(skl[gen.skill].name) +\n ' exp' : '') +\n (t.have === true ? 'held' : '') +\n '
' + MOD_wikiDesc(t.desc) + '
';\n };\n\n var byRank = function (a, b) { return (ttl[a].rar || 1) - (ttl[b].rar || 1); };\n order.forEach(function (sk) {\n groups[sk].sort(byRank);\n h += MOD_wikiGroup(MOD_WIKI.safe(skl[sk].bname || skl[sk].name),\n groups[sk].length, groups[sk].map(row).join(''), 3);\n });\n /* The ladders that belong to no skill, by the same derivation the picker\n uses — so the two cannot drift apart. Sorted by the label, since that is\n what the reader scans. */\n var famList = Object.keys(fams)\n .map(function (stem) {\n fams[stem].sort(byRank);\n return { stem: stem,\n label: MOD_titleFamilyLabel(stem, fams[stem].map(function (k) { return ttl[k]; })) };\n })\n .sort(function (a, b) { return a.label < b.label ? -1 : a.label > b.label ? 1 : 0; });\n if (famList.length) {\n /* Nested one level down rather than sitting beside the skills. Eleven more\n top-level summaries cost ~530px on a page that is already the tallest in\n the wiki, and these are a different KIND of thing from a skill anyway. */\n var famInner = '', famTotal = 0;\n famList.forEach(function (f) {\n famTotal += fams[f.stem].length;\n famInner += MOD_wikiGroup(MOD_WIKI.safe(f.label), fams[f.stem].length,\n fams[f.stem].map(row).join(''), 4);\n });\n h += MOD_wikiGroup('Ladders of their own', famTotal, famInner, 3);\n }\n if (loose.length) {\n loose.sort(byRank);\n /* The story titles are the one group with no skill to divide them, and\n there are enough of them to need dividing. Rank rather than the alphabet:\n it is the axis they are already sorted on, and the one a reader cares\n about — a rank is a statement about where in the game a title comes from. */\n var inner;\n if (loose.length <= MOD_WIKI_ALPHA_AT) {\n inner = loose.map(row).join('');\n } else {\n var byRankNo = {};\n loose.forEach(function (k) {\n var r = Math.min(Math.max(Number(ttl[k].rar) || 1, 1), 10);\n (byRankNo[r] = byRankNo[r] || []).push(k);\n });\n inner = '';\n Object.keys(byRankNo).sort(function (a, b) { return a - b; }).forEach(function (r) {\n var st = MOD_rankStyle(r);\n inner += MOD_wikiGroup('Rank ' + r + '', byRankNo[r].length,\n byRankNo[r].map(row).join(''), 4);\n });\n }\n h += MOD_wikiGroup('From the story', loose.length, inner, 3);\n }\n return h;\n});\n\n/* --- 7. areas ------------------------------------------------------------- */\nMOD_wikiPage('areas', 'Areas & enemies', function () {\n var h = '

Areas & enemies

' +\n '

Every area, what lives in it and at what levels, and what it ' +\n 'drops. Enemy strength is not read off these levels — see the note below the ' +\n 'table.

';\n\n /* Two of the game's 21 areas are not places and listing them misleads:\n nwh \"Somewhere\" — where global.current_z parks whenever you are NOT in\n a fight, populated by creature.default (\"Nothing\").\n tst \"Test\" — reached only from chss.tst, which has id -1 and is in no\n sector, so nothing in the game can travel there.\n The test for \"is this a place\" is whether you can get to it, and neither\n can be. Everything else is listed whether or not you have been. */\n var MOD_WIKI_NOT_PLACES = { nwh: 1, tst: 1 };\n\n var keys = [];\n for (var k in area) {\n if (area[k] && area[k].pop && !MOD_WIKI_NOT_PLACES[k]) keys.push(k);\n }\n /* Ordered by the middle of the level band, which is the closest thing the\n game has to a difficulty order. */\n keys.sort(function (a, b) {\n var mid = function (z) {\n if (!z.pop.length) return 0;\n var s = 0; z.pop.forEach(function (e) { s += (e.lvlmin + e.lvlmax) / 2; });\n return s / z.pop.length;\n };\n return mid(area[a]) - mid(area[b]);\n });\n\n keys.forEach(function (k) {\n var z = area[k];\n /* An area whose baked bands are not finite can never spawn anything — see\n the note rendered under its table. Derived rather than listed by name, so\n it stays true if the game changes. */\n var broken = !z.popc || z.popc.length !== z.pop.length ||\n z.popc.some(function (band) { return !band || !isFinite(band[1] - band[0]); });\n h += '

' + MOD_WIKI.safe(z.name) +\n ' ' + k + '' +\n (broken ? 'nothing spawns' : '') +\n '' + (z.size === -1 ? 'endless' : z.size + ' to clear') +\n '

';\n if (z.pop.length) {\n /* The spawn share is NOT pop[i].c. z_bake normalises those weights into\n area.popc — a list of [lo,hi] bands that mon_gen rolls a uniform\n random against — and the weights do not have to sum to 1, so they are\n off wherever they don't. The Southern forest's .35/.45/.25 are really\n 33/43/24%. Read the bands the game actually rolls against. */\n var bands = z.popc || [];\n h += '';\n z.pop.forEach(function (e, i) {\n var c = e.crt || {};\n var band = bands[i];\n var share = band ? band[1] - band[0] : NaN;\n h += '' +\n '' +\n '';\n });\n h += '
' + MOD_WIKI.safe(c.name || '?') + 'lv ' + e.lvlmin +\n (e.lvlmax !== e.lvlmin ? '–' + e.lvlmax : '') + '' + (isFinite(share)\n ? Math.round(share * 100) + '% of spawns'\n : '—') + '
';\n if (broken) {\n h += '
Nothing spawns here. The area\\'s entries carry ' +\n 'no spawn weight, so the table the game rolls against comes out as ' +\n 'NaN and no creature can ever match it. A bug in the base game, left ' +\n 'as it is — nothing in the game travels to this area either.
';\n }\n }\n if (z.drop && z.drop.length) {\n var d = z.drop.map(function (x) {\n return MOD_WIKI.safe((x.item && x.item.name) || '?') +\n ' ' + ((x.c || 0) * 100).toFixed(2) + '%';\n }).join(', ');\n h += '
Drops: ' + d + '
';\n }\n h += '
';\n });\n\n var fightsOrig = MOD_FIGHTS.mode === 'original';\n h += '

How hard an enemy is

' +\n '

It depends on the Fights setting, which is ' +\n (fightsOrig ? 'Original' : 'Scaled') + ' now. On Original (the default) ' +\n 'every area the author wrote is exactly as he wrote it: his creatures, his ' +\n 'level ranges, and you out-grow them as you do in his game. On Scaled every ' +\n 'area is sized to you. The three areas past the golem arena and the rank ' +\n 'duels are sized to you either way.

' +\n '

Sizing to you works like this. Enemies are not fitted to a level. Each spawn is solved against your ' +\n 'actual power at the moment it appears, aiming at roughly ' + MOD_ENEMY.kill +\n ' swings to kill it and ' + MOD_ENEMY.die + ' before it kills you, with a ' +\n 'guaranteed margin between the two so a fight is never a coin flip. Variety ' +\n 'comes from the area\\'s level band and each creature\\'s own stat weighting, ' +\n 'not from difficulty scaling.

' +\n '

The reason for that: within the first stage alone you go from 1 STR to ' +\n 'around 265, so anything anchored to a level is either trivial at the end of ' +\n 'a stage or impossible at the start of one.

';\n return h;\n});\n\n/* --- 8. items -------------------------------------------------------------\n The one place where \"list everything\" needs help: 321 of the game's 371\n plain items are the same inventory type, so grouping by type alone leaves a\n single group of 321. Anything past MOD_WIKI_ALPHA_AT gets an alphabetical\n sub-split as well — the oldest index pattern there is, and derived rather\n than invented, which a hand-made taxonomy would not be.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_ALPHA_AT = 60;\n\n/* The game's own inventory tabs (isort): ALL / WPN / EQP / USE / OTHER. Using\n its categories rather than new ones means a group here is a tab there. */\nvar MOD_WIKI_STYPE = { 2: 'Weapons', 3: 'Equipment', 4: 'Usable', 5: 'Other' };\n\nfunction MOD_wikiItemRow(it) {\n var val = it.v !== undefined ? it.v : (it.val !== undefined ? it.val : null);\n return '
' +\n '

' + MOD_WIKI.safe(it.name) +\n (it.rar > 1 ? 'rarity ' + it.rar + '' : '') +\n (val !== null ? '' + MOD_wikiNum(val) + ' coin' : '') +\n (it.amount > 0 ? '×' + it.amount + '' : '') +\n '

' + MOD_wikiDesc(it.desc) + '
';\n}\n\n/* The letter a name files under. Several items are quoted — the master's\n manuals are literally named \"Sword Saint Manual\" — and filing those under a\n quotation mark is no use to anyone looking for S. */\nfunction MOD_wikiLetter(name) {\n var m = String(name || '').replace(/^[^0-9A-Za-z]+/, '');\n return (m.charAt(0) || '?').toUpperCase();\n}\n\nfunction MOD_wikiByLetter(a, b) {\n var x = MOD_wikiLetter(a.name) + String(a.name || '').toLowerCase();\n var y = MOD_wikiLetter(b.name) + String(b.name || '').toLowerCase();\n return x < y ? -1 : (x > y ? 1 : 0);\n}\n\n/* Flat below the threshold, alphabetically sub-grouped above it. Ranges are cut\n from the entries actually present, so they stay even as content is added. */\nfunction MOD_wikiAlpha(entries, render, level) {\n if (entries.length <= MOD_WIKI_ALPHA_AT) return entries.map(render).join('');\n var per = Math.ceil(entries.length / Math.ceil(entries.length / 40));\n var out = '';\n for (var i = 0; i < entries.length; i += per) {\n var chunk = entries.slice(i, i + per);\n var first = MOD_wikiLetter(chunk[0].name);\n var last = MOD_wikiLetter(chunk[chunk.length - 1].name);\n out += MOD_wikiGroup(first === last ? first : first + '–' + last,\n chunk.length, chunk.map(render).join(''), level || 4);\n }\n return out;\n}\n\nMOD_wikiPage('items', 'Items', function () {\n var h = '

Items

' +\n '

Everything the game defines, whether or not you have seen ' +\n 'it. Grouped the way your inventory groups it. Values are the base sell ' +\n 'price where the game gives one.

';\n\n /* item[] first, split by the inventory tab it appears under. */\n var buckets = {};\n for (var k in item) {\n var it = item[k];\n if (!it || !it.name || it.name === 'dummy') continue;\n var lab = MOD_WIKI_STYPE[it.stype] || 'Uncategorised';\n (buckets[lab] = buckets[lab] || []).push(it);\n }\n ['Usable', 'Other', 'Weapons', 'Equipment', 'Uncategorised'].forEach(function (lab) {\n var list = buckets[lab];\n if (!list || !list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(lab, list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n\n /* and the equipment objects, which live in their own globals */\n [['Weapons', wpn], ['Armour', eqp], ['Shields', sld], ['Accessories', acc]]\n .forEach(function (g) {\n var list = [];\n for (var kk in g[1]) {\n if (g[1][kk] && g[1][kk].name && g[1][kk].name !== 'dummy') list.push(g[1][kk]);\n }\n if (!list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(g[0], list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n return h;\n});\n\n/* --- 9. actions ----------------------------------------------------------- */\nMOD_wikiPage('actions', 'Actions', function () {\n var unlock = {};\n MOD_ACTION_UNLOCKS.forEach(function (u) {\n if (u.act) unlock[u.act.id] = (skl[u.skill] ? skl[u.skill].name : u.skill) +\n ' level ' + u.lv;\n });\n if (typeof act.mod_qi !== 'undefined') {\n unlock[act.mod_qi.id] = 'clearing the dojo\\'s Easiest, Easy and Normal fights';\n }\n\n var h = '

Actions

' +\n '

What you can be doing. One at a time, and only where the ' +\n 'action makes sense — unless \"Unrestricted actions\" is ticked in settings, ' +\n 'which lets several run at once anywhere. Both limits are deliberate; the ' +\n 'switch is there because they are not to everyone\\'s taste.

';\n\n var keys = [];\n for (var k in act) if (act[k] && act[k].name && act[k].name !== 'dummy') keys.push(k);\n keys.sort(function (a, b) { return act[a].name < act[b].name ? -1 : 1; });\n\n keys.forEach(function (k) {\n var a = act[k];\n var how = unlock[a.id];\n h += '
' +\n '

' + MOD_WIKI.safe(a.name) +\n (a.have === true ? 'unlocked'\n : 'locked') +\n (how ? 'from ' + MOD_WIKI.safe(how) + '' : '') +\n '

' + MOD_wikiDesc(a.desc) + '
';\n });\n return h;\n});\n\n/* --- 10. the dojo --------------------------------------------------------- */\nMOD_wikiPage('dojo', 'The dojo', function () {\n var h = '

The dojo

' +\n '

The instructor promises a reward every five levels. The base ' +\n 'game stops delivering at 30; the mod carries the same ladder to 110, on the ' +\n 'same terms.

' +\n '

Only the lowest rung you have not claimed is offered, which is how the ' +\n 'first six behave. Rewards are sized against the character experience curve ' +\n '(4×level3 + level2) — a level at 110 costs 5.3 ' +\n 'million, which is why the base game\\'s best pill is worth 0.3% of one there.

' +\n '' +\n '';\n\n MOD_DOJO_RUNGS.forEach(function (r) {\n var pill = item[r.pill];\n var extra = [];\n if (r.realmPill) extra.push('breakthrough pill for realm ' + r.realmPill);\n if (r.manual) extra.push('a choice of master\\'s manual');\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
LevelRewardCoinAlso
' + r.lv + '' + (pill ? MOD_WIKI.safe(pill.name) + ' ×' + r.n\n : '—') + '' + MOD_wikiNum(r.coin) + '' + (extra.join(', ') || '—') + '' + (global.flags[r.flag] ? 'claimed' : '') +\n '
' +\n\n '

Spirit pills

' +\n '

Character experience in a bottle. The four the mod adds continue the ' +\n 'game\\'s own series up to the new curve.

' +\n '' +\n '';\n MOD_PILLS.forEach(function (p) {\n h += '';\n });\n h += '
PillExperience
' + MOD_WIKI.safe(p[2]) + '' +\n p[3].toLocaleString() + '
' +\n\n '

Master\\'s manuals

' +\n '

A third grade of skillbook, offered at levels 50, 75 and 100 as a choice ' +\n 'the same way the first one was. Each is worth +' +\n Math.round(MOD_MANUAL_RATE * 100) + '% experience in its skill, permanently.

' +\n '' +\n '';\n MOD_MANUALS.forEach(function (m) {\n h += '';\n });\n h += '
ManualSkill
' + MOD_WIKI.safe(m[2]) + '' +\n MOD_WIKI.safe(skl[m[0]] ? (skl[m[0]].bname || skl[m[0]].name) : m[0]) +\n '
';\n return h;\n});\n\n/* --- 11. mechanics -------------------------------------------------------- */\nMOD_wikiPage('mechanics', 'Mechanics', function () {\n return '

Mechanics

' +\n\n '

Damage is subtractive, in both directions

' +\n '

A hit does the attacker\\'s strength minus the defender\\'s strength, plus ' +\n 'the weapon, plus one, before criticals. A defender\\'s STR is their ' +\n 'armour — there is no separate armour stat doing that job.

' +\n '

Two consequences worth carrying around:

' +\n '
    ' +\n '
  • Damage does not taper, it stops. Once a defender\\'s STR passes ' +\n 'yours, your hits do not get small; they land for nothing. Progress against ' +\n 'something too strong is not slow, it is zero.
  • ' +\n '
  • A stat ratio tells you nothing. Comparing your HP to an enemy\\'s ' +\n 'strength ignores the subtraction entirely and reads several times off in ' +\n 'either direction.
  • ' +\n '
' +\n '

This is also the reason enemies are never scaled by raising ' +\n 'their strength: it would raise their offence and their armour together, and ' +\n 'the second effect arrives first.

' +\n\n '

Criticals carry the late game

' +\n '

Critical rate reaches about a third at the top of the ladder and a ' +\n 'critical is roughly eight times a normal swing, so around three quarters of ' +\n 'all damage done at that point is critical damage. Anything that raises ' +\n 'critical rate or critical damage is worth more than it looks; Sharp Eye and ' +\n 'War are the two that do.

' +\n\n '

Hit chance

' +\n '

Landing a hit is a contest of agility. It is a ratio rather than a ' +\n 'subtraction, so agility behaves more gently than strength does — falling ' +\n 'behind on it costs you accuracy, not everything.

' +\n\n '

Skill sections and parents

' +\n '

Each of the ' + Object.keys(MOD_SECTIONS).length + ' sections has a parent ' +\n 'skill. A parent earns ' + Math.round(MOD_PARENT.feed * 100) + '% of everything ' +\n 'its children earn and grants no stats of its own. In return, a child below ' +\n 'the parent\\'s level trains ' + Math.round(MOD_PARENT.perLevel * 100) +\n '% faster per level of lag, up to ' + MOD_PARENT.maxGap + ' levels behind. ' +\n 'Bringing one section up makes every skill in it cheaper to catch up.

' +\n\n '

Saving

' +\n '

Three slots, switched from the bottom bar. The game builds its world once ' +\n 'at startup and cannot unload a save, so switching slots or starting a new ' +\n 'game reloads the page. Exporting a save before running anything unusual is ' +\n 'always the safe move.

';\n});\n\n/* --- the document ---------------------------------------------------------\n One string, no fetches, no dependencies: this has to work from file:// as\n readily as from a server.\n ------------------------------------------------------------------------- */\n\nfunction MOD_wikiCss() {\n return [\n ':root{color-scheme:dark}',\n '*{box-sizing:border-box}',\n /* Menlo and Consolas before the generic: with MS Gothic missing (any Mac)\n \"monospace\" is Courier, which Safari sets wider than Chrome does, and\n table cells wrapped on one and not the other. */\n 'body{margin:0;background:#060a1c;color:#dfe6f5;font:14px/1.6 \"MS Gothic\",' +\n '\"MS Mincho\",Menlo,Consolas,\"DejaVu Sans Mono\",monospace;display:flex;min-height:100vh}',\n 'a{color:#7cb0ff}',\n 'nav{width:210px;flex:0 0 210px;background:#081040;border-right:1px solid #46a;' +\n 'position:sticky;top:0;height:100vh;overflow:auto;padding:14px 0}',\n 'nav h2{font-size:13px;color:#9fb4d8;margin:0 0 10px 14px;letter-spacing:1px}',\n 'nav a{display:block;padding:6px 14px;text-decoration:none;color:#cfe0ff;' +\n 'border-left:3px solid transparent}',\n 'nav a:hover{background:#154080}',\n 'nav a.on{background:#123070;border-left-color:#7cb0ff;color:#fff}',\n 'nav .srch{margin:0 10px 12px;width:calc(100% - 20px);background:#050a20;' +\n 'color:#dfe6f5;border:1px solid #46a;padding:5px;font:inherit}',\n 'nav .xall{display:block;margin:12px 10px 0;color:#8296b8;font-size:12px;' +\n 'cursor:pointer;-webkit-user-select:none;user-select:none}',\n 'nav .xall input{vertical-align:-1px;margin-right:4px}',\n 'details.wk-g{border:1px solid #17224a;background:#0a1130;margin:8px 0}',\n 'details.wk-g>summary{cursor:pointer;padding:7px 12px;list-style:none;' +\n 'background:#0d1740}',\n 'details.wk-g>summary:hover{background:#154080}',\n 'details.wk-g>summary::-webkit-details-marker{display:none}',\n // a caret drawn rather than borrowed, so it is the same in every browser\n 'details.wk-g>summary::before{content:\"\\\\25B8\";color:#7cb0ff;' +\n 'display:inline-block;width:1em;transition:transform .12s}',\n 'details.wk-g[open]>summary::before{transform:rotate(90deg)}',\n 'details.wk-g>summary h2,details.wk-g>summary h3{display:inline;margin:0;' +\n 'font-size:15px;color:#fff}',\n '.wk-gi{padding:2px 12px 10px}',\n 'details.wk-g details.wk-g>summary h4{display:inline;margin:0;font-size:13px;' +\n 'color:#cfe0ff;font-weight:normal}',\n 'details.wk-g details.wk-g>summary{padding:5px 10px;background:#0b1435}',\n 'main{flex:1;min-width:0;padding:22px 30px 80px;max-width:1000px}',\n 'h1{font-size:26px;margin:0 0 6px;color:#fff;border-bottom:1px solid #46a;' +\n 'padding-bottom:8px}',\n 'h2{font-size:19px;color:#bcd4ff;margin:30px 0 8px}',\n 'h3{font-size:15px;color:#fff;margin:0 0 4px}',\n 'p.lede{color:#a8bcdc;margin:0 0 18px}',\n '.dim{color:#8296b8;font-weight:normal}',\n '.flav{font-size:12px;font-style:italic}',\n '.note{background:#0b1638;border-left:3px solid #46a;padding:8px 12px;' +\n 'color:#a8bcdc;margin:14px 0}',\n 'code,pre{background:#0b1638;border:1px solid #24356e;padding:1px 5px;' +\n 'color:#9fd0ff}',\n 'pre{padding:10px;overflow-x:auto}',\n 'dl dt{color:#fff;margin-top:14px}',\n 'dl dd{margin:2px 0 0 0;color:#a8bcdc}',\n 'ul{color:#a8bcdc}',\n 'table.wk-tbl{border-collapse:collapse;width:100%;margin:10px 0;' +\n 'display:block;overflow-x:auto}',\n 'table.wk-tbl th{text-align:left;color:#9fb4d8;font-weight:normal;' +\n 'border-bottom:1px solid #46a;padding:5px 9px;white-space:nowrap}',\n 'table.wk-tbl td{border-bottom:1px solid #17224a;padding:5px 9px;' +\n 'vertical-align:top}',\n 'table.wk-tbl tr.done td{color:#cfe0ff}',\n 'table.wk-tbl tr.here{background:#123070}',\n '.num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}',\n '.wk-e{border:1px solid #17224a;background:#081040;padding:9px 12px;' +\n 'margin:7px 0;border-radius:2px}',\n 'tr.wk-e{border:0;background:none}',\n '.wk-e.done{border-color:#2a4a8c}',\n '.wk-e h3{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}',\n '.lv{margin-left:auto;font-size:12px;color:#8296b8;font-weight:normal}',\n '.lv.capped{color:#ffd24a}',\n '.rank{font-size:12px;color:#8296b8;font-weight:normal}',\n '.desc{color:#a8bcdc;font-size:13px}',\n '.proj{color:#ffd24a;font-size:12px;margin-top:3px}',\n '.tag{font-size:11px;border:1px solid #46a;color:#9fb4d8;padding:0 5px;' +\n 'border-radius:2px;font-weight:normal}',\n '.tag.mod{border-color:#7a4ea8;color:#c07bff}',\n '.tag.done{border-color:#2e7d4f;color:#6fdc9a}',\n '.tag.here{border-color:#ffd24a;color:#ffd24a}',\n '.tag.warn{border-color:#a8562e;color:#ff9a5c}',\n 'table.wk-perks{border-collapse:collapse;margin:6px 0 0;width:100%}',\n 'table.wk-perks td{padding:2px 8px 2px 0;font-size:12px;color:#8296b8;' +\n 'border-bottom:1px solid #101a3c}',\n 'table.wk-perks tr.done td{color:#cfe0ff}',\n 'table.wk-perks td.tick{color:#6fdc9a;width:1em;text-align:right}',\n // fixed columns so the level bands line up between one area and the next\n 'table.wk-pop{table-layout:fixed}',\n 'table.wk-pop td:first-child{width:60%}',\n 'table.wk-pop td:nth-child(2){width:20%}',\n '.wk-title .tname{font-weight:bold}',\n '.wk-now{background:#0b1638;border:1px solid #24356e;padding:10px 14px;' +\n 'margin:0 0 18px;display:inline-block;min-width:320px}',\n '.wk-now h3{color:#9fb4d8;font-weight:normal;font-size:13px;' +\n 'letter-spacing:1px;margin-bottom:6px}',\n '.wk-now td{padding:2px 16px 2px 0}',\n '.wk-now td:last-child{color:#fff;text-align:right}',\n '.pg{display:none}.pg.on{display:block}',\n 'footer{color:#5a6c8c;font-size:12px;border-top:1px solid #17224a;' +\n 'margin-top:40px;padding-top:12px}',\n '@media(max-width:760px){body{display:block}nav{width:auto;height:auto;' +\n 'position:static;flex:none}main{padding:16px}}'\n ].join('\\n');\n}\n\n/* Filtering, page switching and the hash. Small enough to inline, and inlining\n it is the point — the document has to stand on its own. */\nfunction MOD_wikiJs() {\n return [\n 'var pages=[].slice.call(document.querySelectorAll(\".pg\"));',\n 'var links=[].slice.call(document.querySelectorAll(\"nav a[data-p]\"));',\n 'function show(id){',\n ' var found=false;',\n ' pages.forEach(function(p){var on=p.id===\"pg-\"+id;if(on)found=true;',\n ' p.className=\"pg\"+(on?\" on\":\"\")});',\n ' if(!found&&pages.length){pages[0].className=\"pg on\";id=pages[0].id.slice(3)}',\n ' links.forEach(function(a){a.className=a.dataset.p===id?\"on\":\"\"});',\n ' window.scrollTo(0,0);return id;}',\n 'links.forEach(function(a){a.addEventListener(\"click\",function(e){',\n ' e.preventDefault();location.hash=a.dataset.p;show(a.dataset.p)})});',\n 'window.addEventListener(\"hashchange\",function(){show(location.hash.slice(1))});',\n 'show(location.hash.slice(1)||\"start\");',\n // Search filters entries, hides groups left with none, and OPENS the groups\n // that still have some — a match hidden inside a collapsed group reads as\n // no match at all, which is worse than not having search.\n 'var box=document.getElementById(\"srch\");',\n 'var xall=document.getElementById(\"xall\");',\n 'var groups=[].slice.call(document.querySelectorAll(\"details.wk-g\"));',\n 'function filter(){',\n ' var q=box.value.trim().toLowerCase();',\n ' pages.forEach(function(p){',\n ' [].slice.call(p.querySelectorAll(\".wk-e\")).forEach(function(e){',\n ' e.style.display=(!q||e.textContent.toLowerCase().indexOf(q)>=0)?\"\":\"none\"})});',\n // Every page, not just the one on screen: the nav says how many entries\n // match in each, dims the ones with none, and Enter jumps to the first\n // page that has any. A prose page with no entries counts as one if its text\n // contains the query.\n ' links.forEach(function(a){',\n ' var pg=document.getElementById(\"pg-\"+a.dataset.p),n=0;',\n ' if(a.dataset.l===undefined)a.dataset.l=a.textContent;',\n ' if(q&&pg){var es=[].slice.call(pg.querySelectorAll(\".wk-e\"));',\n ' if(es.length)es.forEach(function(e){if(e.textContent.toLowerCase().indexOf(q)>=0)n++});',\n ' else if(pg.textContent.toLowerCase().indexOf(q)>=0)n=1}',\n ' a.dataset.n=n;a.style.opacity=(q&&!n)?\".4\":\"\";',\n ' a.textContent=a.dataset.l+(q&&n?\" (\"+n+\")\":\"\")});',\n ' groups.forEach(function(g){',\n ' var any=[].slice.call(g.querySelectorAll(\".wk-e\")).some(function(e){',\n ' return e.style.display!==\"none\"});',\n ' g.style.display=any?\"\":\"none\";',\n ' g.open=xall.checked||(!!q&&any)}); }',\n 'box.addEventListener(\"input\",filter);',\n 'box.addEventListener(\"keydown\",function(e){',\n ' if(e.key===\"Escape\"){box.value=\"\";filter()}',\n ' if(e.key===\"Enter\"&&box.value.trim()){',\n ' var cur=links.filter(function(a){return a.className===\"on\"})[0];',\n ' if(cur&&+cur.dataset.n)return;',\n ' var hit=links.filter(function(a){return +a.dataset.n>0})[0];',\n ' if(hit){location.hash=hit.dataset.p;show(hit.dataset.p)}}});',\n // Everything is collapsed by default because three of these pages run past\n // 40,000px open. This is the way back to one long document, which is what\n // the browser's own find needs.\n 'xall.addEventListener(\"change\",filter);'\n ].join('\\n');\n}\n\nfunction MOD_wikiBuild() {\n var nav = '', body = '';\n MOD_WIKI_PAGES.forEach(function (pg) {\n nav += '' +\n MOD_WIKI.safe(pg.label) + '';\n var inner;\n /* One page throwing must not take the wiki down with it — the page says so\n and the rest still renders. */\n try { inner = pg.build(); }\n catch (e) {\n inner = '

' + MOD_WIKI.safe(pg.label) + '

This page ' +\n 'could not be built: ' + MOD_WIKI.safe(e.message) + '

';\n console.warn('[mod] wiki page \"' + pg.id + '\" failed: ' + e.message);\n }\n body += '
' + inner + '
';\n });\n\n var stamp = new Date().toLocaleString();\n return '' +\n '' +\n '' + MOD_WIKI.safe(MOD_WIKI.title) + '' +\n '' +\n '' +\n '
' + body +\n '
Generated from the running game at ' + MOD_WIKI.safe(stamp) +\n ' — mod ' + MOD_WIKI.safe(MOD.version) + '. Everything here is read from the ' +\n 'live data, including your own save, so it cannot fall out of step with the ' +\n 'game. The game is truezhangwei\\'s; the mod is not endorsed by the author.' +\n '
' +\n ' inside content closing the block early, so that is the\n one thing scrubbed. */\n safe: function (h) {\n return String(h === undefined || h === null ? '' : h).replace(/<\\/script/gi, '<\\\\/script');\n }\n};\n\n/* Descriptions carry real HTML (coloured spans, the game's separator div), so\n they are passed through rather than escaped — but the separator becomes a\n rule, since a wiki row is not a tooltip. */\nfunction MOD_wikiDesc(d) {\n var s;\n try { s = (typeof d === 'function') ? d() : d; } catch (e) { s = ''; }\n if (!s) return '';\n s = String(s).split('
 
').join('
');\n return MOD_WIKI.safe(s);\n}\n\nfunction MOD_wikiNum(n) {\n if (!isFinite(n)) return '—';\n return Math.round(n).toLocaleString();\n}\n\n/* An effect line at a level the player has not reached yet. The effect\n functions read the skill they are handed, so the level is borrowed and put\n back; anything that throws simply gets no projection. */\nfunction MOD_wikiEffectAt(key, sk, lvl) {\n var eff = MOD_EFFECTS[key];\n if (!eff) return '';\n var was = sk.lvl, out = '';\n try { sk.lvl = lvl; out = eff(sk) || ''; }\n catch (e) { out = ''; }\n finally { sk.lvl = was; }\n return MOD_WIKI.safe(out);\n}\n\n/* --- page builders --------------------------------------------------------\n Each returns the inner HTML of one page. `wk-e` marks a searchable entry and\n `wk-g` a group that hides itself when all its entries are filtered out.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_PAGES = [];\n\nfunction MOD_wikiPage(id, label, build) {\n MOD_WIKI_PAGES.push({ id: id, label: label, build: build });\n}\n\n/* A collapsible group.\n\n Not decoration: with every skill, item and title listed, three of the eleven\n pages ran past 40,000 pixels — a hundred screens of scrolling, which is the\n same as not having the content at all. Collapsed, each of those pages opens\n as an index you can read in one screen and expand where you want.\n\n
rather than a click handler, so it still works with JavaScript off,\n the browser's own find can be made to reach inside it, and the summary is\n keyboard-reachable for free. `level` is the heading level for the summary,\n since a group is sometimes a section of a page and sometimes a subsection. */\nfunction MOD_wikiGroup(heading, count, inner, level) {\n var hn = 'h' + (level || 2);\n return '
<' + hn + '>' + heading +\n (count !== null && count !== undefined\n ? ' (' + count + ')' : '') +\n '
' + inner + '
';\n}\n\n/* --- 1. orientation ------------------------------------------------------- */\nMOD_wikiPage('start', 'Start here', function () {\n var cap = MOD_levelCap();\n var realm = MOD_realm();\n var h = '

proto23

' +\n '

An idle RPG by truezhangwei, plus a mod. You train skills by ' +\n 'doing things, the things you can do open up as the story does, and none of ' +\n 'it is in a hurry.

' +\n\n '

Where you are right now

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Character level' + MOD_wikiNum(you.lvl) + '
Skill level cap' + cap + ' — ' +\n MOD_WIKI.safe(MOD_CAP.name) + '
Cultivation realm' + MOD_WIKI.safe(realm.name) +\n ' (' + realm.n + ' of ' + (MOD_REALMS.length - 1) + ')
Titles held' + MOD_titleCount() + ' — renown ' +\n (skl.rnwn ? skl.rnwn.lvl : 0) + '
Coin' + MOD_wikiNum(you.wealth || 0) + '
' +\n\n '

The four things worth knowing early

' +\n '
' +\n '
Your skill levels are capped by the story, not by grinding.
' +\n '
The cap starts at 10 and ends at 110, and it moves when you reach a new ' +\n 'place — not when you earn enough experience. If a skill has stopped ' +\n 'levelling, you are not doing it wrong; you need to go somewhere. See ' +\n 'Progression.
' +\n\n '
Armour is subtraction, so levels matter more than they look.
' +\n '
A hit does the attacker\\'s STR minus the defender\\'s STR. That means a ' +\n 'small gap in strength is a large gap in damage, and a big enough one takes ' +\n 'your damage to nothing at all rather than merely reducing it. See ' +\n 'Mechanics.
' +\n\n '
Most skills have a parent that pulls them along.
' +\n '
Every section has a parent skill that earns a share of everything its ' +\n 'children do, and any child lagging behind the parent trains faster — up to ' +\n 'twice as fast at ' + MOD_PARENT.maxGap + ' levels behind. A neglected skill is ' +\n 'cheap to bring up later, which is the only reason having this many is ' +\n 'workable.
' +\n\n '
Cultivation is a second ladder, and it is not automatic.
' +\n '
Circulating qi levels a skill; the skill does not raise your realm. ' +\n 'Reaching the level opens a bottleneck, and breaking through it costs a pill ' +\n 'and can fail. See Cultivation.
' +\n '
' +\n\n '

What is the mod and what is the game

' +\n '

The game is index.html and it is entirely truezhangwei\\'s ' +\n 'work. Everything in mod.js is a modification of it and is not ' +\n 'endorsed by the author. Pages here mark added content with ' +\n 'mod where the distinction matters. The full ' +\n 'list of changes is in the changelog, on the bottom bar.

';\n return h;\n});\n\n/* --- 2. progression ------------------------------------------------------- */\nMOD_wikiPage('progress', 'Progression', function () {\n var cap = MOD_levelCap();\n var h = '

Progression

' +\n '

Skill levels are capped by how far the story has gone. Ten ' +\n 'rungs, 10 through 110. The cap applies to every skill at once; Renown is ' +\n 'the single exception, since it is counted rather than trained.

' +\n '' +\n '';\n\n MOD_TIERS.forEach(function (t) {\n var got = null;\n try { got = MOD_tierFlag(t); } catch (e) {}\n var here = (t.cap === cap);\n var need = t.needText ? t.needText\n : (t.flags.length ? t.flags.join(' or ') : 'the start of the game');\n h += '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
CapStageWhat opens it
' + t.cap + '' + MOD_WIKI.safe(t.name) + '' + MOD_WIKI.safe(need) + '' + (here ? 'you are here'\n : got ? 'reached' : '') + '
' +\n\n '

The experience curve

' +\n '

The game\\'s own skill curve is super-exponential — level 109 to 110 ' +\n 'alone costs ' + MOD_wikiNum(MOD_vanillaExpnext(109)) + ' experience, so the top of the ladder ' +\n 'is unreachable by a factor of about ten trillion. But the author only ever ' +\n 'designed skills up to level 15, and below that his curve is fine.

' +\n (MOD_XP.vanillaTo > 0\n ? '

So the mod keeps it. Below level ' + MOD_XP.vanillaTo + ' a skill costs ' +\n 'exactly what the original charges; from there it rises by ×' +\n MOD_XP.ratio.toFixed(4) + ' a level, a ratio solved so the climb to 110 takes ' +\n 'the same time it always did.'\n : '

You have chosen the Mod before 4.2 pacing: a plain geometric curve, ' +\n '×' + MOD_XP.ratio.toFixed(3) + ' a level from level 0, much faster than the ' +\n 'original early on. The Original pacing in settings puts back the ' +\n 'author\\'s own cost below level 10.') +\n ' Character level uses the game\\'s own curve, untouched.

' +\n '' +\n '';\n [1, 5, 9, 10, 15, 25, 50, 75, 90, 109].forEach(function (l) {\n h += '';\n });\n h += '
LevelExperience for the nextOriginal game
' + l +\n '' + MOD_wikiNum(MOD_expnextFor(l)) + '' +\n MOD_wikiNum(MOD_vanillaExpnext(l)) + '
';\n return h;\n});\n\n/* --- 3. skills ------------------------------------------------------------ */\nMOD_wikiPage('skills', 'Skills', function () {\n var cap = MOD_levelCap();\n var bySection = {};\n var keys = [];\n for (var k in skl) {\n var s = skl[k];\n if (!s || typeof s !== 'object' || !s.name) continue;\n keys.push(k);\n }\n keys.forEach(function (k) {\n var s = skl[k];\n var sec = MOD_sectionOf(s);\n (bySection[sec] = bySection[sec] || []).push(k);\n });\n\n var h = '

Skills

' +\n '

' + keys.length + ' skills in ' +\n Object.keys(MOD_SECTIONS).length + ' sections. Every one has perks at ' +\n MOD_LADDER.join(', ') + ', and every one that has milestones grants titles at ' +\n MOD_TITLE_RUNGS.join(', ') + '. Levels and ticks below are yours.

';\n\n Object.keys(MOD_SECTIONS).forEach(function (sec) {\n var list = bySection[sec];\n if (!list || !list.length) return;\n var parentKey = MOD_PARENT_KEY[sec];\n var g = '';\n if (parentKey && skl[parentKey]) {\n g += '

Parent skill: ' +\n MOD_WIKI.safe(skl[parentKey].bname || skl[parentKey].name) +\n ' — earns ' + Math.round(MOD_PARENT.feed * 100) +\n '% of what its children earn, and speeds up any child that lags behind it.

';\n }\n list.sort(function (a, b) { return skl[a].name < skl[b].name ? -1 : 1; });\n list.forEach(function (k) {\n var s = skl[k];\n var lvl = Number(s.lvl) || 0;\n g += '
' +\n '

' + MOD_WIKI.safe(s.bname || s.name) +\n (s.bname ? ' (' + MOD_WIKI.safe(s.name) + ')' : '') +\n '= cap ? ' capped' : '') + '\">lv ' + lvl +\n (lvl >= cap ? ' — at the cap' : '') + '

' +\n '
' + MOD_wikiDesc(s.desc) + '
';\n\n var at110 = MOD_wikiEffectAt(k, s, 110);\n if (at110) g += '
At level 110: ' + at110 + '
';\n\n if (s.mlstn && s.mlstn.length) {\n g += '';\n s.mlstn.forEach(function (m) {\n g += '' +\n '' +\n '' +\n '';\n });\n g += '
' + m.lv + '' + MOD_WIKI.safe(m.p || '—') + '' + (m.g === true ? '✓' : '') + '
';\n }\n g += '
';\n });\n h += MOD_wikiGroup(MOD_WIKI.safe(MOD_SECTIONS[sec]), list.length, g, 2);\n });\n return h;\n});\n\n/* --- 4. cultivation ------------------------------------------------------- */\nMOD_wikiPage('cultivation', 'Cultivation', function () {\n var now = MOD_realm();\n var elig = 0;\n try { elig = MOD_realmEligible().n; } catch (e) {}\n var qic = skl.qic ? skl.qic.lvl : 0;\n\n var h = '

Cultivation

' +\n '

Ten realms above Mortal. Circulating qi levels the Qi ' +\n 'Circulation skill; the skill does not advance the realm. Reaching the level ' +\n 'opens a bottleneck, and breaking through it costs a pill and can fail — the ' +\n 'pill is spent either way.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Realm' + MOD_WIKI.safe(MOD_realmTitle()) + '
Spiritual Root' + MOD_WIKI.safe(MOD_root().name) + '
Qi Circulationlevel ' + qic + '
Insight' + MOD_insight() + '
Bottleneck open' +\n (elig > now.n ? 'yes — ' + MOD_WIKI.safe(MOD_REALMS[now.n + 1].name) +\n ' is within reach' : 'no') + '
Qi Deviation' + (MOD_deviated()\n ? MOD_deviationLeft() + ' in-game minutes left' : 'no') + '
' +\n '

Realm thresholds are the same levels that earn a title rank, so realm N ' +\n 'is the level at which a rank N title arrives. Every realm multiplies all ' +\n 'stats and health, and the multipliers do not stack with each other — the ' +\n 'realm you are in is the one that applies.

' +\n '' +\n '';\n\n MOD_REALMS.forEach(function (r) {\n var pill = r.n >= 1 ? item['mod_bp' + r.n] : null;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmNameQi Circ.StatsHealthPill
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + r.qic + '×' + r.mult + '×' + r.hp + '' + (pill ? MOD_WIKI.safe(pill.name) : '—') +\n '
' +\n\n '

Where the pills come from

' +\n '

Every breakthrough pill has a source in normal play. None of them are ' +\n 'craftable, and none are cheap — a bottleneck you can buy in bulk is not a ' +\n 'bottleneck.

' +\n '' +\n '' +\n '' +\n '' +\n '
RealmsSource
1The dojo instructor, when qi ' +\n 'circulation is first taught
2–5The Herbalist in the village ' +\n '(vendor.pha1), restocked
6–10The dojo\\'s Level Advancement ' +\n 'ladder, and the Pill Tower
' +\n '

Realm 1 is deliberately not sold at the marketplace: ' +\n 'the marketplace is behind a 40%-a-visit chance encounter, and realm 1 opens ' +\n 'long before that is reliable.

' +\n\n '

Breaking through

' +\n '

A bottleneck wants three things, which is what the genre says it wants: ' +\n 'a pill, insight, and enough consolidation to make the ' +\n 'attempt worth making. The pill and the insight are both spent whether the ' +\n 'attempt works or not.

' +\n '

The odds improve the further past the threshold you are, plus your root, ' +\n 'plus time sat in Closed Door Training, and cap short of certain.

' +\n '' +\n '';\n [0, 2, 4, 6, 8].forEach(function (o) {\n var base = Math.min(0.55 + o * 0.05 + MOD_root().odds, 0.95);\n h += '';\n });\n h += '
Levels past the thresholdChanceAfter full seclusion
+' + o + '' +\n Math.round(base * 100) + '%' +\n Math.round(Math.min(base + MOD_CULT.secludeOdds, 0.95) * 100) + '%
' +\n\n '

Insight

' +\n '

Insight is the half of the price that cannot be bought. It comes from ' +\n 'three places, and they are the three the genre names: meditating, fights you ' +\n 'nearly lost, and going somewhere you have not been.

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
SourceRate
Circulate Qi, at a wall' +\n (MOD_CULT.meditateChance * 100).toFixed(1) + '% a second
Closed Door Training' +\n (MOD_CULT.secludeChance * 100).toFixed(1) + '% a second
A kill made under ' +\n Math.round(MOD_CULT.brinkFrac * 100) + '% health — ' +\n (MOD_FIGHTS.mode === 'original'\n ? 'the author\\'s areas rarely come close once you reach them; the three past ' +\n 'the golem arena and the rank duels do, within a few fights, as does an area ' +\n 'you walk into early'\n : 'every fight costs a share of your health, so a few in a row without resting') +\n '' +\n Math.round(MOD_CULT.brinkChance * 100) + '% a kill
Entering an area for the first timealways
' +\n '

The insight a realm asks for is sized to the climb to it: about ' +\n Math.round(MOD_CULT.wallShare * 100) + '% of the Circulate Qi time the climb took, ' +\n 'spent at the wall. Seclusion earns it three times as fast, and full ' +\n 'consolidation takes as long as that on average.

' +\n '' +\n '' +\n '';\n var wkHours = function (sec) {\n return sec >= 7200 ? (sec / 3600).toFixed(sec >= 36000 ? 0 : 1) + ' h' : Math.round(sec / 60) + ' min';\n };\n MOD_REALMS.forEach(function (r) {\n if (!r.n) return;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmClimb (Circulate Qi, 1x)Insight to enterFull seclusionQi Deviation (in-game)
' + MOD_WIKI.safe(r.name) + '' + wkHours(MOD_realmClimbSec(r.n)) + '' + MOD_insightNeed(r.n) + '' + wkHours(MOD_secludeFull(r.n)) + '' + (MOD_devMinutes(r.n) / 1440).toFixed(1) + ' days
' +\n\n '

The nine layers

' +\n '

Each realm is nine layers deep, derived from where Qi Circulation sits ' +\n 'between this realm\\'s threshold and the next one\\'s — so breaking through ' +\n 'puts you at the first layer of the new realm on its own. Layers are worth ' +\n '×' + MOD_CULT.layerMult + ' stats and ×' + MOD_CULT.layerBody +\n ' body each above the first, and the top realm has no rung above it to ' +\n 'measure against, so it stays at its first layer.

' +\n '' +\n '';\n for (var li = 1; li <= MOD_CULT.layers; li++) {\n h += '';\n }\n h += '
LayerCalled
' +\n li + '' + MOD_LAYER_ORD[li] + ' layer, ' +\n MOD_WIKI.safe(MOD_layerStage(li)) + '
' +\n '

At the threshold of the next realm and before breaking ' +\n 'through, you are a half step to it — the genre\\'s own name for the ' +\n 'state this mod calls a bottleneck.

' +\n\n '

Closed Door Training

' +\n '

Seclusion does two things. It consolidates — a full sitting ' +\n '(the table above) is worth +' + Math.round(MOD_CULT.secludeOdds * 100) +\n '% on the attempt — and it shelters: a breakthrough attempted while ' +\n 'you are still sitting in it cannot end in Qi Deviation, only in a lost ' +\n 'pill. It will not start unless you are actually at a wall.

' +\n\n '

Qi Deviation

' +\n '

Failing a breakthrough out in the open leaves your cultivation base ' +\n 'unstable for as long as the seclusion you skipped (the table above, never ' +\n 'under ' + MOD_CULT.devMinutes + ' in-game minutes): stats at ×' +\n MOD_CULT.devStat + ', body at ×' + MOD_CULT.devBody + ', and no second ' +\n 'attempt until it passes. It clears on its own, or at once with a ' +\n 'Qi Settling Pill. It never kills — the death system stays out of it.

' +\n\n '

Heavenly Tribulation

' +\n '

From realm ' + MOD_CULT.tribFrom + ' up, a successful breakthrough is ' +\n 'answered. The cost is a fraction of your maximum health, so it is a gate on ' +\n 'walking in healthy rather than a damage race; the realm bonus you are ' +\n 'standing on is what pays it. Fall to it and the realm does not open.

' +\n '' +\n '';\n MOD_REALMS.forEach(function (r) {\n if (!r.n || r.n < MOD_CULT.tribFrom) return;\n h += '' +\n '' +\n '';\n });\n h += '
RealmBoltsOf your max HP
' + MOD_WIKI.safe(r.name) + '' + MOD_tribulationBolts(r.n) + '' + Math.round(MOD_tribulationCost(r.n) * 100) + '%
' +\n\n '

Spiritual Root

' +\n '

Rolled once, when the dojo finishes teaching you to fight. It is never a ' +\n 'dead end: the worst grade costs cultivation speed, not access, and a ' +\n 'Root Cleansing Pill moves you up a grade.

' +\n '' +\n '';\n var rootTotal = 0;\n MOD_ROOTS.forEach(function (r) { rootTotal += r.weight; });\n MOD_ROOTS.forEach(function (r) {\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
GradeRootChanceCultivationBreakthrough
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + Math.round(r.weight / rootTotal * 100) + '%×' + r.xp + '' + (r.odds >= 0 ? '+' : '') +\n Math.round(r.odds * 100) + '%
';\n return h;\n});\n\n/* --- 5. elemental mastery ------------------------------------------------- */\nMOD_wikiPage('mastery', 'Elemental mastery', function () {\n var h = '

Elemental mastery mod

' +\n '

Six masteries, each with a technique that fires on its own ' +\n 'during a fight. Combat is automatic and there is no ability picker, so a ' +\n 'technique is a chance on each of your swings to become something else — one ' +\n 'that scales with INT and the element rather than with your weapon.

' +\n '

Nothing fires while you are still Mortal: the channels have to be open ' +\n 'first. Chance is ' + (MOD_TECH.perLevel * 100).toFixed(1) + '% per mastery ' +\n 'level plus ' + (MOD_TECH.perRealm * 100).toFixed(1) + '% per realm, capped at ' +\n Math.round(MOD_TECH.cap * 100) + '%; a technique lands for ×' +\n MOD_TECH.power + ' of a normal strike.

' +\n '' +\n '';\n\n var realm = 0;\n try { realm = MOD_realm().n; } catch (e) {}\n MOD_MASTERY.forEach(function (m) {\n var lvl = Number(m.skill.lvl) || 0;\n var ch = realm < 1 ? 0\n : Math.min(lvl * MOD_TECH.perLevel + realm * MOD_TECH.perRealm, MOD_TECH.cap);\n h += '' +\n '' +\n '' +\n '';\n });\n h += '
MasteryTechniqueLevelChance now
' +\n MOD_WIKI.safe(m.skill.name) + '' + MOD_WIKI.safe(m.name) + '' + lvl + '' + (realm < 1 ? 'Mortal'\n : (ch * 100).toFixed(1) + '%') + '
';\n return h;\n});\n\n/* --- 6. titles ------------------------------------------------------------ */\nMOD_wikiPage('titles', 'Titles', function () {\n var perm = MOD_permanentRank();\n var held = MOD_titleCount();\n var total = 0; for (var kk in ttl) if (ttl[kk] && ttl[kk].name) total++;\n\n var h = '

Titles

' +\n '

A title\\'s rank runs 1 to 10 and is derived from the level ' +\n 'that earns it, so a rank is a statement about where in the game you are. ' +\n 'Only the title you are wearing applies — until renown makes the lower ranks ' +\n 'permanent.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '
Titles held' + held + ' of ' + total + '
Renown level' + (skl.rnwn ? skl.rnwn.lvl : 0) +\n ' of ' + MOD_RENOWN_MAX + '
Ranks that apply without wearing' +\n (perm >= 1 ? '1–' + perm : 'none yet') + '
' +\n\n '

Ranks

' +\n '';\n MOD_RANK_AT.forEach(function (lv, i) {\n var st = MOD_rankStyle(i + 1);\n h += '' +\n '';\n });\n h += '
RankEarned from levelColour
' + (i + 1) + '' + lv + '' + '●●●' + '
' +\n\n '

Renown

' +\n '

Renown is not trained. It counts the titles you hold, and each level ' +\n 'multiplies every stat by ' + (1 + MOD_RENOWN_RATE) + ' — compounding, so ' +\n 'level ' + MOD_RENOWN_MAX + ' is ×' +\n (Math.round(MOD_renownMult(MOD_RENOWN_MAX) * 1000) / 1000) + '. At renown N, ' +\n 'every title of rank N or below applies permanently.

' +\n '' +\n '';\n MOD_RENOWN_STEPS.forEach(function (n, i) {\n h += '= n ? 'done' : '') + '\">' +\n '';\n });\n h += '
RenownTitles neededAll statsPassive ranks
' + (i + 1) +\n '' + n + '×' +\n (Math.round(MOD_renownMult(i + 1) * 1000) / 1000) + '1–' + (i + 1) + '
' +\n\n '

Every title

' +\n '

Grouped by the skill that grants it. What is left is either ' +\n 'a ladder of its own — creatures killed, items gathered, the ' +\n 'cultivation realms — or a one-off from the story.

';\n\n var groups = {}, fams = {}, loose = [];\n for (var k in ttl) {\n var t = ttl[k];\n /* Unlike the picker, the wiki KEEPS the author's broken entries and says\n so — same call as area.clg, which is listed and tagged \"nothing spawns\"\n rather than quietly dropped. A reader looking for why a title never\n appears is better served by the row than by its absence. */\n if (!t || t.name === undefined || t.name === null) continue;\n var sk = MOD_TITLE_SKILL[k];\n if (sk && skl[sk]) { (groups[sk] = groups[sk] || []).push(k); continue; }\n var stem = MOD_titleStem(k);\n if (stem) { (fams[stem] = fams[stem] || []).push(k); continue; }\n loose.push(k);\n }\n /* Same rule as the picker: a family of one is not a family. */\n Object.keys(fams).forEach(function (stem) {\n if (fams[stem].length < 2) { loose.push(fams[stem][0]); delete fams[stem]; }\n });\n var order = Object.keys(groups).sort(function (a, b) {\n return skl[a].name < skl[b].name ? -1 : 1;\n });\n\n var row = function (k) {\n var t = ttl[k];\n var st = MOD_rankStyle(t.rar);\n var gen = null;\n for (var i = 0; i < MOD_SKILL_TITLES.length; i++) {\n if (MOD_SKILL_TITLES[i].key === k) { gen = MOD_SKILL_TITLES[i]; break; }\n }\n /* The author left a handful unfinished: ttl.ddcd carries the literal string\n \"null\" for both name and description, and shpt2/shpt3/mone3 are blank.\n None is granted by anything, so none can be earned. */\n var broken = !t.name || t.name === 'null';\n return '
' +\n '' +\n MOD_WIKI.safe(t.name || '(unnamed)') + '' +\n (broken ? 'unfinished' : '') +\n 'rank ' + (t.rar || 1) + '' +\n (gen ? 'lv ' + gen.rung + ' · +' +\n Math.round(gen.xp * 100) + '% ' + MOD_WIKI.safe(skl[gen.skill].name) +\n ' exp' : '') +\n (t.have === true ? 'held' : '') +\n '
' + MOD_wikiDesc(t.desc) + '
';\n };\n\n var byRank = function (a, b) { return (ttl[a].rar || 1) - (ttl[b].rar || 1); };\n order.forEach(function (sk) {\n groups[sk].sort(byRank);\n h += MOD_wikiGroup(MOD_WIKI.safe(skl[sk].bname || skl[sk].name),\n groups[sk].length, groups[sk].map(row).join(''), 3);\n });\n /* The ladders that belong to no skill, by the same derivation the picker\n uses — so the two cannot drift apart. Sorted by the label, since that is\n what the reader scans. */\n var famList = Object.keys(fams)\n .map(function (stem) {\n fams[stem].sort(byRank);\n return { stem: stem,\n label: MOD_titleFamilyLabel(stem, fams[stem].map(function (k) { return ttl[k]; })) };\n })\n .sort(function (a, b) { return a.label < b.label ? -1 : a.label > b.label ? 1 : 0; });\n if (famList.length) {\n /* Nested one level down rather than sitting beside the skills. Eleven more\n top-level summaries cost ~530px on a page that is already the tallest in\n the wiki, and these are a different KIND of thing from a skill anyway. */\n var famInner = '', famTotal = 0;\n famList.forEach(function (f) {\n famTotal += fams[f.stem].length;\n famInner += MOD_wikiGroup(MOD_WIKI.safe(f.label), fams[f.stem].length,\n fams[f.stem].map(row).join(''), 4);\n });\n h += MOD_wikiGroup('Ladders of their own', famTotal, famInner, 3);\n }\n if (loose.length) {\n loose.sort(byRank);\n /* The story titles are the one group with no skill to divide them, and\n there are enough of them to need dividing. Rank rather than the alphabet:\n it is the axis they are already sorted on, and the one a reader cares\n about — a rank is a statement about where in the game a title comes from. */\n var inner;\n if (loose.length <= MOD_WIKI_ALPHA_AT) {\n inner = loose.map(row).join('');\n } else {\n var byRankNo = {};\n loose.forEach(function (k) {\n var r = Math.min(Math.max(Number(ttl[k].rar) || 1, 1), 10);\n (byRankNo[r] = byRankNo[r] || []).push(k);\n });\n inner = '';\n Object.keys(byRankNo).sort(function (a, b) { return a - b; }).forEach(function (r) {\n var st = MOD_rankStyle(r);\n inner += MOD_wikiGroup('Rank ' + r + '', byRankNo[r].length,\n byRankNo[r].map(row).join(''), 4);\n });\n }\n h += MOD_wikiGroup('From the story', loose.length, inner, 3);\n }\n return h;\n});\n\n/* --- 7. areas ------------------------------------------------------------- */\nMOD_wikiPage('areas', 'Areas & enemies', function () {\n var h = '

Areas & enemies

' +\n '

Every area, what lives in it and at what levels, and what it ' +\n 'drops. Enemy strength is not read off these levels — see the note below the ' +\n 'table.

';\n\n /* Two of the game's 21 areas are not places and listing them misleads:\n nwh \"Somewhere\" — where global.current_z parks whenever you are NOT in\n a fight, populated by creature.default (\"Nothing\").\n tst \"Test\" — reached only from chss.tst, which has id -1 and is in no\n sector, so nothing in the game can travel there.\n The test for \"is this a place\" is whether you can get to it, and neither\n can be. Everything else is listed whether or not you have been. */\n var MOD_WIKI_NOT_PLACES = { nwh: 1, tst: 1 };\n\n var keys = [];\n for (var k in area) {\n if (area[k] && area[k].pop && !MOD_WIKI_NOT_PLACES[k]) keys.push(k);\n }\n /* Ordered by the middle of the level band, which is the closest thing the\n game has to a difficulty order. */\n keys.sort(function (a, b) {\n var mid = function (z) {\n if (!z.pop.length) return 0;\n var s = 0; z.pop.forEach(function (e) { s += (e.lvlmin + e.lvlmax) / 2; });\n return s / z.pop.length;\n };\n return mid(area[a]) - mid(area[b]);\n });\n\n keys.forEach(function (k) {\n var z = area[k];\n /* An area whose baked bands are not finite can never spawn anything — see\n the note rendered under its table. Derived rather than listed by name, so\n it stays true if the game changes. */\n var broken = !z.popc || z.popc.length !== z.pop.length ||\n z.popc.some(function (band) { return !band || !isFinite(band[1] - band[0]); });\n h += '

' + MOD_WIKI.safe(z.name) +\n ' ' + k + '' +\n (broken ? 'nothing spawns' : '') +\n '' + (z.size === -1 ? 'endless' : z.size + ' to clear') +\n '

';\n if (z.pop.length) {\n /* The spawn share is NOT pop[i].c. z_bake normalises those weights into\n area.popc — a list of [lo,hi] bands that mon_gen rolls a uniform\n random against — and the weights do not have to sum to 1, so they are\n off wherever they don't. The Southern forest's .35/.45/.25 are really\n 33/43/24%. Read the bands the game actually rolls against. */\n var bands = z.popc || [];\n h += '';\n z.pop.forEach(function (e, i) {\n var c = e.crt || {};\n var band = bands[i];\n var share = band ? band[1] - band[0] : NaN;\n h += '' +\n '' +\n '';\n });\n h += '
' + MOD_WIKI.safe(c.name || '?') + 'lv ' + e.lvlmin +\n (e.lvlmax !== e.lvlmin ? '–' + e.lvlmax : '') + '' + (isFinite(share)\n ? Math.round(share * 100) + '% of spawns'\n : '—') + '
';\n if (broken) {\n h += '
Nothing spawns here. The area\\'s entries carry ' +\n 'no spawn weight, so the table the game rolls against comes out as ' +\n 'NaN and no creature can ever match it. A bug in the base game, left ' +\n 'as it is — nothing in the game travels to this area either.
';\n }\n }\n if (z.drop && z.drop.length) {\n var d = z.drop.map(function (x) {\n return MOD_WIKI.safe((x.item && x.item.name) || '?') +\n ' ' + ((x.c || 0) * 100).toFixed(2) + '%';\n }).join(', ');\n h += '
Drops: ' + d + '
';\n }\n h += '
';\n });\n\n var fightsOrig = MOD_FIGHTS.mode === 'original';\n h += '

How hard an enemy is

' +\n '

It depends on the Fights setting, which is ' +\n (fightsOrig ? 'Original' : 'Scaled') + ' now. On Original (the default) ' +\n 'every area the author wrote is exactly as he wrote it: his creatures, his ' +\n 'level ranges, and you out-grow them as you do in his game. On Scaled every ' +\n 'area is sized to you. The three areas past the golem arena and the rank ' +\n 'duels are sized to you either way.

' +\n '

Sizing to you works like this. Enemies are not fitted to a level. Each spawn is solved against your ' +\n 'actual power at the moment it appears, aiming at roughly ' + MOD_ENEMY.kill +\n ' swings to kill it and ' + MOD_ENEMY.die + ' before it kills you, with a ' +\n 'guaranteed margin between the two so a fight is never a coin flip. Variety ' +\n 'comes from the area\\'s level band and each creature\\'s own stat weighting, ' +\n 'not from difficulty scaling.

' +\n '

The reason for that: within the first stage alone you go from 1 STR to ' +\n 'around 265, so anything anchored to a level is either trivial at the end of ' +\n 'a stage or impossible at the start of one.

';\n return h;\n});\n\n/* --- 8. items -------------------------------------------------------------\n The one place where \"list everything\" needs help: 321 of the game's 371\n plain items are the same inventory type, so grouping by type alone leaves a\n single group of 321. Anything past MOD_WIKI_ALPHA_AT gets an alphabetical\n sub-split as well — the oldest index pattern there is, and derived rather\n than invented, which a hand-made taxonomy would not be.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_ALPHA_AT = 60;\n\n/* The game's own inventory tabs (isort): ALL / WPN / EQP / USE / OTHER. Using\n its categories rather than new ones means a group here is a tab there. */\nvar MOD_WIKI_STYPE = { 2: 'Weapons', 3: 'Equipment', 4: 'Usable', 5: 'Other' };\n\nfunction MOD_wikiItemRow(it) {\n var val = it.v !== undefined ? it.v : (it.val !== undefined ? it.val : null);\n return '
' +\n '

' + MOD_WIKI.safe(it.name) +\n (it.rar > 1 ? 'rarity ' + it.rar + '' : '') +\n (val !== null ? '' + MOD_wikiNum(val) + ' coin' : '') +\n (it.amount > 0 ? '×' + it.amount + '' : '') +\n '

' + MOD_wikiDesc(it.desc) + '
';\n}\n\n/* The letter a name files under. Several items are quoted — the master's\n manuals are literally named \"Sword Saint Manual\" — and filing those under a\n quotation mark is no use to anyone looking for S. */\nfunction MOD_wikiLetter(name) {\n var m = String(name || '').replace(/^[^0-9A-Za-z]+/, '');\n return (m.charAt(0) || '?').toUpperCase();\n}\n\nfunction MOD_wikiByLetter(a, b) {\n var x = MOD_wikiLetter(a.name) + String(a.name || '').toLowerCase();\n var y = MOD_wikiLetter(b.name) + String(b.name || '').toLowerCase();\n return x < y ? -1 : (x > y ? 1 : 0);\n}\n\n/* Flat below the threshold, alphabetically sub-grouped above it. Ranges are cut\n from the entries actually present, so they stay even as content is added. */\nfunction MOD_wikiAlpha(entries, render, level) {\n if (entries.length <= MOD_WIKI_ALPHA_AT) return entries.map(render).join('');\n var per = Math.ceil(entries.length / Math.ceil(entries.length / 40));\n var out = '';\n for (var i = 0; i < entries.length; i += per) {\n var chunk = entries.slice(i, i + per);\n var first = MOD_wikiLetter(chunk[0].name);\n var last = MOD_wikiLetter(chunk[chunk.length - 1].name);\n out += MOD_wikiGroup(first === last ? first : first + '–' + last,\n chunk.length, chunk.map(render).join(''), level || 4);\n }\n return out;\n}\n\nMOD_wikiPage('items', 'Items', function () {\n var h = '

Items

' +\n '

Everything the game defines, whether or not you have seen ' +\n 'it. Grouped the way your inventory groups it. Values are the base sell ' +\n 'price where the game gives one.

';\n\n /* item[] first, split by the inventory tab it appears under. */\n var buckets = {};\n for (var k in item) {\n var it = item[k];\n if (!it || !it.name || it.name === 'dummy') continue;\n var lab = MOD_WIKI_STYPE[it.stype] || 'Uncategorised';\n (buckets[lab] = buckets[lab] || []).push(it);\n }\n ['Usable', 'Other', 'Weapons', 'Equipment', 'Uncategorised'].forEach(function (lab) {\n var list = buckets[lab];\n if (!list || !list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(lab, list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n\n /* and the equipment objects, which live in their own globals */\n [['Weapons', wpn], ['Armour', eqp], ['Shields', sld], ['Accessories', acc]]\n .forEach(function (g) {\n var list = [];\n for (var kk in g[1]) {\n if (g[1][kk] && g[1][kk].name && g[1][kk].name !== 'dummy') list.push(g[1][kk]);\n }\n if (!list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(g[0], list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n return h;\n});\n\n/* --- 9. actions ----------------------------------------------------------- */\nMOD_wikiPage('actions', 'Actions', function () {\n var unlock = {};\n MOD_ACTION_UNLOCKS.forEach(function (u) {\n if (u.act) unlock[u.act.id] = (skl[u.skill] ? skl[u.skill].name : u.skill) +\n ' level ' + u.lv;\n });\n if (typeof act.mod_qi !== 'undefined') {\n unlock[act.mod_qi.id] = 'clearing the dojo\\'s Easiest, Easy and Normal fights';\n }\n\n var h = '

Actions

' +\n '

What you can be doing. One at a time, and only where the ' +\n 'action makes sense — unless \"Unrestricted actions\" is ticked in settings, ' +\n 'which lets several run at once anywhere. Both limits are deliberate; the ' +\n 'switch is there because they are not to everyone\\'s taste.

';\n\n var keys = [];\n for (var k in act) if (act[k] && act[k].name && act[k].name !== 'dummy') keys.push(k);\n keys.sort(function (a, b) { return act[a].name < act[b].name ? -1 : 1; });\n\n keys.forEach(function (k) {\n var a = act[k];\n var how = unlock[a.id];\n h += '
' +\n '

' + MOD_WIKI.safe(a.name) +\n (a.have === true ? 'unlocked'\n : 'locked') +\n (how ? 'from ' + MOD_WIKI.safe(how) + '' : '') +\n '

' + MOD_wikiDesc(a.desc) + '
';\n });\n return h;\n});\n\n/* --- 10. the dojo --------------------------------------------------------- */\nMOD_wikiPage('dojo', 'The dojo', function () {\n var h = '

The dojo

' +\n '

The instructor promises a reward every five levels. The base ' +\n 'game stops delivering at 30; the mod carries the same ladder to 110, on the ' +\n 'same terms.

' +\n '

Only the lowest rung you have not claimed is offered, which is how the ' +\n 'first six behave. Rewards are sized against the character experience curve ' +\n '(4×level3 + level2) — a level at 110 costs 5.3 ' +\n 'million, which is why the base game\\'s best pill is worth 0.3% of one there.

' +\n '' +\n '';\n\n MOD_DOJO_RUNGS.forEach(function (r) {\n var pill = item[r.pill];\n var extra = [];\n if (r.realmPill) extra.push('breakthrough pill for realm ' + r.realmPill);\n if (r.manual) extra.push('a choice of master\\'s manual');\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
LevelRewardCoinAlso
' + r.lv + '' + (pill ? MOD_WIKI.safe(pill.name) + ' ×' + r.n\n : '—') + '' + MOD_wikiNum(r.coin) + '' + (extra.join(', ') || '—') + '' + (global.flags[r.flag] ? 'claimed' : '') +\n '
' +\n\n '

Spirit pills

' +\n '

Character experience in a bottle. The four the mod adds continue the ' +\n 'game\\'s own series up to the new curve.

' +\n '' +\n '';\n MOD_PILLS.forEach(function (p) {\n h += '';\n });\n h += '
PillExperience
' + MOD_WIKI.safe(p[2]) + '' +\n p[3].toLocaleString() + '
' +\n\n '

Master\\'s manuals

' +\n '

A third grade of skillbook, offered at levels 50, 75 and 100 as a choice ' +\n 'the same way the first one was. Each is worth +' +\n Math.round(MOD_MANUAL_RATE * 100) + '% experience in its skill, permanently.

' +\n '' +\n '';\n MOD_MANUALS.forEach(function (m) {\n h += '';\n });\n h += '
ManualSkill
' + MOD_WIKI.safe(m[2]) + '' +\n MOD_WIKI.safe(skl[m[0]] ? (skl[m[0]].bname || skl[m[0]].name) : m[0]) +\n '
';\n return h;\n});\n\n/* --- 11. mechanics -------------------------------------------------------- */\nMOD_wikiPage('mechanics', 'Mechanics', function () {\n return '

Mechanics

' +\n\n '

Damage is subtractive, in both directions

' +\n '

A hit does the attacker\\'s strength minus the defender\\'s strength, plus ' +\n 'the weapon, plus one, before criticals. A defender\\'s STR is their ' +\n 'armour — there is no separate armour stat doing that job.

' +\n '

Two consequences worth carrying around:

' +\n '
    ' +\n '
  • Damage does not taper, it stops. Once a defender\\'s STR passes ' +\n 'yours, your hits do not get small; they land for nothing. Progress against ' +\n 'something too strong is not slow, it is zero.
  • ' +\n '
  • A stat ratio tells you nothing. Comparing your HP to an enemy\\'s ' +\n 'strength ignores the subtraction entirely and reads several times off in ' +\n 'either direction.
  • ' +\n '
' +\n '

This is also the reason enemies are never scaled by raising ' +\n 'their strength: it would raise their offence and their armour together, and ' +\n 'the second effect arrives first.

' +\n\n '

Criticals carry the late game

' +\n '

Critical rate reaches about a third at the top of the ladder and a ' +\n 'critical is roughly eight times a normal swing, so around three quarters of ' +\n 'all damage done at that point is critical damage. Anything that raises ' +\n 'critical rate or critical damage is worth more than it looks; Sharp Eye and ' +\n 'War are the two that do.

' +\n\n '

Hit chance

' +\n '

Landing a hit is a contest of agility. It is a ratio rather than a ' +\n 'subtraction, so agility behaves more gently than strength does — falling ' +\n 'behind on it costs you accuracy, not everything.

' +\n\n '

Skill sections and parents

' +\n '

Each of the ' + Object.keys(MOD_SECTIONS).length + ' sections has a parent ' +\n 'skill. A parent earns ' + Math.round(MOD_PARENT.feed * 100) + '% of everything ' +\n 'its children earn and grants no stats of its own. In return, a child below ' +\n 'the parent\\'s level trains ' + Math.round(MOD_PARENT.perLevel * 100) +\n '% faster per level of lag, up to ' + MOD_PARENT.maxGap + ' levels behind. ' +\n 'Bringing one section up makes every skill in it cheaper to catch up.

' +\n\n '

Saving

' +\n '

Three slots, switched from the bottom bar. The game builds its world once ' +\n 'at startup and cannot unload a save, so switching slots or starting a new ' +\n 'game reloads the page. Exporting a save before running anything unusual is ' +\n 'always the safe move.

';\n});\n\n/* --- the document ---------------------------------------------------------\n One string, no fetches, no dependencies: this has to work from file:// as\n readily as from a server.\n ------------------------------------------------------------------------- */\n\nfunction MOD_wikiCss() {\n return [\n ':root{color-scheme:dark}',\n '*{box-sizing:border-box}',\n /* Menlo and Consolas before the generic: with MS Gothic missing (any Mac)\n \"monospace\" is Courier, which Safari sets wider than Chrome does, and\n table cells wrapped on one and not the other. */\n 'body{margin:0;background:#060a1c;color:#dfe6f5;font:14px/1.6 \"MS Gothic\",' +\n '\"MS Mincho\",Menlo,Consolas,\"DejaVu Sans Mono\",monospace;display:flex;min-height:100vh}',\n 'a{color:#7cb0ff}',\n 'nav{width:210px;flex:0 0 210px;background:#081040;border-right:1px solid #46a;' +\n 'position:sticky;top:0;height:100vh;overflow:auto;padding:14px 0}',\n 'nav h2{font-size:13px;color:#9fb4d8;margin:0 0 10px 14px;letter-spacing:1px}',\n 'nav a{display:block;padding:6px 14px;text-decoration:none;color:#cfe0ff;' +\n 'border-left:3px solid transparent}',\n 'nav a:hover{background:#154080}',\n 'nav a.on{background:#123070;border-left-color:#7cb0ff;color:#fff}',\n 'nav .srch{margin:0 10px 12px;width:calc(100% - 20px);background:#050a20;' +\n 'color:#dfe6f5;border:1px solid #46a;padding:5px;font:inherit}',\n 'nav .xall{display:block;margin:12px 10px 0;color:#8296b8;font-size:12px;' +\n 'cursor:pointer;-webkit-user-select:none;user-select:none}',\n 'nav .xall input{vertical-align:-1px;margin-right:4px}',\n 'details.wk-g{border:1px solid #17224a;background:#0a1130;margin:8px 0}',\n 'details.wk-g>summary{cursor:pointer;padding:7px 12px;list-style:none;' +\n 'background:#0d1740}',\n 'details.wk-g>summary:hover{background:#154080}',\n 'details.wk-g>summary::-webkit-details-marker{display:none}',\n // a caret drawn rather than borrowed, so it is the same in every browser\n 'details.wk-g>summary::before{content:\"\\\\25B8\";color:#7cb0ff;' +\n 'display:inline-block;width:1em;transition:transform .12s}',\n 'details.wk-g[open]>summary::before{transform:rotate(90deg)}',\n 'details.wk-g>summary h2,details.wk-g>summary h3{display:inline;margin:0;' +\n 'font-size:15px;color:#fff}',\n '.wk-gi{padding:2px 12px 10px}',\n 'details.wk-g details.wk-g>summary h4{display:inline;margin:0;font-size:13px;' +\n 'color:#cfe0ff;font-weight:normal}',\n 'details.wk-g details.wk-g>summary{padding:5px 10px;background:#0b1435}',\n 'main{flex:1;min-width:0;padding:22px 30px 80px;max-width:1000px}',\n 'h1{font-size:26px;margin:0 0 6px;color:#fff;border-bottom:1px solid #46a;' +\n 'padding-bottom:8px}',\n 'h2{font-size:19px;color:#bcd4ff;margin:30px 0 8px}',\n 'h3{font-size:15px;color:#fff;margin:0 0 4px}',\n 'p.lede{color:#a8bcdc;margin:0 0 18px}',\n '.dim{color:#8296b8;font-weight:normal}',\n '.flav{font-size:12px;font-style:italic}',\n '.note{background:#0b1638;border-left:3px solid #46a;padding:8px 12px;' +\n 'color:#a8bcdc;margin:14px 0}',\n 'code,pre{background:#0b1638;border:1px solid #24356e;padding:1px 5px;' +\n 'color:#9fd0ff}',\n 'pre{padding:10px;overflow-x:auto}',\n 'dl dt{color:#fff;margin-top:14px}',\n 'dl dd{margin:2px 0 0 0;color:#a8bcdc}',\n 'ul{color:#a8bcdc}',\n 'table.wk-tbl{border-collapse:collapse;width:100%;margin:10px 0;' +\n 'display:block;overflow-x:auto}',\n 'table.wk-tbl th{text-align:left;color:#9fb4d8;font-weight:normal;' +\n 'border-bottom:1px solid #46a;padding:5px 9px;white-space:nowrap}',\n 'table.wk-tbl td{border-bottom:1px solid #17224a;padding:5px 9px;' +\n 'vertical-align:top}',\n 'table.wk-tbl tr.done td{color:#cfe0ff}',\n 'table.wk-tbl tr.here{background:#123070}',\n '.num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}',\n '.wk-e{border:1px solid #17224a;background:#081040;padding:9px 12px;' +\n 'margin:7px 0;border-radius:2px}',\n 'tr.wk-e{border:0;background:none}',\n '.wk-e.done{border-color:#2a4a8c}',\n '.wk-e h3{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}',\n '.lv{margin-left:auto;font-size:12px;color:#8296b8;font-weight:normal}',\n '.lv.capped{color:#ffd24a}',\n '.rank{font-size:12px;color:#8296b8;font-weight:normal}',\n '.desc{color:#a8bcdc;font-size:13px}',\n '.proj{color:#ffd24a;font-size:12px;margin-top:3px}',\n '.tag{font-size:11px;border:1px solid #46a;color:#9fb4d8;padding:0 5px;' +\n 'border-radius:2px;font-weight:normal}',\n '.tag.mod{border-color:#7a4ea8;color:#c07bff}',\n '.tag.done{border-color:#2e7d4f;color:#6fdc9a}',\n '.tag.here{border-color:#ffd24a;color:#ffd24a}',\n '.tag.warn{border-color:#a8562e;color:#ff9a5c}',\n 'table.wk-perks{border-collapse:collapse;margin:6px 0 0;width:100%}',\n 'table.wk-perks td{padding:2px 8px 2px 0;font-size:12px;color:#8296b8;' +\n 'border-bottom:1px solid #101a3c}',\n 'table.wk-perks tr.done td{color:#cfe0ff}',\n 'table.wk-perks td.tick{color:#6fdc9a;width:1em;text-align:right}',\n // fixed columns so the level bands line up between one area and the next\n 'table.wk-pop{table-layout:fixed}',\n 'table.wk-pop td:first-child{width:60%}',\n 'table.wk-pop td:nth-child(2){width:20%}',\n '.wk-title .tname{font-weight:bold}',\n '.wk-now{background:#0b1638;border:1px solid #24356e;padding:10px 14px;' +\n 'margin:0 0 18px;display:inline-block;min-width:320px}',\n '.wk-now h3{color:#9fb4d8;font-weight:normal;font-size:13px;' +\n 'letter-spacing:1px;margin-bottom:6px}',\n '.wk-now td{padding:2px 16px 2px 0}',\n '.wk-now td:last-child{color:#fff;text-align:right}',\n '.pg{display:none}.pg.on{display:block}',\n 'footer{color:#5a6c8c;font-size:12px;border-top:1px solid #17224a;' +\n 'margin-top:40px;padding-top:12px}',\n '@media(max-width:760px){body{display:block}nav{width:auto;height:auto;' +\n 'position:static;flex:none}main{padding:16px}}'\n ].join('\\n');\n}\n\n/* Filtering, page switching and the hash. Small enough to inline, and inlining\n it is the point — the document has to stand on its own. */\nfunction MOD_wikiJs() {\n return [\n 'var pages=[].slice.call(document.querySelectorAll(\".pg\"));',\n 'var links=[].slice.call(document.querySelectorAll(\"nav a[data-p]\"));',\n 'function show(id){',\n ' var found=false;',\n ' pages.forEach(function(p){var on=p.id===\"pg-\"+id;if(on)found=true;',\n ' p.className=\"pg\"+(on?\" on\":\"\")});',\n ' if(!found&&pages.length){pages[0].className=\"pg on\";id=pages[0].id.slice(3)}',\n ' links.forEach(function(a){a.className=a.dataset.p===id?\"on\":\"\"});',\n ' window.scrollTo(0,0);return id;}',\n 'links.forEach(function(a){a.addEventListener(\"click\",function(e){',\n ' e.preventDefault();location.hash=a.dataset.p;show(a.dataset.p)})});',\n 'window.addEventListener(\"hashchange\",function(){show(location.hash.slice(1))});',\n 'show(location.hash.slice(1)||\"start\");',\n // Search filters entries, hides groups left with none, and OPENS the groups\n // that still have some — a match hidden inside a collapsed group reads as\n // no match at all, which is worse than not having search.\n 'var box=document.getElementById(\"srch\");',\n 'var xall=document.getElementById(\"xall\");',\n 'var groups=[].slice.call(document.querySelectorAll(\"details.wk-g\"));',\n 'function filter(){',\n ' var q=box.value.trim().toLowerCase();',\n ' pages.forEach(function(p){',\n ' [].slice.call(p.querySelectorAll(\".wk-e\")).forEach(function(e){',\n ' e.style.display=(!q||e.textContent.toLowerCase().indexOf(q)>=0)?\"\":\"none\"})});',\n // Every page, not just the one on screen: the nav says how many entries\n // match in each, dims the ones with none, and Enter jumps to the first\n // page that has any. A prose page with no entries counts as one if its text\n // contains the query.\n ' links.forEach(function(a){',\n ' var pg=document.getElementById(\"pg-\"+a.dataset.p),n=0;',\n ' if(a.dataset.l===undefined)a.dataset.l=a.textContent;',\n ' if(q&&pg){var es=[].slice.call(pg.querySelectorAll(\".wk-e\"));',\n ' if(es.length)es.forEach(function(e){if(e.textContent.toLowerCase().indexOf(q)>=0)n++});',\n ' else if(pg.textContent.toLowerCase().indexOf(q)>=0)n=1}',\n ' a.dataset.n=n;a.style.opacity=(q&&!n)?\".4\":\"\";',\n ' a.textContent=a.dataset.l+(q&&n?\" (\"+n+\")\":\"\")});',\n ' groups.forEach(function(g){',\n ' var any=[].slice.call(g.querySelectorAll(\".wk-e\")).some(function(e){',\n ' return e.style.display!==\"none\"});',\n ' g.style.display=any?\"\":\"none\";',\n ' g.open=xall.checked||(!!q&&any)}); }',\n 'box.addEventListener(\"input\",filter);',\n 'box.addEventListener(\"keydown\",function(e){',\n ' if(e.key===\"Escape\"){box.value=\"\";filter()}',\n ' if(e.key===\"Enter\"&&box.value.trim()){',\n ' var cur=links.filter(function(a){return a.className===\"on\"})[0];',\n ' if(cur&&+cur.dataset.n)return;',\n ' var hit=links.filter(function(a){return +a.dataset.n>0})[0];',\n ' if(hit){location.hash=hit.dataset.p;show(hit.dataset.p)}}});',\n // Everything is collapsed by default because three of these pages run past\n // 40,000px open. This is the way back to one long document, which is what\n // the browser's own find needs.\n 'xall.addEventListener(\"change\",filter);'\n ].join('\\n');\n}\n\nfunction MOD_wikiBuild() {\n var nav = '', body = '';\n MOD_WIKI_PAGES.forEach(function (pg) {\n nav += '' +\n MOD_WIKI.safe(pg.label) + '';\n var inner;\n /* One page throwing must not take the wiki down with it — the page says so\n and the rest still renders. */\n try { inner = pg.build(); }\n catch (e) {\n inner = '

' + MOD_WIKI.safe(pg.label) + '

This page ' +\n 'could not be built: ' + MOD_WIKI.safe(e.message) + '

';\n console.warn('[mod] wiki page \"' + pg.id + '\" failed: ' + e.message);\n }\n body += '
' + inner + '
';\n });\n\n var stamp = new Date().toLocaleString();\n return '' +\n '' +\n '' + MOD_WIKI.safe(MOD_WIKI.title) + '' +\n '' +\n '' +\n '
' + body +\n '
Generated from the running game at ' + MOD_WIKI.safe(stamp) +\n ' — mod ' + MOD_WIKI.safe(MOD.version) + '. Everything here is read from the ' +\n 'live data, including your own save, so it cannot fall out of step with the ' +\n 'game. The game is truezhangwei\\'s; the mod is not endorsed by the author.' +\n '
' +\n ' inside content closing the block early, so that is the\n one thing scrubbed. */\n safe: function (h) {\n return String(h === undefined || h === null ? '' : h).replace(/<\\/script/gi, '<\\\\/script');\n }\n};\n\n/* Descriptions carry real HTML (coloured spans, the game's separator div), so\n they are passed through rather than escaped — but the separator becomes a\n rule, since a wiki row is not a tooltip. */\nfunction MOD_wikiDesc(d) {\n var s;\n try { s = (typeof d === 'function') ? d() : d; } catch (e) { s = ''; }\n if (!s) return '';\n s = String(s).split('
 
').join('
');\n return MOD_WIKI.safe(s);\n}\n\nfunction MOD_wikiNum(n) {\n if (!isFinite(n)) return '—';\n return Math.round(n).toLocaleString();\n}\n\n/* An effect line at a level the player has not reached yet. The effect\n functions read the skill they are handed, so the level is borrowed and put\n back; anything that throws simply gets no projection. */\nfunction MOD_wikiEffectAt(key, sk, lvl) {\n var eff = MOD_EFFECTS[key];\n if (!eff) return '';\n var was = sk.lvl, out = '';\n try { sk.lvl = lvl; out = eff(sk) || ''; }\n catch (e) { out = ''; }\n finally { sk.lvl = was; }\n return MOD_WIKI.safe(out);\n}\n\n/* --- page builders --------------------------------------------------------\n Each returns the inner HTML of one page. `wk-e` marks a searchable entry and\n `wk-g` a group that hides itself when all its entries are filtered out.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_PAGES = [];\n\nfunction MOD_wikiPage(id, label, build) {\n MOD_WIKI_PAGES.push({ id: id, label: label, build: build });\n}\n\n/* A collapsible group.\n\n Not decoration: with every skill, item and title listed, three of the eleven\n pages ran past 40,000 pixels — a hundred screens of scrolling, which is the\n same as not having the content at all. Collapsed, each of those pages opens\n as an index you can read in one screen and expand where you want.\n\n
rather than a click handler, so it still works with JavaScript off,\n the browser's own find can be made to reach inside it, and the summary is\n keyboard-reachable for free. `level` is the heading level for the summary,\n since a group is sometimes a section of a page and sometimes a subsection. */\nfunction MOD_wikiGroup(heading, count, inner, level) {\n var hn = 'h' + (level || 2);\n return '
<' + hn + '>' + heading +\n (count !== null && count !== undefined\n ? ' (' + count + ')' : '') +\n '
' + inner + '
';\n}\n\n/* --- 1. orientation ------------------------------------------------------- */\nMOD_wikiPage('start', 'Start here', function () {\n var cap = MOD_levelCap();\n var realm = MOD_realm();\n var h = '

proto23

' +\n '

An idle RPG by truezhangwei, plus a mod. You train skills by ' +\n 'doing things, the things you can do open up as the story does, and none of ' +\n 'it is in a hurry.

' +\n\n '

Where you are right now

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Character level' + MOD_wikiNum(you.lvl) + '
Skill level cap' + cap + ' — ' +\n MOD_WIKI.safe(MOD_CAP.name) + '
Cultivation realm' + MOD_WIKI.safe(realm.name) +\n ' (' + realm.n + ' of ' + (MOD_REALMS.length - 1) + ')
Titles held' + MOD_titleCount() + ' — renown ' +\n (skl.rnwn ? skl.rnwn.lvl : 0) + '
Coin' + MOD_wikiNum(you.wealth || 0) + '
' +\n\n '

The four things worth knowing early

' +\n '
' +\n '
Your skill levels are capped by the story, not by grinding.
' +\n '
The cap starts at 10 and ends at 110, and it moves when you reach a new ' +\n 'place — not when you earn enough experience. If a skill has stopped ' +\n 'levelling, you are not doing it wrong; you need to go somewhere. See ' +\n 'Progression.
' +\n\n '
Armour is subtraction, so levels matter more than they look.
' +\n '
A hit does the attacker\\'s STR minus the defender\\'s STR. That means a ' +\n 'small gap in strength is a large gap in damage, and a big enough one takes ' +\n 'your damage to nothing at all rather than merely reducing it. See ' +\n 'Mechanics.
' +\n\n '
Most skills have a parent that pulls them along.
' +\n '
Every section has a parent skill that earns a share of everything its ' +\n 'children do, and any child lagging behind the parent trains faster — up to ' +\n 'twice as fast at ' + MOD_PARENT.maxGap + ' levels behind. A neglected skill is ' +\n 'cheap to bring up later, which is the only reason having this many is ' +\n 'workable.
' +\n\n '
Cultivation is a second ladder, and it is not automatic.
' +\n '
Circulating qi levels a skill; the skill does not raise your realm. ' +\n 'Reaching the level opens a bottleneck, and breaking through it costs a pill ' +\n 'and can fail. See Cultivation.
' +\n '
' +\n\n '

What is the mod and what is the game

' +\n '

The game is index.html and it is entirely truezhangwei\\'s ' +\n 'work. Everything in mod.js is a modification of it and is not ' +\n 'endorsed by the author. Pages here mark added content with ' +\n 'mod where the distinction matters. The full ' +\n 'list of changes is in the changelog, on the bottom bar.

';\n return h;\n});\n\n/* --- 2. progression ------------------------------------------------------- */\nMOD_wikiPage('progress', 'Progression', function () {\n var cap = MOD_levelCap();\n var h = '

Progression

' +\n '

Skill levels are capped by how far the story has gone. Ten ' +\n 'rungs, 10 through 110. The cap applies to every skill at once; Renown is ' +\n 'the single exception, since it is counted rather than trained.

' +\n '' +\n '';\n\n MOD_TIERS.forEach(function (t) {\n var got = null;\n try { got = MOD_tierFlag(t); } catch (e) {}\n var here = (t.cap === cap);\n var need = t.needText ? t.needText\n : (t.flags.length ? t.flags.join(' or ') : 'the start of the game');\n h += '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
CapStageWhat opens it
' + t.cap + '' + MOD_WIKI.safe(t.name) + '' + MOD_WIKI.safe(need) + '' + (here ? 'you are here'\n : got ? 'reached' : '') + '
' +\n\n '

The experience curve

' +\n '

The game\\'s own skill curve is super-exponential — level 109 to 110 ' +\n 'alone costs ' + MOD_wikiNum(MOD_vanillaExpnext(109)) + ' experience, so the top of the ladder ' +\n 'is unreachable by a factor of about ten trillion. But the author only ever ' +\n 'designed skills up to level 15, and below that his curve is fine.

' +\n (MOD_XP.vanillaTo > 0\n ? '

So the mod keeps it. Below level ' + MOD_XP.vanillaTo + ' a skill costs ' +\n 'exactly what the original charges; from there it rises by ×' +\n MOD_XP.ratio.toFixed(4) + ' a level, a ratio solved so the climb to 110 takes ' +\n 'the same time it always did.'\n : '

You have chosen the Mod before 4.2 pacing: a plain geometric curve, ' +\n '×' + MOD_XP.ratio.toFixed(3) + ' a level from level 0, much faster than the ' +\n 'original early on. The Original pacing in settings puts back the ' +\n 'author\\'s own cost below level 10.') +\n ' Character level uses the game\\'s own curve, untouched.

' +\n '' +\n '';\n [1, 5, 9, 10, 15, 25, 50, 75, 90, 109].forEach(function (l) {\n h += '';\n });\n h += '
LevelExperience for the nextOriginal game
' + l +\n '' + MOD_wikiNum(MOD_expnextFor(l)) + '' +\n MOD_wikiNum(MOD_vanillaExpnext(l)) + '
';\n return h;\n});\n\n/* --- 3. skills ------------------------------------------------------------ */\nMOD_wikiPage('skills', 'Skills', function () {\n var cap = MOD_levelCap();\n var bySection = {};\n var keys = [];\n for (var k in skl) {\n var s = skl[k];\n if (!s || typeof s !== 'object' || !s.name) continue;\n keys.push(k);\n }\n keys.forEach(function (k) {\n var s = skl[k];\n var sec = MOD_sectionOf(s);\n (bySection[sec] = bySection[sec] || []).push(k);\n });\n\n var h = '

Skills

' +\n '

' + keys.length + ' skills in ' +\n Object.keys(MOD_SECTIONS).length + ' sections. Every one has perks at ' +\n MOD_LADDER.join(', ') + ', and every one that has milestones grants titles at ' +\n MOD_TITLE_RUNGS.join(', ') + '. Levels and ticks below are yours.

';\n\n Object.keys(MOD_SECTIONS).forEach(function (sec) {\n var list = bySection[sec];\n if (!list || !list.length) return;\n var parentKey = MOD_PARENT_KEY[sec];\n var g = '';\n if (parentKey && skl[parentKey]) {\n g += '

Parent skill: ' +\n MOD_WIKI.safe(skl[parentKey].bname || skl[parentKey].name) +\n ' — earns ' + Math.round(MOD_PARENT.feed * 100) +\n '% of what its children earn, and speeds up any child that lags behind it.

';\n }\n list.sort(function (a, b) { return skl[a].name < skl[b].name ? -1 : 1; });\n list.forEach(function (k) {\n var s = skl[k];\n var lvl = Number(s.lvl) || 0;\n g += '
' +\n '

' + MOD_WIKI.safe(s.bname || s.name) +\n (s.bname ? ' (' + MOD_WIKI.safe(s.name) + ')' : '') +\n '= cap ? ' capped' : '') + '\">lv ' + lvl +\n (lvl >= cap ? ' — at the cap' : '') + '

' +\n '
' + MOD_wikiDesc(s.desc) + '
';\n\n var at110 = MOD_wikiEffectAt(k, s, 110);\n if (at110) g += '
At level 110: ' + at110 + '
';\n\n if (s.mlstn && s.mlstn.length) {\n g += '';\n s.mlstn.forEach(function (m) {\n g += '' +\n '' +\n '' +\n '';\n });\n g += '
' + m.lv + '' + MOD_WIKI.safe(m.p || '—') + '' + (m.g === true ? '✓' : '') + '
';\n }\n g += '
';\n });\n h += MOD_wikiGroup(MOD_WIKI.safe(MOD_SECTIONS[sec]), list.length, g, 2);\n });\n return h;\n});\n\n/* --- 4. cultivation ------------------------------------------------------- */\nMOD_wikiPage('cultivation', 'Cultivation', function () {\n var now = MOD_realm();\n var elig = 0;\n try { elig = MOD_realmEligible().n; } catch (e) {}\n var qic = skl.qic ? skl.qic.lvl : 0;\n\n var h = '

Cultivation

' +\n '

Ten realms above Mortal. Circulating qi levels the Qi ' +\n 'Circulation skill; the skill does not advance the realm. Reaching the level ' +\n 'opens a bottleneck, and breaking through it costs a pill and can fail — the ' +\n 'pill is spent either way.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
Realm' + MOD_WIKI.safe(MOD_realmTitle()) + '
Spiritual Root' + MOD_WIKI.safe(MOD_root().name) + '
Qi Circulationlevel ' + qic + '
Insight' + MOD_insight() + '
Bottleneck open' +\n (elig > now.n ? 'yes — ' + MOD_WIKI.safe(MOD_REALMS[now.n + 1].name) +\n ' is within reach' : 'no') + '
Qi Deviation' + (MOD_deviated()\n ? MOD_deviationLeft() + ' in-game minutes left' : 'no') + '
' +\n '

Realm thresholds are the same levels that earn a title rank, so realm N ' +\n 'is the level at which a rank N title arrives. Every realm multiplies all ' +\n 'stats and health, and the multipliers do not stack with each other — the ' +\n 'realm you are in is the one that applies.

' +\n '' +\n '';\n\n MOD_REALMS.forEach(function (r) {\n var pill = r.n >= 1 ? item['mod_bp' + r.n] : null;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmNameQi Circ.StatsHealthPill
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + r.qic + '×' + r.mult + '×' + r.hp + '' + (pill ? MOD_WIKI.safe(pill.name) : '—') +\n '
' +\n\n '

Where the pills come from

' +\n '

Every breakthrough pill has a source in normal play. None of them are ' +\n 'craftable, and none are cheap — a bottleneck you can buy in bulk is not a ' +\n 'bottleneck.

' +\n '' +\n '' +\n '' +\n '' +\n '
RealmsSource
1The dojo instructor, when qi ' +\n 'circulation is first taught
2–5The Herbalist in the village ' +\n '(vendor.pha1), restocked
6–10The dojo\\'s Level Advancement ' +\n 'ladder, and the Pill Tower
' +\n '

Realm 1 is deliberately not sold at the marketplace: ' +\n 'the marketplace is behind a 40%-a-visit chance encounter, and realm 1 opens ' +\n 'long before that is reliable.

' +\n\n '

Breaking through

' +\n '

A bottleneck wants three things, which is what the genre says it wants: ' +\n 'a pill, insight, and enough consolidation to make the ' +\n 'attempt worth making. The pill and the insight are both spent whether the ' +\n 'attempt works or not.

' +\n '

The odds improve the further past the threshold you are, plus your root, ' +\n 'plus time sat in Closed Door Training, and cap short of certain.

' +\n '' +\n '';\n [0, 2, 4, 6, 8].forEach(function (o) {\n var base = Math.min(0.55 + o * 0.05 + MOD_root().odds, 0.95);\n h += '';\n });\n h += '
Levels past the thresholdChanceAfter full seclusion
+' + o + '' +\n Math.round(base * 100) + '%' +\n Math.round(Math.min(base + MOD_CULT.secludeOdds, 0.95) * 100) + '%
' +\n\n '

Insight

' +\n '

Insight is the half of the price that cannot be bought. It comes from ' +\n 'three places, and they are the three the genre names: meditating, fights you ' +\n 'nearly lost, and going somewhere you have not been.

' +\n '' +\n '' +\n '' +\n '' +\n '' +\n '
SourceRate
Circulate Qi, at a wall' +\n (MOD_CULT.meditateChance * 100).toFixed(1) + '% a second
Closed Door Training' +\n (MOD_CULT.secludeChance * 100).toFixed(1) + '% a second
A kill made under ' +\n Math.round(MOD_CULT.brinkFrac * 100) + '% health — ' +\n (MOD_FIGHTS.mode === 'original'\n ? 'the author\\'s areas rarely come close once you reach them; the three past ' +\n 'the golem arena and the rank duels do, within a few fights, as does an area ' +\n 'you walk into early'\n : 'every fight costs a share of your health, so a few in a row without resting') +\n '' +\n Math.round(MOD_CULT.brinkChance * 100) + '% a kill
Entering an area for the first timealways
' +\n '

The insight a realm asks for is sized to the climb to it: about ' +\n Math.round(MOD_CULT.wallShare * 100) + '% of the Circulate Qi time the climb took, ' +\n 'spent at the wall. Seclusion earns it three times as fast, and full ' +\n 'consolidation takes as long as that on average.

' +\n '' +\n '' +\n '';\n var wkHours = function (sec) {\n return sec >= 7200 ? (sec / 3600).toFixed(sec >= 36000 ? 0 : 1) + ' h' : Math.round(sec / 60) + ' min';\n };\n MOD_REALMS.forEach(function (r) {\n if (!r.n) return;\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
RealmClimb (Circulate Qi, 1x)Insight to enterFull seclusionQi Deviation (in-game)
' + MOD_WIKI.safe(r.name) + '' + wkHours(MOD_realmClimbSec(r.n)) + '' + MOD_insightNeed(r.n) + '' + wkHours(MOD_secludeFull(r.n)) + '' + (MOD_devMinutes(r.n) / 1440).toFixed(1) + ' days
' +\n\n '

The nine layers

' +\n '

Each realm is nine layers deep, derived from where Qi Circulation sits ' +\n 'between this realm\\'s threshold and the next one\\'s — so breaking through ' +\n 'puts you at the first layer of the new realm on its own. Layers are worth ' +\n '×' + MOD_CULT.layerMult + ' stats and ×' + MOD_CULT.layerBody +\n ' body each above the first, and the top realm has no rung above it to ' +\n 'measure against, so it stays at its first layer.

' +\n '' +\n '';\n for (var li = 1; li <= MOD_CULT.layers; li++) {\n h += '';\n }\n h += '
LayerCalled
' +\n li + '' + MOD_LAYER_ORD[li] + ' layer, ' +\n MOD_WIKI.safe(MOD_layerStage(li)) + '
' +\n '

At the threshold of the next realm and before breaking ' +\n 'through, you are a half step to it — the genre\\'s own name for the ' +\n 'state this mod calls a bottleneck.

' +\n\n '

Closed Door Training

' +\n '

Seclusion does two things. It consolidates — a full sitting ' +\n '(the table above) is worth +' + Math.round(MOD_CULT.secludeOdds * 100) +\n '% on the attempt — and it shelters: a breakthrough attempted while ' +\n 'you are still sitting in it cannot end in Qi Deviation, only in a lost ' +\n 'pill. It will not start unless you are actually at a wall.

' +\n\n '

Qi Deviation

' +\n '

Failing a breakthrough out in the open leaves your cultivation base ' +\n 'unstable for as long as the seclusion you skipped (the table above, never ' +\n 'under ' + MOD_CULT.devMinutes + ' in-game minutes): stats at ×' +\n MOD_CULT.devStat + ', body at ×' + MOD_CULT.devBody + ', and no second ' +\n 'attempt until it passes. It clears on its own, or at once with a ' +\n 'Qi Settling Pill. It never kills — the death system stays out of it.

' +\n\n '

Heavenly Tribulation

' +\n '

From realm ' + MOD_CULT.tribFrom + ' up, a successful breakthrough is ' +\n 'answered. The cost is a fraction of your maximum health, so it is a gate on ' +\n 'walking in healthy rather than a damage race; the realm bonus you are ' +\n 'standing on is what pays it. Fall to it and the realm does not open.

' +\n '' +\n '';\n MOD_REALMS.forEach(function (r) {\n if (!r.n || r.n < MOD_CULT.tribFrom) return;\n h += '' +\n '' +\n '';\n });\n h += '
RealmBoltsOf your max HP
' + MOD_WIKI.safe(r.name) + '' + MOD_tribulationBolts(r.n) + '' + Math.round(MOD_tribulationCost(r.n) * 100) + '%
' +\n\n '

Spiritual Root

' +\n '

Rolled once, when the dojo finishes teaching you to fight. It is never a ' +\n 'dead end: the worst grade costs cultivation speed, not access, and a ' +\n 'Root Cleansing Pill moves you up a grade.

' +\n '' +\n '';\n var rootTotal = 0;\n MOD_ROOTS.forEach(function (r) { rootTotal += r.weight; });\n MOD_ROOTS.forEach(function (r) {\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
GradeRootChanceCultivationBreakthrough
' + r.n + '' + MOD_WIKI.safe(r.name) + '
' +\n MOD_WIKI.safe(r.desc) + '
' + Math.round(r.weight / rootTotal * 100) + '%×' + r.xp + '' + (r.odds >= 0 ? '+' : '') +\n Math.round(r.odds * 100) + '%
';\n return h;\n});\n\n/* --- 5. elemental mastery ------------------------------------------------- */\nMOD_wikiPage('mastery', 'Elemental mastery', function () {\n var h = '

Elemental mastery mod

' +\n '

Six masteries, each with a technique that fires on its own ' +\n 'during a fight. Combat is automatic and there is no ability picker, so a ' +\n 'technique is a chance on each of your swings to become something else — one ' +\n 'that scales with INT and the element rather than with your weapon.

' +\n '

Nothing fires while you are still Mortal: the channels have to be open ' +\n 'first. Chance is ' + (MOD_TECH.perLevel * 100).toFixed(1) + '% per mastery ' +\n 'level plus ' + (MOD_TECH.perRealm * 100).toFixed(1) + '% per realm, capped at ' +\n Math.round(MOD_TECH.cap * 100) + '%; a technique lands for ×' +\n MOD_TECH.power + ' of a normal strike.

' +\n '' +\n '';\n\n var realm = 0;\n try { realm = MOD_realm().n; } catch (e) {}\n MOD_MASTERY.forEach(function (m) {\n var lvl = Number(m.skill.lvl) || 0;\n var ch = realm < 1 ? 0\n : Math.min(lvl * MOD_TECH.perLevel + realm * MOD_TECH.perRealm, MOD_TECH.cap);\n h += '' +\n '' +\n '' +\n '';\n });\n h += '
MasteryTechniqueLevelChance now
' +\n MOD_WIKI.safe(m.skill.name) + '' + MOD_WIKI.safe(m.name) + '' + lvl + '' + (realm < 1 ? 'Mortal'\n : (ch * 100).toFixed(1) + '%') + '
';\n return h;\n});\n\n/* --- 6. titles ------------------------------------------------------------ */\nMOD_wikiPage('titles', 'Titles', function () {\n var perm = MOD_permanentRank();\n var held = MOD_titleCount();\n var total = 0; for (var kk in ttl) if (ttl[kk] && ttl[kk].name) total++;\n\n var h = '

Titles

' +\n '

A title\\'s rank runs 1 to 10 and is derived from the level ' +\n 'that earns it, so a rank is a statement about where in the game you are. ' +\n 'Only the title you are wearing applies — until renown makes the lower ranks ' +\n 'permanent.

' +\n '

Where you are

' +\n '' +\n '' +\n '' +\n '
Titles held' + held + ' of ' + total + '
Renown level' + (skl.rnwn ? skl.rnwn.lvl : 0) +\n ' of ' + MOD_RENOWN_MAX + '
Ranks that apply without wearing' +\n (perm >= 1 ? '1–' + perm : 'none yet') + '
' +\n\n '

Ranks

' +\n '';\n MOD_RANK_AT.forEach(function (lv, i) {\n var st = MOD_rankStyle(i + 1);\n h += '' +\n '';\n });\n h += '
RankEarned from levelColour
' + (i + 1) + '' + lv + '' + '●●●' + '
' +\n\n '

Renown

' +\n '

Renown is not trained. It counts the titles you hold, and each level ' +\n 'multiplies every stat by ' + (1 + MOD_RENOWN_RATE) + ' — compounding, so ' +\n 'level ' + MOD_RENOWN_MAX + ' is ×' +\n (Math.round(MOD_renownMult(MOD_RENOWN_MAX) * 1000) / 1000) + '. At renown N, ' +\n 'every title of rank N or below applies permanently.

' +\n '' +\n '';\n MOD_RENOWN_STEPS.forEach(function (n, i) {\n h += '= n ? 'done' : '') + '\">' +\n '';\n });\n h += '
RenownTitles neededAll statsPassive ranks
' + (i + 1) +\n '' + n + '×' +\n (Math.round(MOD_renownMult(i + 1) * 1000) / 1000) + '1–' + (i + 1) + '
' +\n\n '

Every title

' +\n '

Grouped by the skill that grants it. What is left is either ' +\n 'a ladder of its own — creatures killed, items gathered, the ' +\n 'cultivation realms — or a one-off from the story.

';\n\n var groups = {}, fams = {}, loose = [];\n for (var k in ttl) {\n var t = ttl[k];\n /* Unlike the picker, the wiki KEEPS the author's broken entries and says\n so — same call as area.clg, which is listed and tagged \"nothing spawns\"\n rather than quietly dropped. A reader looking for why a title never\n appears is better served by the row than by its absence. */\n if (!t || t.name === undefined || t.name === null) continue;\n var sk = MOD_TITLE_SKILL[k];\n if (sk && skl[sk]) { (groups[sk] = groups[sk] || []).push(k); continue; }\n var stem = MOD_titleStem(k);\n if (stem) { (fams[stem] = fams[stem] || []).push(k); continue; }\n loose.push(k);\n }\n /* Same rule as the picker: a family of one is not a family. */\n Object.keys(fams).forEach(function (stem) {\n if (fams[stem].length < 2) { loose.push(fams[stem][0]); delete fams[stem]; }\n });\n var order = Object.keys(groups).sort(function (a, b) {\n return skl[a].name < skl[b].name ? -1 : 1;\n });\n\n var row = function (k) {\n var t = ttl[k];\n var st = MOD_rankStyle(t.rar);\n var gen = null;\n for (var i = 0; i < MOD_SKILL_TITLES.length; i++) {\n if (MOD_SKILL_TITLES[i].key === k) { gen = MOD_SKILL_TITLES[i]; break; }\n }\n /* The author left a handful unfinished: ttl.ddcd carries the literal string\n \"null\" for both name and description, and shpt2/shpt3/mone3 are blank.\n None is granted by anything, so none can be earned. */\n var broken = !t.name || t.name === 'null';\n return '
' +\n '' +\n MOD_WIKI.safe(t.name || '(unnamed)') + '' +\n (broken ? 'unfinished' : '') +\n 'rank ' + (t.rar || 1) + '' +\n (gen ? 'lv ' + gen.rung + ' · +' +\n Math.round(gen.xp * 100) + '% ' + MOD_WIKI.safe(skl[gen.skill].name) +\n ' exp' : '') +\n (t.have === true ? 'held' : '') +\n '
' + MOD_wikiDesc(t.desc) + '
';\n };\n\n var byRank = function (a, b) { return (ttl[a].rar || 1) - (ttl[b].rar || 1); };\n order.forEach(function (sk) {\n groups[sk].sort(byRank);\n h += MOD_wikiGroup(MOD_WIKI.safe(skl[sk].bname || skl[sk].name),\n groups[sk].length, groups[sk].map(row).join(''), 3);\n });\n /* The ladders that belong to no skill, by the same derivation the picker\n uses — so the two cannot drift apart. Sorted by the label, since that is\n what the reader scans. */\n var famList = Object.keys(fams)\n .map(function (stem) {\n fams[stem].sort(byRank);\n return { stem: stem,\n label: MOD_titleFamilyLabel(stem, fams[stem].map(function (k) { return ttl[k]; })) };\n })\n .sort(function (a, b) { return a.label < b.label ? -1 : a.label > b.label ? 1 : 0; });\n if (famList.length) {\n /* Nested one level down rather than sitting beside the skills. Eleven more\n top-level summaries cost ~530px on a page that is already the tallest in\n the wiki, and these are a different KIND of thing from a skill anyway. */\n var famInner = '', famTotal = 0;\n famList.forEach(function (f) {\n famTotal += fams[f.stem].length;\n famInner += MOD_wikiGroup(MOD_WIKI.safe(f.label), fams[f.stem].length,\n fams[f.stem].map(row).join(''), 4);\n });\n h += MOD_wikiGroup('Ladders of their own', famTotal, famInner, 3);\n }\n if (loose.length) {\n loose.sort(byRank);\n /* The story titles are the one group with no skill to divide them, and\n there are enough of them to need dividing. Rank rather than the alphabet:\n it is the axis they are already sorted on, and the one a reader cares\n about — a rank is a statement about where in the game a title comes from. */\n var inner;\n if (loose.length <= MOD_WIKI_ALPHA_AT) {\n inner = loose.map(row).join('');\n } else {\n var byRankNo = {};\n loose.forEach(function (k) {\n var r = Math.min(Math.max(Number(ttl[k].rar) || 1, 1), 10);\n (byRankNo[r] = byRankNo[r] || []).push(k);\n });\n inner = '';\n Object.keys(byRankNo).sort(function (a, b) { return a - b; }).forEach(function (r) {\n var st = MOD_rankStyle(r);\n inner += MOD_wikiGroup('Rank ' + r + '', byRankNo[r].length,\n byRankNo[r].map(row).join(''), 4);\n });\n }\n h += MOD_wikiGroup('From the story', loose.length, inner, 3);\n }\n return h;\n});\n\n/* --- 7. areas ------------------------------------------------------------- */\nMOD_wikiPage('areas', 'Areas & enemies', function () {\n var h = '

Areas & enemies

' +\n '

Every area, what lives in it and at what levels, and what it ' +\n 'drops. Enemy strength is not read off these levels — see the note below the ' +\n 'table.

';\n\n /* Two of the game's 21 areas are not places and listing them misleads:\n nwh \"Somewhere\" — where global.current_z parks whenever you are NOT in\n a fight, populated by creature.default (\"Nothing\").\n tst \"Test\" — reached only from chss.tst, which has id -1 and is in no\n sector, so nothing in the game can travel there.\n The test for \"is this a place\" is whether you can get to it, and neither\n can be. Everything else is listed whether or not you have been. */\n var MOD_WIKI_NOT_PLACES = { nwh: 1, tst: 1 };\n\n var keys = [];\n for (var k in area) {\n if (area[k] && area[k].pop && !MOD_WIKI_NOT_PLACES[k]) keys.push(k);\n }\n /* Ordered by the middle of the level band, which is the closest thing the\n game has to a difficulty order. */\n keys.sort(function (a, b) {\n var mid = function (z) {\n if (!z.pop.length) return 0;\n var s = 0; z.pop.forEach(function (e) { s += (e.lvlmin + e.lvlmax) / 2; });\n return s / z.pop.length;\n };\n return mid(area[a]) - mid(area[b]);\n });\n\n keys.forEach(function (k) {\n var z = area[k];\n /* An area whose baked bands are not finite can never spawn anything — see\n the note rendered under its table. Derived rather than listed by name, so\n it stays true if the game changes. */\n var broken = !z.popc || z.popc.length !== z.pop.length ||\n z.popc.some(function (band) { return !band || !isFinite(band[1] - band[0]); });\n h += '

' + MOD_WIKI.safe(z.name) +\n ' ' + k + '' +\n (broken ? 'nothing spawns' : '') +\n '' + (z.size === -1 ? 'endless' : z.size + ' to clear') +\n '

';\n if (z.pop.length) {\n /* The spawn share is NOT pop[i].c. z_bake normalises those weights into\n area.popc — a list of [lo,hi] bands that mon_gen rolls a uniform\n random against — and the weights do not have to sum to 1, so they are\n off wherever they don't. The Southern forest's .35/.45/.25 are really\n 33/43/24%. Read the bands the game actually rolls against. */\n var bands = z.popc || [];\n h += '';\n z.pop.forEach(function (e, i) {\n var c = e.crt || {};\n var band = bands[i];\n var share = band ? band[1] - band[0] : NaN;\n h += '' +\n '' +\n '';\n });\n h += '
' + MOD_WIKI.safe(c.name || '?') + 'lv ' + e.lvlmin +\n (e.lvlmax !== e.lvlmin ? '–' + e.lvlmax : '') + '' + (isFinite(share)\n ? Math.round(share * 100) + '% of spawns'\n : '—') + '
';\n if (broken) {\n h += '
Nothing spawns here. The area\\'s entries carry ' +\n 'no spawn weight, so the table the game rolls against comes out as ' +\n 'NaN and no creature can ever match it. A bug in the base game, left ' +\n 'as it is — nothing in the game travels to this area either.
';\n }\n }\n if (z.drop && z.drop.length) {\n var d = z.drop.map(function (x) {\n return MOD_WIKI.safe((x.item && x.item.name) || '?') +\n ' ' + ((x.c || 0) * 100).toFixed(2) + '%';\n }).join(', ');\n h += '
Drops: ' + d + '
';\n }\n h += '
';\n });\n\n var fightsOrig = MOD_FIGHTS.mode === 'original';\n h += '

How hard an enemy is

' +\n '

It depends on the Fights setting, which is ' +\n (fightsOrig ? 'Original' : 'Scaled') + ' now. On Original (the default) ' +\n 'every area the author wrote is exactly as he wrote it: his creatures, his ' +\n 'level ranges, and you out-grow them as you do in his game. On Scaled every ' +\n 'area is sized to you. The three areas past the golem arena and the rank ' +\n 'duels are sized to you either way.

' +\n '

Sizing to you works like this. Enemies are not fitted to a level. Each spawn is solved against your ' +\n 'actual power at the moment it appears, aiming at roughly ' + MOD_ENEMY.kill +\n ' swings to kill it and ' + MOD_ENEMY.die + ' before it kills you, with a ' +\n 'guaranteed margin between the two so a fight is never a coin flip. Variety ' +\n 'comes from the area\\'s level band and each creature\\'s own stat weighting, ' +\n 'not from difficulty scaling.

' +\n '

The reason for that: within the first stage alone you go from 1 STR to ' +\n 'around 265, so anything anchored to a level is either trivial at the end of ' +\n 'a stage or impossible at the start of one.

';\n return h;\n});\n\n/* --- 8. items -------------------------------------------------------------\n The one place where \"list everything\" needs help: 321 of the game's 371\n plain items are the same inventory type, so grouping by type alone leaves a\n single group of 321. Anything past MOD_WIKI_ALPHA_AT gets an alphabetical\n sub-split as well — the oldest index pattern there is, and derived rather\n than invented, which a hand-made taxonomy would not be.\n ------------------------------------------------------------------------- */\n\nvar MOD_WIKI_ALPHA_AT = 60;\n\n/* The game's own inventory tabs (isort): ALL / WPN / EQP / USE / OTHER. Using\n its categories rather than new ones means a group here is a tab there. */\nvar MOD_WIKI_STYPE = { 2: 'Weapons', 3: 'Equipment', 4: 'Usable', 5: 'Other' };\n\nfunction MOD_wikiItemRow(it) {\n var val = it.v !== undefined ? it.v : (it.val !== undefined ? it.val : null);\n return '
' +\n '

' + MOD_WIKI.safe(it.name) +\n (it.rar > 1 ? 'rarity ' + it.rar + '' : '') +\n (val !== null ? '' + MOD_wikiNum(val) + ' coin' : '') +\n (it.amount > 0 ? '×' + it.amount + '' : '') +\n '

' + MOD_wikiDesc(it.desc) + '
';\n}\n\n/* The letter a name files under. Several items are quoted — the master's\n manuals are literally named \"Sword Saint Manual\" — and filing those under a\n quotation mark is no use to anyone looking for S. */\nfunction MOD_wikiLetter(name) {\n var m = String(name || '').replace(/^[^0-9A-Za-z]+/, '');\n return (m.charAt(0) || '?').toUpperCase();\n}\n\nfunction MOD_wikiByLetter(a, b) {\n var x = MOD_wikiLetter(a.name) + String(a.name || '').toLowerCase();\n var y = MOD_wikiLetter(b.name) + String(b.name || '').toLowerCase();\n return x < y ? -1 : (x > y ? 1 : 0);\n}\n\n/* Flat below the threshold, alphabetically sub-grouped above it. Ranges are cut\n from the entries actually present, so they stay even as content is added. */\nfunction MOD_wikiAlpha(entries, render, level) {\n if (entries.length <= MOD_WIKI_ALPHA_AT) return entries.map(render).join('');\n var per = Math.ceil(entries.length / Math.ceil(entries.length / 40));\n var out = '';\n for (var i = 0; i < entries.length; i += per) {\n var chunk = entries.slice(i, i + per);\n var first = MOD_wikiLetter(chunk[0].name);\n var last = MOD_wikiLetter(chunk[chunk.length - 1].name);\n out += MOD_wikiGroup(first === last ? first : first + '–' + last,\n chunk.length, chunk.map(render).join(''), level || 4);\n }\n return out;\n}\n\nMOD_wikiPage('items', 'Items', function () {\n var h = '

Items

' +\n '

Everything the game defines, whether or not you have seen ' +\n 'it. Grouped the way your inventory groups it. Values are the base sell ' +\n 'price where the game gives one.

';\n\n /* item[] first, split by the inventory tab it appears under. */\n var buckets = {};\n for (var k in item) {\n var it = item[k];\n if (!it || !it.name || it.name === 'dummy') continue;\n var lab = MOD_WIKI_STYPE[it.stype] || 'Uncategorised';\n (buckets[lab] = buckets[lab] || []).push(it);\n }\n ['Usable', 'Other', 'Weapons', 'Equipment', 'Uncategorised'].forEach(function (lab) {\n var list = buckets[lab];\n if (!list || !list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(lab, list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n\n /* and the equipment objects, which live in their own globals */\n [['Weapons', wpn], ['Armour', eqp], ['Shields', sld], ['Accessories', acc]]\n .forEach(function (g) {\n var list = [];\n for (var kk in g[1]) {\n if (g[1][kk] && g[1][kk].name && g[1][kk].name !== 'dummy') list.push(g[1][kk]);\n }\n if (!list.length) return;\n list.sort(MOD_wikiByLetter);\n h += MOD_wikiGroup(g[0], list.length,\n MOD_wikiAlpha(list, MOD_wikiItemRow, 4), 2);\n });\n return h;\n});\n\n/* --- 9. actions ----------------------------------------------------------- */\nMOD_wikiPage('actions', 'Actions', function () {\n var unlock = {};\n MOD_ACTION_UNLOCKS.forEach(function (u) {\n if (u.act) unlock[u.act.id] = (skl[u.skill] ? skl[u.skill].name : u.skill) +\n ' level ' + u.lv;\n });\n if (typeof act.mod_qi !== 'undefined') {\n unlock[act.mod_qi.id] = 'clearing the dojo\\'s Easiest, Easy and Normal fights';\n }\n\n var h = '

Actions

' +\n '

What you can be doing. One at a time, and only where the ' +\n 'action makes sense — unless \"Unrestricted actions\" is ticked in settings, ' +\n 'which lets several run at once anywhere. Both limits are deliberate; the ' +\n 'switch is there because they are not to everyone\\'s taste.

';\n\n var keys = [];\n for (var k in act) if (act[k] && act[k].name && act[k].name !== 'dummy') keys.push(k);\n keys.sort(function (a, b) { return act[a].name < act[b].name ? -1 : 1; });\n\n keys.forEach(function (k) {\n var a = act[k];\n var how = unlock[a.id];\n h += '
' +\n '

' + MOD_WIKI.safe(a.name) +\n (a.have === true ? 'unlocked'\n : 'locked') +\n (how ? 'from ' + MOD_WIKI.safe(how) + '' : '') +\n '

' + MOD_wikiDesc(a.desc) + '
';\n });\n return h;\n});\n\n/* --- 10. the dojo --------------------------------------------------------- */\nMOD_wikiPage('dojo', 'The dojo', function () {\n var h = '

The dojo

' +\n '

The instructor promises a reward every five levels. The base ' +\n 'game stops delivering at 30; the mod carries the same ladder to 110, on the ' +\n 'same terms.

' +\n '

Only the lowest rung you have not claimed is offered, which is how the ' +\n 'first six behave. Rewards are sized against the character experience curve ' +\n '(4×level3 + level2) — a level at 110 costs 5.3 ' +\n 'million, which is why the base game\\'s best pill is worth 0.3% of one there.

' +\n '' +\n '';\n\n MOD_DOJO_RUNGS.forEach(function (r) {\n var pill = item[r.pill];\n var extra = [];\n if (r.realmPill) extra.push('breakthrough pill for realm ' + r.realmPill);\n if (r.manual) extra.push('a choice of master\\'s manual');\n h += '' +\n '' +\n '' +\n '' +\n '' +\n '';\n });\n h += '
LevelRewardCoinAlso
' + r.lv + '' + (pill ? MOD_WIKI.safe(pill.name) + ' ×' + r.n\n : '—') + '' + MOD_wikiNum(r.coin) + '' + (extra.join(', ') || '—') + '' + (global.flags[r.flag] ? 'claimed' : '') +\n '
' +\n\n '

Spirit pills

' +\n '

Character experience in a bottle. The four the mod adds continue the ' +\n 'game\\'s own series up to the new curve.

' +\n '' +\n '';\n MOD_PILLS.forEach(function (p) {\n h += '';\n });\n h += '
PillExperience
' + MOD_WIKI.safe(p[2]) + '' +\n p[3].toLocaleString() + '
' +\n\n '

Master\\'s manuals

' +\n '

A third grade of skillbook, offered at levels 50, 75 and 100 as a choice ' +\n 'the same way the first one was. Each is worth +' +\n Math.round(MOD_MANUAL_RATE * 100) + '% experience in its skill, permanently.

' +\n '' +\n '';\n MOD_MANUALS.forEach(function (m) {\n h += '';\n });\n h += '
ManualSkill
' + MOD_WIKI.safe(m[2]) + '' +\n MOD_WIKI.safe(skl[m[0]] ? (skl[m[0]].bname || skl[m[0]].name) : m[0]) +\n '
';\n return h;\n});\n\n/* --- 11. mechanics -------------------------------------------------------- */\nMOD_wikiPage('mechanics', 'Mechanics', function () {\n return '

Mechanics

' +\n\n '

Damage is subtractive, in both directions

' +\n '

A hit does the attacker\\'s strength minus the defender\\'s strength, plus ' +\n 'the weapon, plus one, before criticals. A defender\\'s STR is their ' +\n 'armour — there is no separate armour stat doing that job.

' +\n '

Two consequences worth carrying around:

' +\n '
    ' +\n '
  • Damage does not taper, it stops. Once a defender\\'s STR passes ' +\n 'yours, your hits do not get small; they land for nothing. Progress against ' +\n 'something too strong is not slow, it is zero.
  • ' +\n '
  • A stat ratio tells you nothing. Comparing your HP to an enemy\\'s ' +\n 'strength ignores the subtraction entirely and reads several times off in ' +\n 'either direction.
  • ' +\n '
' +\n '

This is also the reason enemies are never scaled by raising ' +\n 'their strength: it would raise their offence and their armour together, and ' +\n 'the second effect arrives first.

' +\n\n '

Criticals carry the late game

' +\n '

Critical rate reaches about a third at the top of the ladder and a ' +\n 'critical is roughly eight times a normal swing, so around three quarters of ' +\n 'all damage done at that point is critical damage. Anything that raises ' +\n 'critical rate or critical damage is worth more than it looks; Sharp Eye and ' +\n 'War are the two that do.

' +\n\n '

Hit chance

' +\n '

Landing a hit is a contest of agility. It is a ratio rather than a ' +\n 'subtraction, so agility behaves more gently than strength does — falling ' +\n 'behind on it costs you accuracy, not everything.

' +\n\n '

Skill sections and parents

' +\n '

Each of the ' + Object.keys(MOD_SECTIONS).length + ' sections has a parent ' +\n 'skill. A parent earns ' + Math.round(MOD_PARENT.feed * 100) + '% of everything ' +\n 'its children earn and grants no stats of its own. In return, a child below ' +\n 'the parent\\'s level trains ' + Math.round(MOD_PARENT.perLevel * 100) +\n '% faster per level of lag, up to ' + MOD_PARENT.maxGap + ' levels behind. ' +\n 'Bringing one section up makes every skill in it cheaper to catch up.

' +\n\n '

Saving

' +\n '

Three slots, switched from the bottom bar. The game builds its world once ' +\n 'at startup and cannot unload a save, so switching slots or starting a new ' +\n 'game reloads the page. Exporting a save before running anything unusual is ' +\n 'always the safe move.

';\n});\n\n/* --- the document ---------------------------------------------------------\n One string, no fetches, no dependencies: this has to work from file:// as\n readily as from a server.\n ------------------------------------------------------------------------- */\n\nfunction MOD_wikiCss() {\n return [\n ':root{color-scheme:dark}',\n '*{box-sizing:border-box}',\n /* Menlo and Consolas before the generic: with MS Gothic missing (any Mac)\n \"monospace\" is Courier, which Safari sets wider than Chrome does, and\n table cells wrapped on one and not the other. */\n 'body{margin:0;background:#060a1c;color:#dfe6f5;font:14px/1.6 \"MS Gothic\",' +\n '\"MS Mincho\",Menlo,Consolas,\"DejaVu Sans Mono\",monospace;display:flex;min-height:100vh}',\n 'a{color:#7cb0ff}',\n 'nav{width:210px;flex:0 0 210px;background:#081040;border-right:1px solid #46a;' +\n 'position:sticky;top:0;height:100vh;overflow:auto;padding:14px 0}',\n 'nav h2{font-size:13px;color:#9fb4d8;margin:0 0 10px 14px;letter-spacing:1px}',\n 'nav a{display:block;padding:6px 14px;text-decoration:none;color:#cfe0ff;' +\n 'border-left:3px solid transparent}',\n 'nav a:hover{background:#154080}',\n 'nav a.on{background:#123070;border-left-color:#7cb0ff;color:#fff}',\n 'nav .srch{margin:0 10px 12px;width:calc(100% - 20px);background:#050a20;' +\n 'color:#dfe6f5;border:1px solid #46a;padding:5px;font:inherit}',\n 'nav .xall{display:block;margin:12px 10px 0;color:#8296b8;font-size:12px;' +\n 'cursor:pointer;-webkit-user-select:none;user-select:none}',\n 'nav .xall input{vertical-align:-1px;margin-right:4px}',\n 'details.wk-g{border:1px solid #17224a;background:#0a1130;margin:8px 0}',\n 'details.wk-g>summary{cursor:pointer;padding:7px 12px;list-style:none;' +\n 'background:#0d1740}',\n 'details.wk-g>summary:hover{background:#154080}',\n 'details.wk-g>summary::-webkit-details-marker{display:none}',\n // a caret drawn rather than borrowed, so it is the same in every browser\n 'details.wk-g>summary::before{content:\"\\\\25B8\";color:#7cb0ff;' +\n 'display:inline-block;width:1em;transition:transform .12s}',\n 'details.wk-g[open]>summary::before{transform:rotate(90deg)}',\n 'details.wk-g>summary h2,details.wk-g>summary h3{display:inline;margin:0;' +\n 'font-size:15px;color:#fff}',\n '.wk-gi{padding:2px 12px 10px}',\n 'details.wk-g details.wk-g>summary h4{display:inline;margin:0;font-size:13px;' +\n 'color:#cfe0ff;font-weight:normal}',\n 'details.wk-g details.wk-g>summary{padding:5px 10px;background:#0b1435}',\n 'main{flex:1;min-width:0;padding:22px 30px 80px;max-width:1000px}',\n 'h1{font-size:26px;margin:0 0 6px;color:#fff;border-bottom:1px solid #46a;' +\n 'padding-bottom:8px}',\n 'h2{font-size:19px;color:#bcd4ff;margin:30px 0 8px}',\n 'h3{font-size:15px;color:#fff;margin:0 0 4px}',\n 'p.lede{color:#a8bcdc;margin:0 0 18px}',\n '.dim{color:#8296b8;font-weight:normal}',\n '.flav{font-size:12px;font-style:italic}',\n '.note{background:#0b1638;border-left:3px solid #46a;padding:8px 12px;' +\n 'color:#a8bcdc;margin:14px 0}',\n 'code,pre{background:#0b1638;border:1px solid #24356e;padding:1px 5px;' +\n 'color:#9fd0ff}',\n 'pre{padding:10px;overflow-x:auto}',\n 'dl dt{color:#fff;margin-top:14px}',\n 'dl dd{margin:2px 0 0 0;color:#a8bcdc}',\n 'ul{color:#a8bcdc}',\n 'table.wk-tbl{border-collapse:collapse;width:100%;margin:10px 0;' +\n 'display:block;overflow-x:auto}',\n 'table.wk-tbl th{text-align:left;color:#9fb4d8;font-weight:normal;' +\n 'border-bottom:1px solid #46a;padding:5px 9px;white-space:nowrap}',\n 'table.wk-tbl td{border-bottom:1px solid #17224a;padding:5px 9px;' +\n 'vertical-align:top}',\n 'table.wk-tbl tr.done td{color:#cfe0ff}',\n 'table.wk-tbl tr.here{background:#123070}',\n '.num{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}',\n '.wk-e{border:1px solid #17224a;background:#081040;padding:9px 12px;' +\n 'margin:7px 0;border-radius:2px}',\n 'tr.wk-e{border:0;background:none}',\n '.wk-e.done{border-color:#2a4a8c}',\n '.wk-e h3{display:flex;flex-wrap:wrap;gap:8px;align-items:baseline}',\n '.lv{margin-left:auto;font-size:12px;color:#8296b8;font-weight:normal}',\n '.lv.capped{color:#ffd24a}',\n '.rank{font-size:12px;color:#8296b8;font-weight:normal}',\n '.desc{color:#a8bcdc;font-size:13px}',\n '.proj{color:#ffd24a;font-size:12px;margin-top:3px}',\n '.tag{font-size:11px;border:1px solid #46a;color:#9fb4d8;padding:0 5px;' +\n 'border-radius:2px;font-weight:normal}',\n '.tag.mod{border-color:#7a4ea8;color:#c07bff}',\n '.tag.done{border-color:#2e7d4f;color:#6fdc9a}',\n '.tag.here{border-color:#ffd24a;color:#ffd24a}',\n '.tag.warn{border-color:#a8562e;color:#ff9a5c}',\n 'table.wk-perks{border-collapse:collapse;margin:6px 0 0;width:100%}',\n 'table.wk-perks td{padding:2px 8px 2px 0;font-size:12px;color:#8296b8;' +\n 'border-bottom:1px solid #101a3c}',\n 'table.wk-perks tr.done td{color:#cfe0ff}',\n 'table.wk-perks td.tick{color:#6fdc9a;width:1em;text-align:right}',\n // fixed columns so the level bands line up between one area and the next\n 'table.wk-pop{table-layout:fixed}',\n 'table.wk-pop td:first-child{width:60%}',\n 'table.wk-pop td:nth-child(2){width:20%}',\n '.wk-title .tname{font-weight:bold}',\n '.wk-now{background:#0b1638;border:1px solid #24356e;padding:10px 14px;' +\n 'margin:0 0 18px;display:inline-block;min-width:320px}',\n '.wk-now h3{color:#9fb4d8;font-weight:normal;font-size:13px;' +\n 'letter-spacing:1px;margin-bottom:6px}',\n '.wk-now td{padding:2px 16px 2px 0}',\n '.wk-now td:last-child{color:#fff;text-align:right}',\n '.pg{display:none}.pg.on{display:block}',\n 'footer{color:#5a6c8c;font-size:12px;border-top:1px solid #17224a;' +\n 'margin-top:40px;padding-top:12px}',\n '@media(max-width:760px){body{display:block}nav{width:auto;height:auto;' +\n 'position:static;flex:none}main{padding:16px}}'\n ].join('\\n');\n}\n\n/* Filtering, page switching and the hash. Small enough to inline, and inlining\n it is the point — the document has to stand on its own. */\nfunction MOD_wikiJs() {\n return [\n 'var pages=[].slice.call(document.querySelectorAll(\".pg\"));',\n 'var links=[].slice.call(document.querySelectorAll(\"nav a[data-p]\"));',\n 'function show(id){',\n ' var found=false;',\n ' pages.forEach(function(p){var on=p.id===\"pg-\"+id;if(on)found=true;',\n ' p.className=\"pg\"+(on?\" on\":\"\")});',\n ' if(!found&&pages.length){pages[0].className=\"pg on\";id=pages[0].id.slice(3)}',\n ' links.forEach(function(a){a.className=a.dataset.p===id?\"on\":\"\"});',\n ' window.scrollTo(0,0);return id;}',\n 'links.forEach(function(a){a.addEventListener(\"click\",function(e){',\n ' e.preventDefault();location.hash=a.dataset.p;show(a.dataset.p)})});',\n 'window.addEventListener(\"hashchange\",function(){show(location.hash.slice(1))});',\n 'show(location.hash.slice(1)||\"start\");',\n // Search filters entries, hides groups left with none, and OPENS the groups\n // that still have some — a match hidden inside a collapsed group reads as\n // no match at all, which is worse than not having search.\n 'var box=document.getElementById(\"srch\");',\n 'var xall=document.getElementById(\"xall\");',\n 'var groups=[].slice.call(document.querySelectorAll(\"details.wk-g\"));',\n 'function filter(){',\n ' var q=box.value.trim().toLowerCase();',\n ' pages.forEach(function(p){',\n ' [].slice.call(p.querySelectorAll(\".wk-e\")).forEach(function(e){',\n ' e.style.display=(!q||e.textContent.toLowerCase().indexOf(q)>=0)?\"\":\"none\"})});',\n // Every page, not just the one on screen: the nav says how many entries\n // match in each, dims the ones with none, and Enter jumps to the first\n // page that has any. A prose page with no entries counts as one if its text\n // contains the query.\n ' links.forEach(function(a){',\n ' var pg=document.getElementById(\"pg-\"+a.dataset.p),n=0;',\n ' if(a.dataset.l===undefined)a.dataset.l=a.textContent;',\n ' if(q&&pg){var es=[].slice.call(pg.querySelectorAll(\".wk-e\"));',\n ' if(es.length)es.forEach(function(e){if(e.textContent.toLowerCase().indexOf(q)>=0)n++});',\n ' else if(pg.textContent.toLowerCase().indexOf(q)>=0)n=1}',\n ' a.dataset.n=n;a.style.opacity=(q&&!n)?\".4\":\"\";',\n ' a.textContent=a.dataset.l+(q&&n?\" (\"+n+\")\":\"\")});',\n ' groups.forEach(function(g){',\n ' var any=[].slice.call(g.querySelectorAll(\".wk-e\")).some(function(e){',\n ' return e.style.display!==\"none\"});',\n ' g.style.display=any?\"\":\"none\";',\n ' g.open=xall.checked||(!!q&&any)}); }',\n 'box.addEventListener(\"input\",filter);',\n 'box.addEventListener(\"keydown\",function(e){',\n ' if(e.key===\"Escape\"){box.value=\"\";filter()}',\n ' if(e.key===\"Enter\"&&box.value.trim()){',\n ' var cur=links.filter(function(a){return a.className===\"on\"})[0];',\n ' if(cur&&+cur.dataset.n)return;',\n ' var hit=links.filter(function(a){return +a.dataset.n>0})[0];',\n ' if(hit){location.hash=hit.dataset.p;show(hit.dataset.p)}}});',\n // Everything is collapsed by default because three of these pages run past\n // 40,000px open. This is the way back to one long document, which is what\n // the browser's own find needs.\n 'xall.addEventListener(\"change\",filter);'\n ].join('\\n');\n}\n\nfunction MOD_wikiBuild() {\n var nav = '', body = '';\n MOD_WIKI_PAGES.forEach(function (pg) {\n nav += '' +\n MOD_WIKI.safe(pg.label) + '';\n var inner;\n /* One page throwing must not take the wiki down with it — the page says so\n and the rest still renders. */\n try { inner = pg.build(); }\n catch (e) {\n inner = '

' + MOD_WIKI.safe(pg.label) + '

This page ' +\n 'could not be built: ' + MOD_WIKI.safe(e.message) + '

';\n console.warn('[mod] wiki page \"' + pg.id + '\" failed: ' + e.message);\n }\n body += '
' + inner + '
';\n });\n\n var stamp = new Date().toLocaleString();\n return '' +\n '' +\n '' + MOD_WIKI.safe(MOD_WIKI.title) + '' +\n '' +\n '' +\n '
' + body +\n '
Generated from the running game at ' + MOD_WIKI.safe(stamp) +\n ' — mod ' + MOD_WIKI.safe(MOD.version) + '. Everything here is read from the ' +\n 'live data, including your own save, so it cannot fall out of step with the ' +\n 'game. The game is truezhangwei\\'s; the mod is not endorsed by the author.' +\n '
' +\n '