Skip to content

A local mod for proto23: progression to level 110, rebuilt enemy scaling, save slots - #3

Open
Xeus24 wants to merge 44 commits into
23html:mainfrom
Xeus24:main
Open

Xeus24 wants to merge 44 commits into
23html:mainfrom
Xeus24:main

Conversation

@Xeus24

@Xeus24 Xeus24 commented Sep 9, 2026 •

Copy link
Copy Markdown

Hello — this is a mod of proto23 built on top of your game. Opening it as a PR because it turned up a handful of genuine bugs in the base game that you may want regardless of what you think of the rest, and it seemed better to show you than not.

I would not expect you to merge this as-is. It is 15k lines and it makes opinionated design changes to your game. It is probably most useful as something to read, cherry-pick from, or ignore. The bug findings near the bottom are the part I would actually flag.

How it is put together

Everything lives in mod.js, loaded by one line appended to the bottom of index.html:

<!-- local mod: remove this line to disable -->
<script src="mod.js"></script>

That tag is the only change to any file of yours. Deleting the line reverts everything. mod.js wraps rather than edits: it loads last, shares global scope, and preserves every return value it intercepts. Nothing in your file is rewritten.

The mod's own changelog lives in a separate changelog/mod-changelog.html. changelog/changelog.html is yours and is byte-identical to main — an earlier revision of this branch prepended to it, and that was moved out precisely so the script tag stays the only edit.

What it changes

  • Skill caps tied to story progress, 10 through 110, and a geometric exp curve replacing the base one.
  • Enemy scaling rewritten — solved per spawn against the player's actual power rather than fitted to a level.
  • Perks on every skill at 10/25/50/60/75/90/110. 81 of 94 skills previously gave nothing between level 51 and 110.
  • Title ranks 1-10 derived from the level that earns them, five titles per skill, and titles that carry an effect.
  • The dojo's Level Advancement ladder carried from level 30 to 110 — the instructor already promises "a reward after every 5 levels" and the ladder stops at 30.
  • Cultivation realms with breakthroughs that cost a pill and can fail, and six elemental mastery skills whose techniques fire in combat.
  • The catacombs opened — all 26 rooms are yours, untouched; the mod adds only the Village Center entrance, which is where their own exit already led. And the Pill Tower, which you wrote a choice for and commented out.
  • A rank ladder. The Power rank under the portrait is computed from stats, and rank 1 turns out to be reachable only with every skill at 110 — at 108 across the board you are still rank 3, and all ten ranks live inside the last fifteen skill levels. Ten named challengers now give it somewhere to be won instead.
  • A wiki, generated from the live game data (skl, area, item, ttl, act) rather than written, so it cannot disagree with the game.
  • Three save slots, selling, enemy coin drops, three areas past the golem arena, and effects for the skills and affinities that were defined but inert.

MOD-NOTES.md is the full design record with the reasoning behind each decision.

Things in your code you may want to know about

These are base-game findings, independent of the mod:

  • dscr() type 5, rank 7 throws. The branch sets this.dl.style; every other branch sets this.label.style, and this.dl is only assigned in the type 6/7 branches. It has never run because no base-game title exceeds rar 5, so the tooltip would die half-built the first time one did.
  • The catacombs are unreachable. All 26 locations exist, with ambient text and a bestiary entry pointing at them, but nothing links into chss.catamn — the only smove(chss.catamn) is from inside cata1 going back.
  • area.clg (Damp cellar) can never spawn anything. Its pop entries carry no c, so z_bake produces NaN bands and no roll can ever match. Left as-is and flagged in the wiki rather than fixed.
  • eqp.dummy is shared by every creature, and lvlup writes the player's unarmed progression into it (aff[0] = you.lvl/5, cls[2] = you.lvl/4). So every monster silently inherits it; by character level 20 that is a 2.1x multiplier on their damage.
  • dmg_calc's b multiplier never reaches ctr_r — both branches that set it shadow it with a let inside their own block, so the you.luck/25+1 crit bonus is dead.
  • The defence term goes negative at high skill levels. With shdc high, the final multiplier (100 - (aff*5*(1+shdc/20) + cls*5*(1+shdc/20)))/100 inverts, so the player's defence is added to incoming damage instead of subtracted.
  • The marketplace can be locked out permanently once anything can sell items. The "Pamphlet" is the only object in the game that sets the flag, and the Paper Boy retires the moment it is given rather than read — so losing it takes the marketplace, its three shops, the guard-duty quest and everything behind them, with no way back.
  • Four affinity skills, enmondren, and the dfl sell rates are defined but never wired up.

Testing

25 Playwright scripts under tests/, driving a real browser against a served copy of the game. npm test runs them. The broadest checks every area, every creature, both ends of every level band, ten story tiers and three skill builds — 2,790 matchups, all required to be winnable. tests/ids.mjs exists because the mod shipped an id collision that doubled the skill sheet on every save/load, which is worth knowing about if you ever add skills programmatically: the loader matches ids in a loop with no break.

Thanks for making the game. Happy to split any of this up, drop most of it, or close it if it is not something you want near the repo.

🤖 Generated with Claude Code

Xeus24 and others added 30 commits September 8, 2026 19:50
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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015hvauCeK8WKVAPwnyWsNHp
The design record had accumulated large verbatim duplicate spans from
regenerated content being appended rather than merged:

- Removed a second full copy of the "Selling -> Full perk audit ->
  Interference checks -> Two latent problems" run.
- Removed two back-to-back mega-duplicates (one identical to the
  "Skills now do what their text says" .. "Parents start at level 1"
  span, one identical to the Selling..latent-problems span).
- Promoted the surviving "Selling" heading from H3 to H2; it was the
  only section in that run nested a level too deep, reading as if it
  were part of the tooltip-wording fix above it.

File goes from 2209 to 1171 lines with no unique content lost
(verified: the result equals the original minus the duplicate spans).

Also moved "Balance, fourth pass" to sit directly after "Balance, third
pass" so the four balance passes read in order through the document; it
had been placed far ahead of the passes it builds on. Verified as a pure
relocation (identical line multiset).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…caling

Three changes, plus the test coverage that found the last one.

Skill consolidation (v2.0). The ~100 "discovered by playing" skills are
merged down to 10, with the per-stat effect budget and the permanent /
conditional split preserved exactly, so player power is unchanged by
construction. Each survivor keeps its v1 id: skill level and milestone
flags are stored by id, so v1 saves still load and merged-away skills
simply do not restore. Renumbering them would have collided ids onto
skills with fewer milestone slots and thrown on load.

A reachable level 110 (v2.0). The base game's skill curve,
expnext = 50 + (lvl+1)^ln(9*lvl+1), is super-exponential: 109 -> 110
alone costs 1.16e14 xp, so the cap ladder was unclimbable by ~10^13.
Section 19 replaces it per skill instance with a geometric curve tuned
so a steadily-ticking skill reaches 110 in about six months at 1x. Only
skills are re-curved; character level is untouched. The three added
areas are now gated behind golem arena IV and open in order.

