// ==UserScript== // @name proto23 mod // @namespace https://github.com/Xeus24/23html.github.io // @version 4.7 // @description The proto23 mod (4.7), on the author's hosted game. Backs up your save before it first runs. // @author Xeus24 // @homepageURL https://github.com/Xeus24/23html.github.io // @downloadURL https://raw.githubusercontent.com/Xeus24/23html.github.io/main/userscript/proto23-mod.user.js // @updateURL https://raw.githubusercontent.com/Xeus24/23html.github.io/main/userscript/proto23-mod.user.js // @match https://23html.github.io/ // @match https://23html.github.io/index.html // @run-at document-end // @grant none // ==/UserScript== // GENERATED by tools/build-userscript.mjs from mod.js and // changelog/mod-changelog.html. Do not edit; rebuild. (function () { 'use strict'; var BUILT_FOR = 470; // the author's global.ver this build was tested against if (typeof window.MOD !== 'undefined') return; // the page has the mod already if (typeof window.global !== 'object' || typeof window.you !== 'object') return; // not the game if (window.global.ver !== BUILT_FOR) { console.warn('[proto23 mod] built for game v' + BUILT_FOR + ', this is v' + window.global.ver + '. Loading anyway; your save is backed up first (modBackups()).'); } // read by mod.js section 48, which says so in the game when the versions differ window.MOD_USERSCRIPT = { builtFor: BUILT_FOR, version: "4.7" }; window.MOD_CHANGELOG_HTML = "\n\n
\nmod.js. None of this is the work of the\ngame's author — the game's own changelog is here.<script src=\"mod.js\"> line from index.html and none of it applies.setCatchUp(seconds) changes the cap, 0 turns it off; it travels with your save.nojekyll file so Pages serves the files as they are; a check that every Tower pill is in stock within 8 hoursMOD_ITEM_IDS now holds the ranges with the class table written beside them, so themodItems() prints every added item with the class its id gives itmodMeat() prints the table per area and seasonload() restores its namespace from, and no rarity mayequip() runs off the end of global.text.wecs the moment you wear itttl.ddcd has the literal string \"null\"giveTitle pushes each earned title onto TWO arrays —global.titles and global.titlese — and load() rebuilds global.titles from the savetitlese on top. So everything earned thattitlese empty — but one load is enough,save() writes the inflated array straight back outglobal.titles, so each title got two rowsglobal.titles holds references to the objects in ttl, so the same object twice istitlese is the author'screature.default explicitly,acts/you.skls/furn/qsts all stay distinct across repeated save/load cyclesyou.rank and keeping the earned rank in global.flags, likemod_realm. Nothing is written into a stat, so there is no delta to track and nothingyou.res and skl.p both fell into.you.hpmax at the end of the allbuff wrapper, but allbuffstat_r(), which recomputes hpmax from the base — so the raised value onlyyou.hpmax was 10,050 every time; the2010 — the Companions parent (2000 + section 10,2010 + 0, section 29). Ids are the save's key for abreak, so one shared id pushedglobal.lst_loc is matched the same way, so both would have drawn.false, and an item that sets a flag nothing clears iswiki button on the bottom bar and a \"Game wiki\" row in settings, bothAn 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 '| 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) + ' |
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.
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 '| Cap | Stage | ' +\n 'What opens it | |
|---|---|---|---|
| ' + t.cap + ' | ' +\n '' + MOD_WIKI.safe(t.name) + ' | ' +\n '' + MOD_WIKI.safe(need) + ' | ' +\n '' + (here ? 'you are here'\n : got ? 'reached' : '') + ' |
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 '| Level | Experience for the next | ' +\n 'Original game |
|---|---|---|
| ' + l +\n ' | ' + MOD_wikiNum(MOD_expnextFor(l)) + ' | ' +\n MOD_wikiNum(MOD_vanillaExpnext(l)) + ' |
' + 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 += '| ' + m.lv + ' | ' +\n '' + MOD_WIKI.safe(m.p || '—') + ' | ' +\n '' + (m.g === true ? '✓' : '') + ' |
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 '| Realm | ' + MOD_WIKI.safe(MOD_realmTitle()) + ' |
| Spiritual Root | ' + MOD_WIKI.safe(MOD_root().name) + ' |
| Qi Circulation | level ' + 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') + ' |
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 '| Realm | Name | Qi Circ. | ' +\n 'Stats | Health | Pill |
|---|---|---|---|---|---|
| ' + r.n + ' | ' +\n '' + MOD_WIKI.safe(r.name) + ' ' +\n MOD_WIKI.safe(r.desc) + ' | ' +\n '' + r.qic + ' | ' +\n '×' + r.mult + ' | ' +\n '×' + r.hp + ' | ' +\n '' + (pill ? MOD_WIKI.safe(pill.name) : '—') +\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 '| Realms | Source |
|---|---|
| 1 | The dojo instructor, when qi ' +\n 'circulation is first taught |
| 2–5 | The Herbalist in the village ' +\n '(vendor.pha1), restocked |
| 6–10 | The dojo\\'s Level Advancement ' +\n 'ladder, and the Pill Tower |
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 '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 '| Levels past the threshold | ' +\n 'Chance | After full seclusion |
|---|---|---|
| +' + o + ' | ' +\n Math.round(base * 100) + '% | ' +\n Math.round(Math.min(base + MOD_CULT.secludeOdds, 0.95) * 100) + '% |
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 '| Source | Rate |
|---|---|
| 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 time | always |
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 '| Realm | Climb (Circulate Qi, 1x) | ' +\n 'Insight to enter | Full seclusion | Qi Deviation (in-game) | ' +\n '
|---|---|---|---|---|
| ' + MOD_WIKI.safe(r.name) + ' | ' +\n '' + wkHours(MOD_realmClimbSec(r.n)) + ' | ' +\n '' + MOD_insightNeed(r.n) + ' | ' +\n '' + wkHours(MOD_secludeFull(r.n)) + ' | ' +\n '' + (MOD_devMinutes(r.n) / 1440).toFixed(1) + ' days |
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 '| Layer | Called | ' +\n '
|---|---|
| ' +\n li + ' | ' + MOD_LAYER_ORD[li] + ' layer, ' +\n MOD_WIKI.safe(MOD_layerStage(li)) + ' |
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 '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 '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 '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 '| Realm | Bolts | ' +\n 'Of your max HP |
|---|---|---|
| ' + MOD_WIKI.safe(r.name) + ' | ' +\n '' + MOD_tribulationBolts(r.n) + ' | ' +\n '' + Math.round(MOD_tribulationCost(r.n) * 100) + '% |
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 '| Grade | Root | Chance | ' +\n 'Cultivation | Breakthrough |
|---|---|---|---|---|
| ' + r.n + ' | ' +\n '' + MOD_WIKI.safe(r.name) + ' ' +\n MOD_WIKI.safe(r.desc) + ' | ' +\n '' + Math.round(r.weight / rootTotal * 100) + '% | ' +\n '×' + r.xp + ' | ' +\n '' + (r.odds >= 0 ? '+' : '') +\n Math.round(r.odds * 100) + '% |
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 '| Mastery | Technique | ' +\n 'Level | Chance now |
|---|---|---|---|
| ' +\n MOD_WIKI.safe(m.skill.name) + ' | ' +\n '' + MOD_WIKI.safe(m.name) + ' | ' +\n '' + lvl + ' | ' +\n '' + (realm < 1 ? 'Mortal'\n : (ch * 100).toFixed(1) + '%') + ' |
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 '| 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') + ' |
| Rank | ' +\n 'Earned from level | Colour |
|---|---|---|
| ' + (i + 1) + ' | ' + lv + ' | ' +\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 '| Renown | Titles needed | ' +\n 'All stats | Passive ranks |
|---|---|---|---|
| ' + (i + 1) +\n ' | ' + n + ' | ×' +\n (Math.round(MOD_renownMult(i + 1) * 1000) / 1000) + ' | ' +\n '1–' + (i + 1) + ' |
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 '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(c.name || '?') + ' | ' +\n 'lv ' + e.lvlmin +\n (e.lvlmax !== e.lvlmin ? '–' + e.lvlmax : '') + ' | ' +\n '' + (isFinite(share)\n ? Math.round(share * 100) + '% of spawns'\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 '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 = '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 += '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 '| Level | Reward | Coin | ' +\n 'Also | |
|---|---|---|---|---|
| ' + r.lv + ' | ' +\n '' + (pill ? MOD_WIKI.safe(pill.name) + ' ×' + r.n\n : '—') + ' | ' +\n '' + MOD_wikiNum(r.coin) + ' | ' +\n '' + (extra.join(', ') || '—') + ' | ' +\n '' + (global.flags[r.flag] ? 'claimed' : '') +\n ' |
Character experience in a bottle. The four the mod adds continue the ' +\n 'game\\'s own series up to the new curve.
' +\n '| Pill | Experience | ' +\n '
|---|---|
| ' + MOD_WIKI.safe(p[2]) + ' | ' +\n p[3].toLocaleString() + ' |
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 '| Manual | Skill | ' +\n '
|---|---|
| ' + MOD_WIKI.safe(m[2]) + ' | ' +\n MOD_WIKI.safe(skl[m[0]] ? (skl[m[0]].bname || skl[m[0]].name) : m[0]) +\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 '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 '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 '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 '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 '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 = '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 += '