Enemy scaling rewritten (v2.1). The old model multiplied strm/aglm/intm,
which is wrong: dmg_calc is subtractive, so an enemy's STR is its armour
and scaling it clamps your damage to zero rather than lengthening the
fight. Measured through the game's own math, the tutorial needed 63,000
swings and killed you in one, and tiers 3-9 were all kill 1 / die 1. The
old grid search had been scoring enemyHP/playerSTR, a proxy that ignores
the subtraction entirely.

Fights now have three independent fields: hpm for length, a dmg_calc
wrapper for threat (offence without armour), aglm solved for a hit rate.
Nothing is replicated -- the base game's defence term goes negative at
high skill levels (shdc at 110 makes its last multiplier -12.3, so your
defence is added to the enemy's damage) and is ill-conditioned besides,
so each spawn runs the real dmg_calc a few times in both directions
inside MOD_probe and scales against what came out. Targets are ratios
anchored to your power at spawn, because within tier 0 alone the player
goes from STR 1 to STR 265. Save format untouched.

Verified: 2,490 matchups -- every area (21), every creature, both ends
of every level band, ten story tiers, three skill builds -- all winnable,
whiff 0% throughout, twelve consecutive clean runs. Real battles through
attack() resolve and are won. 474/474 perks pass. v1 saves load clean.

Two measurement bugs found on the way, both recorded in MOD-NOTES:
dmg_calc grants skill exp, so sampling it was levelling the player
mid-sweep; and player damage is two tight clusters rather than one
spread, so MOD_meanDamage stratifies on the crit roll instead of taking
a sample mean that is mostly "how many crits landed".

realbal.mjs, tierfit.mjs and sweep5.mjs are deleted -- they measured the
stat-ratio proxy or swept dials that no longer exist, and would report
confidently on a model the mod no longer has.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The game keeps exactly one save: save() writes localStorage "v0.3" and
load() reads it, both by name, and load() runs once from a window load
listener. There is no unload and no reset.

So rather than edit either function, "v0.3" always holds whichever slot
is live, with a mirror beside it (p23_slot_N, p23_slotmeta_N,
p23_slot_active). save() is wrapped to refresh the active slot's mirror,
so it stays current whatever triggered the save -- button, autosave, or
an area's own call. Switching writes "v0.3" and reloads the page, which
is not laziness: the game builds its world at startup and reads the save
once. "Start new save" is the same move with the slot cleared first, so
the game finds nothing and starts a fresh character.

A "saves" button in the save bar opens a panel listing the three slots
with the character, level, cap and save time in each; a "new save"
button starts a fresh character in the first free slot, or opens the
panel to choose when all three are in use rather than picking one to
destroy. Console equivalents: modSaves(), modSwitchSave(n), modNewSave(n),
modDeleteSave(n).

Two things it is careful about. Boot adopts but never overwrites: a
missing mirror is filled from "v0.3", so a character from before this
existed is picked up rather than orphaned, while the live key is never
written from a mirror that could only be staler. And nothing destructive
happens without the target in front of you -- overwriting or deleting a
slot names the character and level in the confirm, and leaving a slot
saves it first, abandoning the move if that save throws.

The base game's "delete the save" was localStorage.clear(), which would
have taken all three slots and the mod's settings with it. Rebound to
the active slot only, by cloning the node, since the game's handler is
anonymous.

tests/saveslots.mjs drives the real UI and the real save()/load() through
the whole cycle: three characters coexisting, switching both directions,
starting fresh without touching the others, deleting a slot you are not
in, settings surviving a deletion, and a pre-slot save being adopted.
Also verified by hand in a browser. Full suite green, 13 scripts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two changes to how the mod's four actions work.

They are earned now. Circulate Qi, Forage, Practice Calligraphy and
Endurance Drill used to be granted 2.5 seconds after load and re-granted
on a 5-second timer, so a new character had four unexplained abilities
before leaving the tutorial. Each now comes from a milestone on the
base-game skill it grows out of, which is the game's own mechanism --
skl.walk level 1 is what grants "Run":

  Toughness  4  Endurance Drill       taking hits teaches you to condition
  Harvesting 4  Forage                gathering teaches you to search
  Temperance 5  Circulate Qi          letting go teaches you to hold
  Literacy   8  Practice Calligraphy  read before you write

All four skills train through ordinary play. Which skills could be used
was constrained: milestone flags are stored by array index, so entries
must be appended with ascending levels, and section 3 had already put
level-50 milestones on Walking, Meditation, Foraging, Patience, Fighting
and Sleeping. Meditation was the obvious home for Circulate Qi and is one
of the blocked ones; Temperance had no milestones and fits better anyway.

A milestone rather than a tick watching for the condition, because the
requirement then shows in the skill panel like any other perk and the
load ordering is already solved -- milestones re-fire before the action
list is rebuilt from the save, so a replayed grant is overwritten a
moment later and a save from before the unlock comes back without it.

tests/audit.mjs needed teaching about this: snap() did not track acts, so
a perk granting only an action counted as doing nothing. With acts.length
added it also surfaces a base-game text gap -- walk lv1 says "AGL +1" and
quietly grants Run too.

Second, a settings checkbox: "Unrestricted actions". Several sustained
actions at once, started anywhere -- in a fight, indoors, in the dark.
Off by default, since both are deliberate limits.

The game enforces one-at-a-time twice: activateAct deactivates whatever
was running, and every activate() overwrites the single shared
timers.actm. Nothing is reimplemented -- each action's own
activate/deactivate is wrapped and the timer SLOT is swapped around the
call, so the action's own clearInterval/setInterval end up operating on
its own interval. That works unmodified for the base game's Run and
Investigate as well as the mod's four. activateAct treats a click on an
already-running action as "stop it", since the click handler can
otherwise only toggle off the most recent one. Unchecking stops
everything, because the game only understands one.

Guarded: use() is wrapped so a tick running somewhere it was never meant
to cannot throw every second forever (five failures stops it), and load()
clears the per-action intervals, since it resets every active flag.

New tests actionlock.mjs and freeactions.mjs, 44 checks between them.
Also verified by hand in a browser. Full suite green, 15 scripts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Section 19 made level 110 reachable. Nothing was waiting up there.
Measured across all 94 skills: 25 had no milestones at all, 12 had
nothing past level 10, 44 had nothing past 50, and of the 13 with
anything higher only 10 went past 60. So 81 of 94 skills gave nothing
whatsoever between level 51 and 110 -- the entire top half of a ladder
the seventh pass had just spent its effort making real.

One rule now: every skill gets the rungs it does not already have, from
10/25/50/60/75/90/110, appended above its current highest milestone.
Uniform, no per-skill list, and always ascending -- which the save format
requires, since "granted" flags are keyed by array index. 447 perks
across 93 skills; Renown is skipped, being driven by titles not levels.
The rungs are the cap ladder's own, so reaching a story cap and pushing a
skill into it pay off together.

What each perk does comes from the skill's type, so a resistance skill
gets resistance and an absorption skill gets elemental defence rather
than everything getting the same stat. Two deliberate avoidances:
skl.<x>.p is never touched, because skill xp multipliers restore AFTER
milestones fire and a new perk raising one would be overwritten on the
load that first granted it; and absorption perks use you.caff rather than
you.res, since section 13 already drives you.res for those skills and
reconciles it through global.flags.mod_aff.

The balance suite then caught two things. Critical damage had to come
down to a quarter: cpwr is shared by 13 combat skills and compounds
across seven rungs, so at full value it went 1.2 -> 13.0 by cap 110, a
crit worth 23x a normal swing landing a fifth of the time. And allareas,
combat and earlybal were still taking a plain sample mean of a
distribution whose tail had just got heavier, which produced three false
failures out of 2,490 with the familiar signature -- die exact, kill
twice its target. They now stratify on the crit roll the way
MOD_meanDamage does, written out in each rather than calling the mod's
copy. savecompat was also asserting a hardcoded seven-element milestone
array; it now checks the invariant that actually matters, that saved
flags restore into the slots they came from.

Separately, the changelog. The game links to it from the version number,
but the handler is window.open('/changelog/changelog.html') -- absolute
from the server root, so it 404s from a file:// URL or any subdirectory,
which is how a local copy usually gets opened. There is a labelled
"changelog" button in the bottom bar now, the path resolves against
document.baseURI, and the version number still works. The mod's own
entries sit at the top of that file under a gold header, above the
author's, which stay untouched below a divider. The file also gained an
explicit white background: it sets no colours of its own, and the
author's palette assumes a white page, so dark-mode browsers rendered
half of it unreadable.

Every change from here on gets an entry there.

New test perkcoverage.mjs. Full suite green, 16 scripts, 932 perks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…em do something

Three problems, all confirmed by measurement first.

Rank meant nothing. `rar` ran 0-5 and was assigned by feel, so the game
handed out rank 3 titles at skill level 8: Runner at Walking 10, Rookie at
Fighting 15, Dissector at Disassembly 8. Eight of them, all early.

There were too few. 120 titles across 93 skills, and 73 of those come from
story events rather than skills, so most skills granted none at all.

And they did almost nothing. Four titles in the entire game carry a
`talent`. The rest are flavour: the selection window sets `you.title`,
which is the name printed beside your level and nothing else.

Rank now runs 1-10 and is derived from the level that earns the title --
1 at lv 1, 3 at 18, 5 at 45, 7 at 70, 9 at 90, 10 at 110. For the 47
titles a milestone grants, that level is read out of the milestone itself
(f.toString() scanned for a giveTitle call), so it stays correct if a
grant moves. The 73 story titles have no level to read, so the author's
0-5 is stretched over the ranks the skill rungs leave free; nothing
story-granted reaches 9 or 10, which is the point.

Every skill grants five titles, at levels 25/50/75/90/110 -- ranks
3/5/7/9/10. 465 new titles, pool 120 -> 585, all ten ranks populated. The
grant is folded into the milestone already at that level, because two
milestones at one level are a duplicate the save cannot tell apart by
index. Names are formulaic and the rank words are nouns: the form is "X
of Y", and the first draft produced "Peerless of Fighting".

Each title raises its own skill's exp rate, 5% at rank 3 to 30% at rank
10. That is skl.<x>.p, which is saved and is restored AFTER milestones
fire, so it is not written from milestone code -- it is reconciled on the
tick against global.flags, the same shape section 13 uses for you.res.
tests/titles.mjs checks it against stacking over thirty ticks and across
a reload, which is the failure that pattern exists to prevent.

Base-game talents were already permanent -- giveTitle fires talent() once
and sets tget, and clicking a title only ever changed the displayed name.
These new bonuses are the opposite by default: only the title you are
WEARING applies, which gives the selection window a reason to exist.
Renown then buys that away -- at renown level N every title of rank N or
lower applies permanently, worn or not, so renown 10 makes the whole
collection passive. Renown's thresholds are rescaled to the larger pool:
level 10 wants 550 titles rather than 100.

Also fixes modActions(), which a bad text substitution had left calling
an undefined variable.

New test titles.mjs. Full suite green, 17 scripts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Section 24 took the pool from 120 titles to 585, which turned the game's
flat picker into a scroll of hundreds of near-identical rows in a 300px
column, most of them superseded by the one below.

Titles from one skill now collapse to a single row showing the best you
hold, with a caret to open the rest:

  > "Champion of Fighting"           (8)
  > "Aegis of Cold Resistance"       (4)
    "Thrasher"
    "Wolf Slayer"

The caret expands without selecting; clicking the row itself wears the
best one. Story titles have no skill to group under and stay as plain
rows below the groups.

The hook is a SECOND listener on dom.d3 rather than a replacement. The
game's picker is built by an anonymous listener there, so it cannot be
removed, and cloning the node -- the trick used for "delete the save" and
the version number -- would drop dom.d3.update and the addDesc tooltip
the game attached to it. Listeners fire in the order they were added, so
the game builds dom.ttlbd and this rebuilds its contents immediately
after, before a frame is drawn.

Grouping needs to know which skill a title belongs to. The 465 generated
ones say so themselves; for the base game's, the milestone that grants
the title is found by scanning f source for a giveTitle call, the same
trick section 24 uses to date them. So Civilian, Trained Civilian,
Fighter and Rookie group under Fighting alongside the generated five
rather than being stranded.

New test titlepicker.mjs, 13 checks. Also verified by hand in a browser:
36 titles collapse to 10 rows, Fighting opens to all 8. Full suite green,
18 scripts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Answering "are there new colours for the titles too": the game already
had them, in the type 5 branch of dscr() -- grey 0, cyan 2, lime 3,
yellow 4, orange 5, purple 6. Two problems, neither of which could
surface until section 24 started using the upper ranks.

Rank 7 THREW. Its branch sets this.dl.style where every other branch sets
this.label.style; this.dl is assigned in the type 6 and 7 branches of the
same function, so in a type 5 call it is undefined and the tooltip dies
half-built -- name placed, description never. The base game topped out at
rank 5, so that branch had never once run. There are now 93 titles at
rank 7.

Ranks 1, 8, 9 and 10 had no branch at all, so 220 titles rendered with no
colour, including every rank 10 in the game.

Rather than patch a switch inside a 200-line function that builds six
tooltip layouts, the title's rank is parked at 1 for the duration of the
call -- a value the switch has no branch for, and so cannot throw on --
and the label is coloured afterwards from a ten-rank table. The author's
five colours stay exactly where they were; the ramp only extends past
where they stopped. The tooltip now also states the rank and the level it
was earned at, which is worth saying now that rank means a level.

The picker rows use the same table, which is the point of having it: at
five titles per skill, rank is what you are reading. That took the colour
slot away from marking which title is worn, so worn is now weight plus a
background tint -- not a glyph, since the caret already owns the one that
reads best and two triangles in one column are unreadable.
tests/titlepicker.mjs caught that collision on the first run.

titles.mjs gains four checks: no rank throws when hovered, every
populated rank renders a colour, the tooltip states the rank, and the ten
ranks are ten distinct colours. Full suite green, 18 scripts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The dojo already has a reward ladder, and the instructor already
promises it continues -- "After every 5 levels you reach, come here and
receive your share!" -- and then it stops at character level 30
(dj1rw1..6, levels 5 to 30). The mod's endgame is around character level
92, so the promise went unkept for the last eighty levels. Sixteen more
rungs finish it, 35 through 110 in fives, sequential like the base six.

The pills had to be rebuilt first. Character exp per level is
4*lvl^3 + lvl^2, which is 5,336,100 for one level at 110, against a
best-in-game pill worth 15,000 -- the ladder would have been handing out
0.3% of a level. Four grades continue the author's own progression at
roughly his ratio: Superior 100,000, Refined 600,000, Sublime 3,500,000,
Transcendent 20,000,000. The last is worth 3.7 levels at 110; the first
is not a fifth of one at 50.

And a third grade of skillbook at +30%, offered as a CHOICE at the 50, 75
and 100 rungs -- the same shape as the one the instructor gives the first
time you clear the dummies, which is the moment the request pointed at.
Named in the author's register (Sword Saint, Nightblade, Headsman,
Dragoon, Earthbreaker, Iron Body) because the grade below is already
called "Bladesman Manual". They write skl.<x>.p the way the base books
do, which coexists with section 24's title bonuses since that reconciler
only ever adds and removes its own tracked delta.

Worth recording what the first draft got wrong: I built a ladder of new
golem trials before reading far enough into chss.t3.sl, and only found
Level Advancement -- and the second grade of skillbook that already
existed -- when a probe printed a lobby screen full of manuals I had not
written. The trials were thrown away. Same lesson as the catacombs and
the dead affinities: read the whole handler before adding a parallel one.

The continuation is its own lobby entry rather than an extension of the
original's screen, because the base rungs are drawn by an anonymous
handler created inside sl() and cannot be appended to. It appears only
once dj1rw6 is set, so the two never show at once.

New test dojo.mjs, 18 checks. Full suite green, 19 scripts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The repository had none. It is a fork of someone else's game, so the
README leads with that: index.html is truezhangwei's work, unchanged
apart from the single script tag, and removing that line reverts
everything.

Beyond the usual (what it changes, how to run it, how to test it) it
carries one thing forward deliberately: dmg_calc is subtractive in both
directions, so a defender's STR is their armour and scaling an enemy's
STR clamps your damage to zero rather than lengthening the fight. That
mistake cost a full rewrite of the enemy model, and it is the first thing
anyone touching combat needs to know.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
Two reports, one root cause each, both the mod's own doing.

## A hundred Fire Masteries

skl.par_10 "Companions Discipline" is 2000 + section 10 (section 11).
skl.mod_fire "Fire Mastery" was 2010 + 0 (section 29). Two numbering schemes,
each sensible alone, that met at exactly one value.

An id is the save's key for a skill, and the loader matches on it in a loop with
no `break`, so one shared id pushes both skills for both saved entries and the
next save writes all of them. Measured: 1, 2, 4, 8, 16 rows per cycle. Seven
save/loads is 128 of each. MOD_KEY_BY_ID is id-keyed too, so the clash had also
quietly taken the Companions parent's level cap.

Only Fire Mastery moves (2010 -> 2016). Water through dark keep 2011-2015, the
ids they were saved under, so nobody loses those levels, and the ambiguous value
goes back to the skill that claimed it first. The six are an explicit table now
rather than arithmetic.

Fixing the id only stops it getting worse — the duplicates are in the save.
MOD_dedupeSkills prunes you.skls by identity on load and redraws the panel: a
damaged 138-row sheet comes back as 12 through a real reload, levels intact.

Scanning every namespace turned up one more of the mod's own: chss.mod_hollow
and chss.mod_pltwr both had location id 976, from MOD 2.9. The tower is 980.

## Getting into the marketplace

Exactly one item in the game sets mkplc1u — the "Pamphlet" — and the Paper Boy
who hands it over is retired the moment it is given rather than read. Section 15
added selling and never marked it a key item, so it could be sold at the food
stand, outside the marketplace, before being read. That takes the marketplace,
its three shops, the guard-duty quest and the realm 2-5 breakthrough pills away
permanently. Reproduced before touching anything: 500 visits, no Paper Boy.

* Key items cannot be sold. Derived, not listed: the game is one inline script,
  so its source is readable at runtime; a flag it never sets back to false is a
  one-way unlock and the item that sets one is the only copy of something. Six
  qualify. A Smoke Bomb sets a flag the game does clear, so it stays sellable —
  which is why the rule is derived rather than "sets a flag".
* The Paper Boy comes back if you no longer have a Pamphlet. That is what gets
  an already-stuck save out without a console; a fresh character still meets him
  exactly once, through the author's own code.
* The village center says WHY the door is missing instead of omitting the line.
  "It seems like I can't" was the report; an invisible gate was the bug. The
  Herbalist inside gets the same treatment.

Two layout constraints found doing it: .chs is height:22px with no overflow
rule, so a wrapping choice spills into the next row (added lines stay under 50
characters, sentence in the tooltip); and chs() appends, so MOD_chsAboveBack
puts the line above the location's "<= Return".

tests/ids.mjs (32 checks) and tests/marketplace.mjs (30). Full suite green:
24 scripts, 2,490 matchups, 974 perks.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`you.rank()` is the base game's Power rank, derived from live stats on every
read. Measured on a character built to each rung of the story ladder, rank 1
IS reachable — but only with every skill at 110. At 108 across the board you
are still rank 3, and all ten ranks live inside the last fifteen skill levels,
after a whole game in which the number is an unreadable ten-digit blob.

Section 34 adds a second way to hold one. The Hall of the First Gate, off the
Old Path trailhead, holds ten named challengers from rank 10 down to rank 1,
fought strictly in order and gated on character level so the ten fights span
the endgame. Winning writes the rank to global.flags and `you.rank` is wrapped
to floor at it — it never caps, so your own stats can still beat it.

Each duel is a protected one-creature area, which MOD_scaleEnemy already reads
as an arena boss, so every rung is anchored to the player's power at spawn.
The per-rung step goes into fight length rather than incoming damage, and the
kill<die margin is re-derived from the targets MOD_scaleEnemy stored rather
than hoped for — scaling both targets in opposite directions loses the
guarantee by a step of 1.16.

tests/ranks.mjs: 31 checks, including all 100 rung x tier matchups through the
real dmg_calc. allareas is now 2,790 matchups across 31 areas. Full suite green
across 25 scripts.

Also records a pre-existing bug found while testing, deliberately NOT fixed
here: hpTrack assigns you.hpmax at the end of the allbuff wrapper, but allbuff
opens with stat_r(), which recomputes it from the base. Sampled 43 times across
six seconds of real play, you.hpmax was 10,050 every time and the hpTrack value
of 184,754 never appeared — so enemy damage is sized for ~18x the health the
character actually has. Written up in MOD-NOTES.md and CLAUDE.md; it moves a
number all 2,790 matchups are measured against, so it needs its own change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Found by auditing, not reported. `giveTitle` pushes each earned title onto two
arrays — `global.titles` and `global.titlese` — and `load()` rebuilds
`global.titles` from the save by index and then appends the whole of `titlese`
on top. Everything earned that session therefore lands in the array twice.
Measured on a character holding 241 titles: 241 before a load, 481 after, 241
distinct. Reproduced in the UI: with seven titles held, "Safehouse" rendered
twice in the picker after one save/load.

It does not compound — the next load finds `titlese` empty — but one load is
enough, and `save()` writes the inflated array straight back out. Both the
game's own title screen and the mod's grouped picker iterate `global.titles`,
so every title got two rows.

A base-game bug, but the mod is what makes it loud: vanilla hands out around a
hundred titles and section 24 grants five per skill. Section 17 already dodged
it once for renown by counting from `ttl`; this repairs it properly.

MOD_dedupeTitles prunes by identity on load and at boot, the same way and for
the same reason as MOD_dedupeSkills — `global.titles` holds references to the
objects in `ttl`, so the same object twice is always wrong whatever put it
there. Deliberately not fixed at the source: `titlese` is the author's
mechanism and the mod has no business changing what it means.

Also hardens MOD_scaleRankDuel with the explicit player / creature.default
guard MOD_scaleEnemy has, rather than relying on neither carrying a spawn
target.

tests/ids.mjs gains 8 checks: distinct before a load, after one, after two, no
holes, the worn title still in the list the picker reads, and an already-doubled
list repaired. Confirmed they fail without the fix (85 entries for 43 distinct).

The rest of the audit found nothing to change: all 75 locations draw, no choice
row wraps its 22px line, all 12 wiki pages build with no horizontal overflow at
375/768/1200px, no id clashes, no interval leaks over 15s at 20x, and acts /
you.skls / furn / qsts stay distinct across repeated save/load cycles.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The picker groups by skill. Measured across the whole pool: 595 titles, 512
attributed to a skill, and 83 in a flat list at the bottom — which is the wall
of rows the grouping existed to remove, just smaller. Reading them, almost none
were really one-offs.

Two more attributions, both derived rather than typed out:

- MOD_FLAGSHIP already pairs a skill with the title it grants, but those
  milestones are built from the table so the closure holds a variable and the
  existing source scan finds no literal to match. Reading the table attributes
  10 (Executioner, Deathless, Sage...).
- Around thirty of the author's titles are keyed <skill><n> — tghs1/2/3 under
  Toughness, srd3/srd4 under Swordsmanship, dth4 under Death, rtr1 under
  Retreating. Most are granted by nothing at all, so no milestone scan can see
  them; they are unfinished tiers like area.clg, and they group correctly
  whether or not they are ever earned. That attributes 9 more.

The trailing digit in that rule is load-bearing. ttl.thr is "Thrasher", for
smashing the dojo's equipment; its bare stem is skl.thr, Throwing. Only the
numbered convention is safe to read this way, and an ambiguous prefix is left
alone rather than guessed at. tests/titlepicker.mjs asserts both directions.

The remaining 40 are ladders of their own — the ten cultivation realms, five
for creatures killed, four each for items gathered, titles collected and punch
power, three each for time indoors, shields and jobs. They share a key stem the
same way the numbered tiers do, so membership is derived; MOD_TITLE_FAMILY
holds only a label per family (eleven of them), with the lowest-ranked member's
name as the fallback. A ladder's best title does not say what the ladder is —
"Nameless (4)" could be punch power or titles collected — so the label sits on
the row, not only in the caret's tooltip.

595 titles held now renders as 126 collapsed rows, and the flat tail is down
from 83 to 23 genuine story one-offs.

The wiki's title page had the same problem solved worse (everything skill-less
under "From the story", split into rank buckets) and now takes the same
families, nested under one "Ladders of their own" group — eleven more top-level
summaries cost ~530px on the page wiki.mjs already caps at 6,000.

Also found five titles the author never finished: ttl.ddcd carries the literal
string "null" as both name and description, and shpt2/shpt3/mone3 are blank.
Nothing grants any of them. The picker skips them rather than drawing a blank
row; the wiki lists and tags them "unfinished", the same call area.clg got.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The question was whether anything above one star can be crafted. Measured
across all 62 recipes, twice — by the rarity of what they produce, and by
whether any giveRcp call anywhere can actually teach them:

    stars  recipes  reachable
      1★     43        35
      2★     17         7
      3★      1         0     <- rcp.trr, Trinity
      4★      1         1     <- rcp.clrpin, Clover Pin
      5★      0         0

So the literal answer is yes, narrowly. But the 3-star tier is one recipe
nothing can teach, 5-star is empty, and the single 4-star result is a Clover
Pin you get for holding seven clovers. Crafting effectively stops at two.

Section 35 adds four rungs, 2★ to 5★, each a full set (weapon, body armour,
shield, accessory, tonic) — twenty craftable things from twelve materials along
three lines, gathered at four nodes behind one door on the Village Center.
Each rung consumes the rung below it. The Gather action comes from the location
rather than the action, so one action serves all four nodes, and it trains
Mining, Geology and Foraging, three skills the base game defines and barely
uses. The first material out of a seam teaches that rung's blueprints, the way
the base game teaches rcp.wfar from three wolf fangs.

Three things this had to get right:

- Ids are blocked by namespace and load() depends on it — a saved id resolves
  through itemgroup[(id+1)/10000<<0]. Items 9200+, weapons 10101+, armour
  20101+, shields 30101+, accessories 40101+.
- rar cannot exceed 6: equip() does global.text.wecs[w.rar][0] and that table
  has seven entries, so a 7-star anything throws when worn.
- The value model needed re-indexing. Section 15 builds MOD_VAL.madeBy in an
  IIFE at load, so new gear fell past rule 2 (price from inputs) to rule 3
  (price from stats): a 5★ sword priced at 1,161 while each of the eight ingots
  it eats priced at 13,000, and selling the ore paid ninety times better than
  using it. Materials are anchored (6/18/54/162) rather than priced on the
  equipment rarity scale, sized so gathering pays ~6 coin/s sold against ~1
  from endgame combat, and the gather rate came down from 55% a tick to 14%.

Also reports a base-game finding: 19 of the 62 recipes have no giveRcp
anywhere — defined, priced, complete, unteachable. Listed in the new wiki page
rather than wired up, like area.clg and the unfinished titles.

tests/crafting.mjs: 27 checks including both traps above. Full suite green
across 26 scripts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Two things that break quietly, neither of which errored.

NAMES. Ids are the save's key and tests/ids.mjs guards them; names are the
player's key and nothing guarded those. Section 4 added skl.frg named
"Foraging" when the base game already had skl.hvt under exactly that name. Two
identical rows in the skill panel — and ten identically named titles, because
section 24 derives a title's name from its skill's, so mod_t_hvt_25 and
mod_t_frg_25 both came out "Gleaner of Foraging", five rungs each.

The mod's is Wildcraft now. Key and id untouched, so nobody loses a level:
names are not saved, ids are. "Wildcraft" rather than something invented — the
author's register is plain single words (Foraging, Harvesting, Topography,
Elusion, Temperance) and this skill reads the wild rather than harvesting it.

Swept the rest: all 140 things the mod names are unique, including across the
whole inventory bag, since item/wpn/eqp/sld/acc share one list. The author's
own repeats are reported and left alone — Chashu Ramen twice, Bandage on both
an item and an armour, two Blue Slimes, two Namelesses, nine Training Grounds.

THE HUNTER'S QUEST. quest.hnt1 wants ten Raw Meat held at once, and Raw Meat
rots: planner.chkrot runs once an in-game day, divided by a season modifier
that makes summer twice as fast, and destroys amount*randf(.1,.2)+1 when it
fires. A perishable stock converges rather than accumulating, so there is a
ceiling and the quest needs it above ten.

The mod never touched the drop table. It changed how long a rabbit takes to
kill: MOD_ENEMY.kill targets eight landed swings, and at the point the quest is
offered a Wild Rabbit has 3,574 HP against the base game's 93, so one that died
in a single hit now takes ten. Ten times less meat an hour against an unchanged
rot clock. Measured at the old 6%, taking a conservative fifteen seconds an
end-to-end kill: steady state 15.6 normal, 51 winter, and 4.3 in summer against
the ten required. Completable in three seasons and impossible in the fourth is
a bug, and it is ours.

Raised to 18%, solved backwards from the requirement (a steady state of 25 in
the worst season and the first hunting area) rather than picked, and applied to
every creature carrying rwmt1 — found by scanning the drop tables, not by
naming the rabbit and the wolf. Worst case is now 26 against 10. Not an income
either: a kill in the first hunting area yields 0.28 coin of meat.

tests/names.mjs covers both. Verified it fails without either fix — the name
half reports the skill clash and all five title collisions, the meat half a
summer steady state of 4 against 10.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An item id is two schemes at once and the mod only knew about one. The
namespace block it knew: load() resolves a saved id with
itemgroup[(id+1)/10000<<0], so an item has to sit under 10000. The other is the
CLASS, which dscr type 1 reads straight off the number with no field for it:

    id < 3000          Food           + a "Tried:" footer
    3000 <= id < 5000  Medicine/Tool
    5000 <= id < 9000  Material/Misc
    id >= 9000         Book           + a "Read:" footer

stype is not a proxy — the author's stype 4 spans Food, Medicine and Book.
Everything the mod added was parked at 9100+ to clear his maxima, so 26 of its
32 items read as Books with a "Read: Never" line under them: the twelve
crafting materials, the four tonics, the ten breakthrough pills and the four
spirit pills. Only the six skillbook manuals were right, by accident.

Moved: tonics 4201-4204, breakthrough pills 4210-4219, spirit pills 4220-4223,
materials 5200-5211, manuals where they were. MOD_ITEM_IDS now holds the ranges
with the class table beside them, so the next addition picks from it rather
than from whatever is free above the author's highest.

Moving them was not free. The inventory saves {id, am, data} and restores by
matching id, so an entry whose id no longer exists is dropped without a word —
and a Tribulation Pill is seven million coin at the Pill Tower. Section 37
reads the blob before the game's load runs (that is what clears and rebuilds
inv), notes what the moved ids were carrying, lets the original load run
untouched, and gives those stacks back afterwards under the new ids. The save
is never rewritten, and anything unexpected in it makes the migration do
nothing — a migration that fails is better than a load that fails.

tests/ids.mjs gains 7 checks: the class of every added item, derived from the
mod's own tables rather than from the ids; that nothing which is not a book
gets the "Read:" footer; and a round trip through a save deliberately rewritten
back to 3.6's ids, which must come back with the same amounts and must not
double-grant on the next save. Verified they fail before the fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Tooling only — no change to mod.js, index.html, or any save.

Every balance figure in this suite is a sample mean of something heavy-tailed:
crit rate reaches 33% and a crit is ~8x a normal swing. The scripts reported
point estimates — fightsmoke medians nine duels, allareas prints one number per
matchup — with nothing to say how much of a gap between measurement and target
was real.

tests/lib/stats.mjs is the first shared code in tests/. Confidence intervals on
Student's t rather than the normal approximation, because fightsmoke fights
nine times per area and z at n=9 is ~15% too narrow — the one direction that
turns noise into a confident wrong answer. Between table rows the critical
value takes the lower-df (wider) row, never the narrower. Plus withinBand(),
which asks the question that usually matters — is this practically on target —
and fails two ways, by real drift or by an interval too wide to conclude
anything, saying which.

fightsmoke now prints a CI beside the median, and that was immediately useful:
The Long Vigil's median of 2 swings carries an interval of [0.46, 4.87], ±83%.
That number was being read as a measurement and it is noise. Flagged in place,
not failed — nine fights in a heavy-tailed distribution are allowed to be
imprecise.

tests/targets.mjs is new and answers a question nothing else here asks. Every
other balance script checks a RELATION — kill < die, whiff 0, nothing
unwinnable — and none checks whether the model delivers its own dials, which a
single matchup cannot tell you. It samples 45 spawns an area and compares
against the target THAT SPAWN was solved for (_modKillT/_modDieT), not the flat
dial. Result: every area tracks its per-spawn kill and die targets to within
1%, and kill < die holds 45/45 in all four areas sampled. Verified it fails —
a 15% drift injected into _modDmg is caught at 0.869 of target.

Four corrections along the way, every one to the new test and none to the mod:
comparing against the flat dial rather than the per-spawn target (killT = kill
* band^hpSpread * creatureShape, so a basement rat is legitimately a five-swing
kill); omitting the hit rate, which pinned every die ratio at exactly 0.45 —
MOD_ENEMY.hit, the tell; a plain sample mean instead of the crit stratification
CLAUDE.md already demands, which put two of forty-five rank-duel spawns the
wrong side of kill < die on noise alone; and testing significance rather than
equivalence, which at ±0.1% precision fails on a 1% quantisation bias that
means nothing in play.

No third-party skill was vendored for this. The whole computation is
mean ± t·sd/√n, and adding a Python dependency to a repo with 0 Python files
and 27 Node scripts to do arithmetic the test scripts can do inline would be a
dependency, not an improvement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… given

hpTrack (sections 9/10) raised max HP with offence by ASSIGNING you.hpmax at
the end of the allbuff wrapper. The assignment did not survive: stat_r
recomputes hpmax as (hp_r + hpa) * hpm * hpe, and the tick, fght, update_d and
allbuff itself all call it constantly. Measured on a cap-60 character, a single
stat_r() took max HP from 1,492,947 to 72,377, and sampling every 137ms across
six seconds of real play found the low number in all 43 samples. The HP bar
agreed: hp: 2,438/10,050.

MOD_scaleEnemy reads you.hpmax in the instant after allbuff, while the
assignment still stands, and sizes _modDmg against it. So every fight was built
for a character with ~18x the health the character actually had — harmless
early, worst at the cap — and all 2,790 allareas matchups were validating
against a number the player never held.

It writes you.hpm now, which IS an input to stat_r and therefore survives. Max
HP holds through 200 consecutive stat_r calls.

Three things needed a correction each, all caught by measurement:

- hpm is SAVED (hpm:you.hpm in the save object), so the mod's factor is tracked
  in global.flags.mod_hpm and divided back out before re-applying — the
  MOD_applyMoneyDrops pairing, and the trap you.res once fell into. Verified
  across six save/load cycles and 20,000 allbuff calls: zero drift.
- The recovered base needs flooring at 1. freshYou() resets you.hpm without
  clearing the flag, so the division recovered 1/18.86 and max HP collapsed to
  12; allareas caught it at once with six rank duels flipped to kill > die.
  Flooring is an invariant, not a patch — only milestones touch hpm, and they
  only add.
- Math.ceil, not Math.round: stat_r ceils, and one unit apart is enough for the
  durability check to fail.

stat_r() is deliberately not called from inside the wrapper — the mod's skill
use() calls add straight into you.str, and stat_r would recompute those from
(r + a) * m * e and wipe them.

tests/targets.mjs gains 5 checks, verified to fail against the old behaviour
(1,492,947 -> 72,377 on one stat_r). All 2,790 allareas matchups still pass:
they were measuring the right number all along, it was play that had the wrong
one.

Also adds -c-1 to tests/run.sh. Without it http-server sends max-age=3600 and a
browser can be handed a mod.js from before the edit under test — which surfaced
during this work as an allareas failure that would not reproduce standalone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sourced from Wuxiaworld's "General Glossary of Terms in Wuxia, Xianxia &
Xuanhuan Novels" -- the genre's own reference work -- and from the book
structure of two canonical long works there. Its sentence on bottlenecks
was the brief: cultivators "may require new Insights, the aid of
medicinal pills, or even harsher training" to break through. The mod
shipped with one of those three.

Section 38 adds the rest:

  Spiritual Root (灵根)       five grades by purity, rolled once at the
                             dojo, never a dead end -- the worst costs
                             cultivation speed, not access
  Nine layers (层)            derived from Qi Circulation between a
                             realm's threshold and the next, so a
                             breakthrough lands you at layer 1 on its
                             own. "Half a step to Nascent Soul" is the
                             genre's name for what this mod called a
                             bottleneck
  Insight (参悟)              the half of the price that cannot be
                             bought, from the three sources the glossary
                             names and no fourth of ours
  Closed Door Training (闭关)  a fifth action that refuses to start unless
                             you are at a wall. Consolidates, and
                             shelters: breaking through from inside it
                             cannot deviate you
  Qi Deviation (走火入魔)      what failing in the open costs now -- half
                             an in-game day unstable, never a death
  Heavenly Tribulation (天劫)  realm 10 was named Tribulation
                             Transcendence with no tribulation in it
  Impurities (杂质)            the genre's signature note at the moment

Fixed on the way: a realm's body multiplier did nothing at all. Section
28 assigned you.hpmax and you.satmax -- the same mistake v3.9 fixed for
hpTrack, still sitting in the realm code -- and stat_r recomputes both
from their inputs. Measured at realm 10: hpmax 332 after allbuff and 39
after one stat_r, satmax 1700 then 200.

Nothing writes hpm or satm now. Two delta-trackers on one field cannot
coexist, since each recovers its base by dividing out its own factor and
finds the other's inside it. Contributors publish a factor to MOD_BODY
and one idempotent applier writes the product, one flag per axis.
Verified: survives stat_r, zero drift over 200 calls and 5,000 allbuff
calls, Mortal untouched.

tests/actionlock.mjs counted four actions by literal and failed on a
legitimate fifth; the count is derived now. Full suite green, including
all 2,790 allareas matchups.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Every skill gets a rarity, ten of them from Novice to Transcendent, and
the skill list paints each row in its colour. It is the rank of the
level you have taken the skill to -- MOD_rankForLevel against
MOD_RANK_AT, the same thresholds and colours titles use. Derived, not
assigned: a per-skill rarity would be a hundred numbers to keep in step
with a ladder that moves.

Jade Slips (玉简, from the same Wuxiaworld glossary section 38 used) fold
a skill into its section. Fold every skill in a section and it is one
line on the sheet instead of up to twenty-five. Sold at the Slip
Archive on the dojo lobby, at a price that climbs with use.

Folding HIDES a skill; it never removes it from you.skls. That is the
design, not a detail. Shortening you.skls breaks three things at once:

  * the game's panel only redraws when the list grows, and the size it
    compares against is captured once and never updated, so a removed
    row stays on screen forever;
  * giveSkExp pushes a skill back onto the list at its next level-up,
    banner and all, so a fold would undo itself;
  * you.skls is saved by id.

It works because you.skls was only ever the display list: the mod's
allbuff wrappers walk skl by key, and most base-game skills pay out
through milestones already written into stats. So a folded skill keeps
every level, perk and buff. Measured: every stat after folding all
hundred is exactly its unfolded value times the convergence bonus.

A converged section is worth x1.03 on every stat, compounding to x1.34
with all ten; a partial one pays a share in proportion.

The ten converged skills are created unconditionally at load, at the end
of skl, because exp and p are saved positionally over it. They are
aggregates -- no milestones, never trained -- so they take no titles and
perkcoverage skips them the way it already skips Renown.

Also a Skill handbook page in the wiki, beside the skills page.

tests/convergence.mjs, 43 checks. Full suite green, including all 2,790
allareas matchups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Xeus24 and others added 14 commits September 26, 2026 18:33
v4.2 -- brought back in line with the original, measured.

tests/vanilla.mjs loads the game twice, once with mod.js blocked (the
author's game exactly), and runs the same code on both.

  * Skills below level 10 now cost exactly what the original charges --
    checked against HIS expnext, not a reimplementation. The old curve was
    364x faster at level 10, inside the range he designed (all 22 of his
    perk ladders end between 1 and 15). From 10 it rises ~3%/level at a
    solved ratio, so 110 is exactly as far away as before.
  * Skill xp 1x and enemy coin drops off by default -- the original's
    values. Both settings boxes still work and a saved value still wins.
  * Fixed: the six masteries were stuck on the original's curve (level
    20 cost 7,474,050 instead of 375) because the curve was installed
    before they existed. It is installed again at the end of mod.js.
  * Fixed: a damage sample with no crit fell back to a plain mean that
    DROPS the crits (30% of damage at cap 30). ~5% of spawns came out
    ~20% weak, and the tests read fights 40% long 0.15% of the time,
    which is what intermittently failed the rank duels in allareas.
    Empty strata are topped up now, in the mod and all five test copies.
  * Fights are the one axis deliberately not matched: the original's
    early fights take ~1 swing because you out-level fixed creatures.
    The test prints the gap instead of asserting a parity.
  * Number formats: short / sci / myriad / as the original. Up to 9,999
    every mode prints what the original prints -- his own threshold, from
    a printDamageNumber that called formatw and discarded the result.

v4.3 -- small fixes.

  * The realm sits on the rank line and is the breakthrough button at a
    bottleneck. The panel is a full 310px box, so no new line, and ASCII
    only: a triangle and a warning sign each grew it, measured.
  * A "What next" wiki page reading the save, built on each system's own
    gate so it cannot say a locked door is open.
  * Rarity named in the skill tooltip; sort by rarity; a rarity filter.
  * The skill panel rebuilt every row every second forever once the list
    grew (the game's sklsize is never updated). Replaced its updater.
  * The Damp cellar had no spawn weights, no entrance and an exit into
    screens never written. Notice 23html#1 on the Message Board now.
  * Four unfinished titles finished from the author's own intent; ddcd
    left alone, nothing says what it was for.
  * Rank-duel margin 1.25 -> 1.4; allareas gains a fully merged build
    (3,720 matchups); three dead allbuff wrappers removed.

Full suite green, including tests/vanilla.mjs and tests/polish.mjs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Documentation, mostly. MOD-NOTES.md opens with an overview of the mod
as it stands at 4.3 and gains the missing chapters for 4.0-4.3; older
passages that later versions changed carry a dated note rather than
being rewritten. README.md and tests/README.md describe 43 sections, 31
test scripts and 3,720 matchups. Two errors corrected on the way: the
top realm is Tribulation Transcendence, not "Ascendant", and there are
ten realms above Mortal, not seven.

CLAUDE.md: the author's changelog is now checked against upstream/main
(origin is the fork since the remotes were renamed), plus current
action, id-range, matchup and line counts, and the dead wrappers it
said were left in place are gone since 4.3.

modHelp() listed six commands from the first version and said skill
exp resets on reload. It lists all fifty-four now, grouped by purpose,
and tests/polish.mjs reads every mod-/set-/get-/reset- function out of
mod.js and fails if one is missing from the help.

The comments in sections 38 and 39, and two lines of the 4.0 changelog
entry, quoted Wuxiaworld's glossary at length. They paraphrase and cite
it instead. No behaviour changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
v4.4.

Section 0: the first time a new version of the mod loads, the live save
and all three slots are copied into p23_backup_1..3 before the game has
read anything. It is the first code in mod.js and depends on nothing
below it, because an update that breaks is exactly when the backup is
wanted, and a section that throws stops every line after it while the
game goes on loading and autosaving. Three kept, oldest replaced first
by sequence number (the clock ties within a millisecond).

Section 44: the saves panel lists the backups, restores one, or backs
up on demand. Restoring puts every slot back, backs up the present
first so it can be undone, and reloads. It restores saves, not code.

tests/backups.mjs caught a real bug before it shipped: with no room, the
first version cleared every older backup, retried, and lost them anyway
when the retry failed. It now frees only the one it is replacing.

Section 45: a Pacing setting. Original (the default since 4.2) or Mod
before 4.2 -- 50 x 1.106^level exactly, via a new MOD_XP.fixedRatio
that skips the solve, plus 2x exp and a 15% coin drop. Level 110 is the
same distance away in both, and fights are unchanged. The preset is
stored and its curve re-applied at load; changing a box reads Custom.
tests/pacing.mjs checks every level 0-109 against the old curve.

Docs, changelog and CLAUDE.md updated. Full suite green, 33 scripts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
….5, v4.6)

v4.5
- Three bugs in the author's code fixed: Death took more energy as it
  levelled (negative past 10), Shield Mastery could add defence to damage
  taken, and luck never reached the crit roll (now capped at 50% from luck)
- Breakthrough history; daily backups and backups as downloadable files;
  shop prices follow the number format
- Tests for the cultivation road and the three fixes, and a committed
  balance baseline (tests/baselines/balance.json)

v4.6
- Fights setting (section 47): Original, the default, leaves the author's
  areas exactly as he wrote them (all 70 spawns match his page) with his
  6% Raw Meat; Scaled is the mod's model everywhere. The mod's own areas
  are always scaled
- Realm walls sized to the climb: insight, seclusion and deviation scale
  with the Circulate Qi time to each realm; meditation earns insight only
  at a wall
- Pill Tower realm 8-10 pills repriced for no coin drop; tests/econ.mjs
  holds every required price to 10% of the climb it gates
- Userscript to run the mod on the hosted game (tools/, userscript/)
- Safari: skill rows no longer wrap under a classic scrollbar, dropdowns
  match the panel, -webkit-user-select; skill panel writes only what changed
- Realm tooltip showed source code instead of text
- Tooling: tests on GitHub (Chromium and WebKit), BROWSER=webkit, parallel
  run.sh with timings, tests/authorfiles.sh

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…v4.7)

- Qi Settling Pill costs the breakthrough pill of the realm at the wall
  (never under 5,200), in the listing and in rows already stocked
- The rank line shows the wall's insight, have/need
- The mod's settings are written into the save and adopted by a browser
  that has none of its own; a browser's own settings are never overwritten
- The log says when the mod was updated, and where things are on a first
  install; Safari is reminded to download a backup; the userscript says
  in the game when the author's game is an untested version
- No spinner arrows on the mod's number boxes; a real monospace in the
  wiki; the Mod before 4.2 preset labelled legacy
- Wiki says where near-death insight happens on Original fights
- Tests: tooltips.mjs renders every tooltip and fails on code or missing
  values; small.mjs covers section 48; econ.mjs prices the optional pills;
  fights.mjs measures the brink; tests wait two game ticks instead of 4.5s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…in CI

Settings are adopted from a save once per browser, so a slot switch cannot pull
another slot's snapshot in. tests/docs.mjs checks the counts the docs state.
Firefox joins the GitHub matrix.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Rank 2 and up shows its number after the skill name, drawn by CSS from an
attribute so colour is not the only signal. CI caches the Playwright browsers.
Whole suite verified on Firefox locally.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Pill Tower chances for realms 8/9/10 go to 26/20/15% (mean wait 4.6/6/8 real
hours; realm 10 was ~20). tests/cultivation.mjs holds 8 hours. README explains
serving the fork from GitHub Pages; .nojekyll added.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
It was gitignored, so npm ci, the npm cache and the browser cache key all failed
at the first step of every GitHub run.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
The page list shows the match count per page, dims pages with none, and Enter
jumps to the first page with a match.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ction 49)

Browsers slow a hidden tab's chained setTimeout to about once a minute. The
mod replays up to an hour of missed ticks plus the action's seconds, never
combat. setCatchUp(sec), persisted and carried in the save. tests/catchup.mjs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…llow step

Section 50 hands the author's unteachable dishes over as Cooking levels rise.
tests/screens.mjs draws every mod screen with a capped character and long name;
it found the gate's two locked hints wrapping, now shortened with the sentence
in a tooltip. fights.mjs holds the step into the Hollow winnable with margin.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
All 48 creatures held the one shared eqp.dummy, onto which the author wrote 32
per-creature affinity lines, and the player's fists (the same object) leaked
their level into every creature's damage. Each creature now owns its gear.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…l once

The dojo pays those pills at character levels, the walls are Circulate Qi
levels; the two clocks differ. Also adds the missing test README rows.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant