UPGRADING

No pity. No failstacks. No protection. Every attempt is a pure roll. Failure costs currency only — items are never destroyed or downgraded.

Grading up (Worn → Void)

One shared ladder for gear (paid in Scrap) and enhancements (paid in Dust / Essence / Ink). Same success rates, different currencies. Attempt cost scales with the item's tier (2026-07-31): the T10 column is the hour-priced ladder unchanged, and lower tiers are discounted on the measured income curve — Cost(rung, T) = max(1, round(base × income(T) / income(10))) — so the time price of a rung is the same at every tier. Measured income at 30 battles/h (dismantle-everything basis): Scrap 16.7 → 201/h and Dust/Essence/Ink 1.32 → 19.71/h from T1 → T10 (×12.0 / ×14.9). All 360 attempt rows (4 categories × 9 rungs × 10 tiers) are authored in codex/grading.csv, generated by tools/gen_grading.py in dfmc-client; soul bundles in codex/soul_grading.csv (souls have no tier axis and are untouched).

Gear (Scrap per attempt)

RungSuccessT1T2T3T4T5T6T7T8T9T10
→Common100%2222222222
→Uncommon90%2222222334
→Rare70%22233466710
→Epic50%245891117192130
→Legendary35%571016192234384360
→Abyssal20%121722374451808899140
→Divine8%152229475766102114128180
→Primordial3%182735587080125139156220
→Void0.5%182735587080125139156220

Enhancements (Dust / Essence / Ink per attempt — identical grid per material)

RungSuccessT1T2T3T4T5T6T7T8T9T10
→Common100%1111111222
→Uncommon90%1111223345
→Rare70%11233467710
→Epic50%13457812131520
→Legendary35%35811131623273040
→Abyssal20%5111621273246536080
→Divine8%11213243546493106119160
→Primordial3%21436486107129186212239320
→Void0.5%4386129171214257371424477640
Target gradeSuccessGear @T10 (Scrap/try)Gear cumulative expected @T10Enh. @T10 (material/try)Enh. cumulative expected @T10
Common100%2222
Uncommon90%4658
Rare70%10211022
Epic50%30812062
Legendary35%6025240176
Abyssal20%14095280576
Divine8%1803,2021602,576
Primordial3%22010,53532013,243
Void0.5%22054,535640141,243
One item Worn→Void at T10: expected ~54,535 Scrap ≈ 16.6h at floor 119 (3,287 Scrap/h at 109 battles/h, python tools/statmodel.py grids). Void gate alone: expected 44,000. No pity anywhere. Costs were tier-flat until 2026-07-31; measured income scaling (×12 Scrap, ×15 materials T1→T10) meant a T1 attempt cost 12–15× the time an endgame attempt did, and grading was the only Scrap sink that ignored tier while crafting (25×T) scaled. The tier grid above holds hours-per-success constant across every tier instead; the odds curve still carries the sink.
One Void enhancement at T10: expected ~141,243 material ≈ 341.5h of Essence at floor 119 (414/h; Dust and Ink ~374/h). The realistic endgame target is Divine (~2,576 material ≈ 6.2h each). Halved 2026-10-23 (C4) from ~282,000.

Soul grading (Worn → Void)

Class souls only — grading applies to souls slotted into the character's eight gear slots; creature-side souls do not grade. A class soul's grade is independent of the source creature's quality grade and is retained through the source line's advancement. All souls start Worn. SoulMult = 1 + (1/9) × (grade# − 1) multiplies the soul’s output (passive magnitude / granted move power) — Worn ×1.00 → Void ×2.00, nine equal steps, rebased 2026-08-05 from ×0.05/step which topped out at ×1.45. Which grade that reads depends on the kind of soul. A CLASS soul (CLS-*) uses its own graded field, upgraded through the ladder below. A CREATURE soul has no such field and never did — it reads the source creature’s quality grade, inherited from the egg. Creatures are not upgraded with resources; they are bred and hatched, so that lever is raised by breeding for a better egg, never bought. Until 2026-08-05 creature souls read the class field, which is unset on them, so every creature soul computed as Worn however good the creature was. LIVE on release since 2026-08-05 (0.32). See Equipment for the combined stage×grade surface. Each attempt costs the fixed four-currency bundle authored per step in codex/soul_grading.csv (Worn → Common: 30 Scrap + 2 Dust + 2 Essence + 2 Ink), and every currency in the bundle is required — there is no substituting one for another, so 5,000 Scrap and no Dust is refused “NOT ENOUGH DUST” (client clsGradeStep(), worker rollBundle()). The split keeps the old income-rate weights (15 Scrap : 1 Dust : 1 Essence : 1 Ink). Same success ladder as gear. Ichor excluded — the renamed Food is battle-earned and belongs to the breeding-and-hatching economy, not the sinks.

Target gradeSuccessSoulMultBundle per try (every currency required)Cumulative expected Scrap / each of Dust, Essence, Ink
Common100%×1.1130 Scrap · 2 Dust · 2 Essence · 2 Ink30 / 2
Uncommon90%×1.2230 Scrap · 2 Dust · 2 Essence · 2 Ink63 / 4
Rare70%×1.3360 Scrap · 4 Dust · 4 Essence · 4 Ink149 / 10
Epic50%×1.4490 Scrap · 6 Dust · 6 Essence · 6 Ink329 / 22
Legendary35%×1.56150 Scrap · 10 Dust · 10 Essence · 10 Ink758 / 51
Abyssal20%×1.67240 Scrap · 16 Dust · 16 Essence · 16 Ink1,958 / 131
Divine8%×1.78360 Scrap · 24 Dust · 24 Essence · 24 Ink6,458 / 431
Primordial3%×1.89540 Scrap · 36 Dust · 36 Essence · 36 Ink24,458 / 1,631
Void0.5%×2.001,200 Scrap · 80 Dust · 80 Essence · 80 Ink264,458 / 17,631
One class soul Worn→Void: expected ~264,458 Scrap plus ~17,631 each of Dust, Essence and Ink (the Void step alone is ~240,000 Scrap, 0.5% per try), drawn from four streams in parallel. It competes with gear grading (Scrap) and enhancement crafting (materials) for the same income — the deliberate terminal sink. Generated by tools/codex_tables.py.
Soul grade is retained through the source line's advancement and while unequipped. Failure consumes the cost; the soul is never destroyed or downgraded. No pity.

Creature quality grade

Inherited from the egg. Multiplies Attack, Defense, Utility, and HP. Never Speed.

WornCommonUncommonRareEpicLegendaryAbyssalDivinePrimordialVoid
×0.84×0.88×0.92×0.96×1.00×1.04×1.08×1.12×1.16×1.20

Wild eggs cap at Legendary as eggs, but the hatch roll's +4 upside means a wild Legendary egg can still produce Abyssal 9% / Divine 1.5% / Primordial 0.5% / Void 0.1% hatchlings — above-Legendary creatures remain effectively a breeding pursuit. Void vs Worn = 43% more stats at identical species, level, and gear.

Grading costs scale with tier (2026-07-31)

Ruling reversal, from a playtest report: the 07-29 tier-flat call let endgame income trivialize low-rung grading and undercut the tier-scaled sinks. Attempt cost now carries a Tier column in codex/grading.csv (360 rows), anchored so the T10 column is the approved hour-priced ladder unchanged and every lower tier pays the same time per rung on the measured income curve. The forge's grade surcharge derives from the same ladder at the recipe's tier, so forging can never become the cheap route at any tier — the invariant holds by construction.

Upgrading equipped gear in place (2026-07-30)

Equipped gear never appears in the bag, so the upgrade panel could not reach it. Clicking an equipment slot now offers ATTEMPT UPGRADE on the currently equipped piece: the same grade ladder, costs and success rolls as a bag attempt, resolved in place. Sockets and the scroll follow the item through the promotion (the socket identity key carries the grade, and the promotion migrates it) — this also fixed a pre-existing bug where a bag item's sockets and scroll were silently orphaned by a successful grade upgrade.

Auto-optimize (2026-07-30)

The target decides, and Overall Power breaks the tie. Optimize For maximises the chosen metric, but on a slot where that metric cannot tell the candidates apart it falls back to overall power instead of leaving the slot alone.

Client: optScore2() and the candidate loop in optBuildPlan(). Probe: tools/probe_optfallback.py — BEFORE 6/6 (asserting the defence slots are skipped under ATK), AFTER 7/7.

The equipment header carries AUTO-OPTIMIZE: it proposes the strongest legal loadout from what the player owns — gear slots and tags, plus a global re-assignment of gems and runes drawn from the bag and from everything currently socketed on equipped pieces. The displayed POWER number is the single metric; slots that could host several stats resolve to whichever combination of piece and stones scores highest. Hard rules: stat-match (a stone only enters a host of its own base stat), class-locked weapons are never proposed, creatures, souls and scrolls are never touched, stones on unequipped inventory gear are not cannibalized, and ties keep the current piece so a second run proposes nothing. Nothing changes without the preview's confirmation; the preview states the exact inventory delta and is blocked by the inventory guard if the bag cannot absorb it.

Where the roll happens

The server rolls the attempt, not the client. An upgrade is chance-based, so it is cheatable anywhere else. POST /api/roll with kind:'grade' (or 'soul_grade'), the item's category, its from and to grade, its tier, and an idem key of the client's choosing. The server matches the authored row in grading.csv, draws against its Success_Pct with a secret-keyed HMAC, records the result, and answers {success, pct, cost, on_failure}. The client displays that answer; it does not decide it.

Re-sending the same idem replays the same outcome instead of rolling again, so a dropped connection or an impatient second tap can never re-roll an attempt or charge for it twice. A step that is not in the CSV is refused outright, so the ladder cannot be shortened from the client. Currency is still spent client-side and validated by the save door until inventory moves server-side.

One click is one attempt (Tower rulings form card T9, D 2026-10-18; LIVE on release since 2026-10-23 (0.89)). A plain ATTEMPT is one keyed draw on the server and is charged one bundle - the price the button shows. From 2026-09-28 to 2026-10-22 it was not: B2 7.5 sent batch10:true and the worker drew up to ten attempts in the one call, stopping at the first success and charging every draw (“USED k/10”). D: “Back to 1 try per click. This makes no sense. One click is one try unless you use all in.” The flag, the client’s reading of it and the worker’s rb.batch10 branch are gone; a grade or soul-grade request without all rides the single-lane branch of /api/roll whatever flags it carries. ALL-IN is the same door with the bound set by the wallet, unchanged. Client: upgAsk() / upgResolve().

Where “up to ten per click” came from, and what it is not (Rulings board card attempt10, 2026-10-01). There is no D ruling that a click spends up to ten attempts. The source is backlog item 7.5 (“deterministic upgrade odds until success”), a server write-cost idea D kept on the 2026-09-01 board; the server branch shipped dark on 2026-09-28 and the client was wired the same day on Jeff’s order. The button still reads ATTEMPT · one attempt’s price, while a losing click is charged up to ten times that. There is no pity system: no counter changes the odds, every draw rolls the authored Success_Pct. The forge’s grey “N failed in a row” line is display only; it counts failed clicks (a ten-draw loss adds one) and resets when the bench is reopened. D answered on the Tower rulings form (card T9, 2026-10-18): back to one try per click; the ten-draw wiring is gone on both ends (changelog 2026-10-22).

The refresh reroll, and why the idem key did not stop it (reports #119 + #125)

Status: on release. Two players found this independently and one used it to put his entire loadout on Primordial. Fail a grade, press F5 before the game next saves, and the materials are back and the next click is a brand new chance — in the first reporter's words, “posso fazer isso ate os upgrades darem certo”. It worked on equipment and on class souls.

The paragraph above contains the bug, in the phrase “of the client's choosing”. Every property this door promised was true of a retry and none of them was true of a second attempt:

The fix is the pattern this file already documents twice. The creature sex roll is keyed on the creature and the egg roll on the pen and its egg number, both with the note that a fresh key is right only for “an attempt that has cost nothing yet”. A grade attempt costs something the moment the server answers, so it now belongs to the same family:

  1. the draw is keyed on the target item plus an attempt ordinal the server holds, not on anything the client mints. The client sends only which item it means (upgTargetId(), which names the instance — two copies of one item are two separate attempts);
  2. the ordinal advances only once a save has landed since the answer. Until then the same attempt is replayed, with unsettled:true. A refresh therefore returns the same failure instead of buying a new chance;
  3. the attempt is recorded by the server at the moment it answers, so it exists even if the browser never saves — which is precisely what #125's reporter asked for in his own words (“save the character right after the upgrade result”);
  4. and the client now flushes on failure as well as on success, so an honest second attempt always has a settled save behind it.

What counts as the spend landing is the save version moving past the one current when the roll was answered. The bundle is debited in the same tick the outcome is applied, so any save written after the answer carries it; the version is the cheapest honest evidence available and costs no extra write. The existing idempotence is untouched — a genuine reconnect resending the same key still gets the same answer and is still never charged twice.

Additive on purpose. A client that sends no item id falls through to the old client-keyed behaviour, because one backend serves both branches and release must keep working. The hole closes for an account the moment its client sends the field, which is at the 0.48 release. D RULED 2026-08-12: the items and board standing already obtained this way are LEFT ALONE. Fixing the door does not un-grade them and they are not being rolled back.

Client: upgTargetId(), upgAsk(), upgResolve() in play/app.js. Worker: the /api/roll handler and the roll_att table in src/worker.js. Probe: tools/probe_rollreplay.mjs 15/15, and 11/15 against the build the exploit is live on — it drives the real worker and reproduces the reported behaviour (seven attempts, seven different draws, one payment) before fixing it.

DAY-ONE DEFECT OF THIS CHANGE (report #130, 2026-08-13, fixed the same morning). The attempt table above is created only on the grade path, but the retention cleanup at the end of the roll door runs for every roll kind. So on a player object that had never served a grade since 0.48, the first scroll enchant, starter gender, bred-egg or breeding-economics roll recorded its draw and then crashed one line later — the client read the 500 as a network failure ("could not reach the server, the scroll is untouched") and re-asked under a fresh key, so the recorded draw was never collected. Nothing was consumed and nothing corrupted; new accounts could not roll starter genders at all. The table is now created unconditionally beside the roll-record table. Gate: tools/probe_rollfresh.mjs 8/8 (watched fail 4/8 on the unfixed build); tools/probe_rollreplay.mjs re-run 15/15, so the refresh-reroll door itself is unchanged.

SECOND DAY-ONE DEFECT (report #135, 2026-08-13, fixed the same day): the client sold replayed attempts as fresh ones. The server replays an attempt until a save lands after its answer — but the client's post-attempt save was a 10-second debounce, and the client treated any answer with a boolean outcome as a new draw: it debited the bundle again and applied its current step's result. A standing failure therefore read as collapsed odds and eaten Scrap (observed ~4–5% per click against an authored 20%), and a standing success was a free-promotion ladder — measured on the real client: three clicks, one authored 100% draw, an item climbing two grades free. The rule now: an unsettled replay decides nothing and costs nothing — the bench shows “SETTLING THE LAST ATTEMPT — NOTHING WAS SPENT”, holds 1.5 s, and forces the flush immediately; and every settled outcome (win or loss) now forces its flush too, so the replay window closes at network speed instead of staying open for ten seconds. Gate: tools/probe_upgreplay.py 10/10 — the real client against the real worker, with the replay condition forced by blocking the settle save.

The same door on scrolls — enchant joins the attempt ordinal (2026-08-14)

Status: On release. Everything above described the rule as if it covered grading. It did not cover enchant, and a scroll is worth more than a grade attempt: it overwrites a socket permanently and can never be removed, only replaced. The scroll door was still keyed on the client's own idem, minted fresh per click, so a bad enchant, a reload before the flush, and the scroll was back with a different draw — reroll until the enchant is the one you want.

The client half was worse here than grading's ever was. #125 fixed an asymmetry: grading flushed on success but not on failure, so whether the spend stuck depended on an autosave landing. The enchant path flushed on neither outcome. There was no race to win — the reroll window was the full ten-second debounce, every single time.

What the attempt is keyed on, and why it is not host. The obvious candidate was already on the request: host, added for #96 so the server could resolve the enchant's stat from the worn item rather than trust the client's claim. It is unusable as an attempt key, because it is the item's name — an identity, not an instance. Two slots wearing the same base piece, or the same piece on both creatures, would share one attempt, and enchanting one and then the other before a save would replay the first one's draw onto the second. The client sends the socket instead, in the shape upgTargetId() has always used for a socketed target, plus the scroll's own tier and grade:

The client now settles what it commits. Applying a scroll forces a save immediately — on the empty-socket path at the end of the apply, and on the overwrite path when the confirmation is accepted, which is the moment the scroll is actually consumed. Cancelling deliberately does not flush. And an unsettled:true answer now decides nothing and costs nothing on this door exactly as it does on grading: the bench reads “SETTLING THE LAST ATTEMPT — THE SCROLL IS UNTOUCHED”, holds 1.5 s and forces the flush. Without that, adding enchant to the set would have made things worse than leaving it out — the client would have consumed a second scroll to apply a draw that had already been taken, which is #135 on a new door.

egg and sex were deliberately NOT added. Both already derive from stable server-side identities — the egg from its pen and egg number, the gender from the creature key, which ignores the draw key altogether — so a fresh key cannot buy either a new draw. Adding them would regress a door that is already shut.

Client: askEnchant() and the socket picker's onPick / onSockConfirmYes in play/app.js. Worker: RK_TARGETED in the /api/roll handler, src/worker.js — a membership and key-derivation change only; the ordinal machinery itself is untouched. Probes: tools/probe_rollreplay.mjs extended with the scroll cases (live worker), and tools/probe_rollattkey.mjs 12/12, 8/12 against the build the hole is live on — a lift-based twin that runs anywhere node does, so the before/after is reproducible without a sandbox.

What the upgrade panel paints, and in which grade

A successful upgrade replaces the panel's item with the upgraded item, so gradeStep() immediately describes the next attempt — Rare → Epic. Anything painted from st.toColor after that moment is showing the colour of a grade the item has not reached. Measured before the fix: a Rare item drew rgb(163,53,238), which is Epic, and the word RARE was printed in the same purple.

The circle

The alchemical circle is always behind the item, not only after a success — it is the room the work happens in, not a reward for it. It is drawn as a CSS mask rather than an image so it can be tinted by the item's current grade, which means it turns blue the instant a Rare exists. A success adds a short aura ring in the grade just reached.

The aura alternates between two identical keyframes, and that is deliberate. A CSS animation only replays when its element mounts or its animation name changes, and the success gate stays true from one upgrade to the next — so a second upgrade in the same session would have played no burst at all. Alternating the name on each success is the whole mechanism.

Client: upgradeAttempt(), the upg render values in play/app.js; the panel in play/markup.html. Probe: tools/probe_upgcolor.py — BEFORE 4/4 (asserting the border shows the next grade's colour and no circle exists), AFTER 9/9.

A paid attempt is a ticket, and a ticket always settles

LIVE on release since 2026-08-08 (0.42). Reports #58 and #60: the forge takes the materials and then spins forever, showing no result. Only a relog cleared it, and the materials were gone.

The mechanism (as it was in 0.42). upgradeAttempt() debited the bundle synchronously, then held the roll back for 620 ms so the attempt animation could run. Today nothing is spent until the server answers: upgResolve() debits the bundle in the same tick it applies the outcome, so a ticket that never gets an answer has charged nothing. That delay used to be an entry in this._timers — the component’s general cancellable pool. clearTimers() empties that pool, and every battle path calls it: entering a fight, forfeiting, resuming, starting or ending a PvP game, entering or leaving a replay. So a battle event landing inside the 620 ms window destroyed the resolution outright: the wallet was already spent, upgBusy stayed true, and no result was ever produced. Both reporters sent with a battle resolving, which is exactly why it felt random — it needed autoplay running while the player used the forge.

The rule now. A paid attempt is a ticket (_upgPend), and a ticket always settles:

No RNG in the forge. The client forge reads no RNG at all: upgAsk() sends the attempt to POST /api/roll and the server draws it (see Where the roll happens). The interim single seam upgRoll(st) that made that move one method was retired when the rolls went server-side (item 1 of D’s 2026-08-08 server-side-rolls order).

Probe. tools/probe_upghang.py, seeded on reporter #60’s own save — BEFORE 5/12, AFTER 12/12 across three consecutive runs. tools/repro_upghang.py remains the record of the diagnosis. Three of the gate’s own assertions were wrong first and were fixed rather than relaxed, and each failure mode is worth knowing: a same-tick wallet read reported the pre-charge figure, so the broken build passed the double-charge test; a fixed five-currency sum undercounted a bundle that spends outside the list, reporting “bundle 10, charged 3” on a build behaving correctly; and reading the item’s grade to prove a close had settled made the result depend on the roll — it passed on a win and failed on a loss, on the same build. The last is now spied off advEvent('UPGRADE'), which is written on both branches of the roll.

Changelog

2026-10-23 — C17 part 3: the soul-grading table, its note, the T10 prose and the quality row are generated (dfmc-client lane c17-gen-2). WIKI ONLY. Every bundle cell binds to soul_grading.csv; the cumulative column and the “one class soul Worn→Void” note are the running sum of cost / success; SoulMult is src/power.js:188; the quality row is qmOf. The T10 gear figure was 1 Scrap high (54,536 → 54,535).

2026-10-23 — C17 part 2: the T10 cumulative table is generated (dfmc-client lane c17-gen). WIKI ONLY. The two per-tier grids’ Success column reads grading.csv (Gear / Gem) instead of soul_grading.csv; the T10 cumulative table is Σ cost / success over grading.csv (Primordial and Void gear were 1 Scrap high). The soul ladder table keeps soul_grading.csv. tables marked <!-- gen:NAME --> are generated by tools/codex_tables.py from the CSVs with the engine’s own formulas (each generator cites the engine line it ports); its --check is a dfmc-client gate, so a CSV edit without --write fails the gates.

2026-10-23 — C4, the authored grids re-ruled in one stat-model pass (BACKLOG/INDEX.md § 3 Economy; Tower rulings form card T43, D: no issue, the lane re-rules them). Data only: codex/grading.csv, regenerated by tools/gen_grading.py; priced by the new python tools/statmodel.py grids (expected spend = Cost / p per step, farm hours from src/trader.js acquisitionPerBattle at 109 battles/h); gate tools/probe_c4_grids.py 3/9 → 9/9; write-up docs/c4-grids-2026-10.md (dfmc-client). (1) 2-Scrap floor: no gear attempt costs under 2 Scrap at any tier (Worn→Common was 1 Scrap at T1–T9; T1 is now 2/2/2/2/5/12/15/18/18). (2) monotonic ladders: the gear Primordial→Void step was cheaper than Divine→Primordial at every tier (14 after 18 at T1, 170 after 220 at T10) — an authoring slip; its base is now 220, equal to the step below (T10 Worn→Void ~44,500 → ~54,500 Scrap, ~16.6 h at floor 119). (3) socketable ladder halved for Dust, Essence and Ink alike: T10 2/5/10/20/40/80/160/320/640 (was 5…1280); a T10 socketable Worn→Void ~282,500 → ~141,200 expected (~680 → ~340 h of Essence at floor 119). Odds unchanged (100/90/70/50/35/20/8/3/0.5). The rune % curve moved with it — see Stat Tables § 4. Reaches release when the release ritual advances DATA_PIN.

2026-10-22 — the upgrade attempt is the server's: quoted, priced off the ledger row, keyed by the server, settled on the debit (server-authority run § 3 item 6, the reroll / upgrade half; F017, F6-042, F6-043; R5 R6 R8 R9). LIVE on release since 2026-10-23 (0.89). One click on the bench is now two asks on /api/roll. (1) The QUOTE (quote:1) resolves the item's items row and prices the attempt from it - category family, tier and from-grade are the ledger's, the client's cat/from/to/tier are display: a row that disagrees is re-resolved at the edge from the ledger identity and the attempt proceeds at that price, flagged grade_corrected (a T10 Primordial attempt claimed as tier 1 is priced 220 Scrap, not 18); a uid the ledger does not hold stays refused (no_anchor). The quote answers an attempt key the server signs over (account, realm, kind, anchor, price-row id, server time, nonce) - nothing drawn, nothing written. (2) The DRAW must carry that key: a key the server did not sign is attempt_foreign (403), no key attempt_required (400), a key older than 15 minutes attempt_stale (410), a key already drawn on attempt_spent (409) - a reload, a second tab or an edited body never buys a second draw on the same attempt. The draw is keyed on the attempt key, so the client never chooses the key it is drawn on (the pre-run srv|<iid>|n ordinal and the positional fallback are gone). (3) The settle evidence is the debit: the register row (roll_att: key → idem) is written beside the wallet_ledger spend (ref <key>:<cur>) in the same request; the save version is not read anywhere on the door any more. The honest retry is the same-idem replay, unchanged. Scrolls (enchant) ride the same quote → draw pair. The odds and the ladder are untouched. Gate tools/probe_sa_roll_reroll.py 8/24 → 24/24.

2026-10-22 — a socketed stone is a ledger row, and a grade roll is keyed on the stone (Tower rulings form card T19 part 3, D 2026-10-18: “Investigate and fix it this round. We are wiping now.”). LIVE on release since 2026-10-23 (0.89). Three closes on the socket gaps the card named (F6-042, auto sock_claim_bad 20 rows / roll_unanchored 16 rows). (1) The roll door’s grade anchor (roll_prog) now follows the stone’s ledger row (uid:<uid>) whenever the ledger knows the uid; the socket POSITION string (sock:<host>|@gi|ki-j) keys the anchor only for a stone the ledger has no row for, which post-wipe is nobody. The optimiser moving a stone between sockets no longer leaves its anchor behind. (2) Post-wipe (WIPE_EPOCH >= 2, the same gradeFrozen() line the GRADES ruling drew) a grade ask whose suid the items ledger holds no row for is refused 400 no_anchor instead of being grandfathered on the claim; below the wipe it is still answered and flagged, unchanged. (3) Post-wipe the save door refuses (409 sock_unledgered) a save whose sockOv entry has no uid, or a uid the ledger has no live row for or has spent; a held_state mismatch is bookkeeping and stays a watch-only flag at both epochs. Nothing stacks (D 2026-08-01), so every stone comes through a server door with its own uid and every client move carries it; the one client path that did not file its move-in claim - the replace-a-scroll dialog - now does. The 72 uid-less stones on D’s own save are erased by the wipe, not backfilled; a pre-wipe save restored after it is sockmig’s job. Not closed here: F6-043 (“newer save = paid”) - the roll door already books the debit in the wallet ledger the moment it answers (wgPost), and the save’s wallet following that ledger is T19a’s door. Gate tools/probe_t19c.py 9/15 → 15/15 at both epochs.

2026-10-22 — one click is one attempt again (Tower rulings form card T9, D 2026-10-18: “Back to 1 try per click. This makes no sense. One click is one try unless you use all in.”). LIVE on release since 2026-10-23 (0.89). Since B2 7.5 (2026-09-28, Jeff’s write-cost wiring order, never a D ruling) a plain ATTEMPT sent batch10:true and the worker drew up to ten keyed attempts in the one call, stopping at the first success and charging every draw while the button priced one. Both ends are retired: upgAsk sends no batch flag and upgResolve no longer reads a burst on a plain ticket (no server-spend debit, no “USED k/10” tail); the rb.batch10 branch of /api/roll is deleted, so a grade or soul-grade request without all rides the single-lane branch whatever flags it carries — one draw, one attempt’s price, the same attempt ledger. ALL-IN is untouched. The write cost 7.5 was built to avoid (one rolls + roll_att pair per honest click) is what this ruling accepts. Gates: tools/probe_t9.py 3/4 → 4/4 on D’s save (batch10 on the wire → absent, one bundle debited, “THE ATTEMPT FAILS” with no tail); probe_upgodds.mjs, probe_upgodds_client.mjs, probe_upgbench.mjs, probe_reviews_1001.py re-aimed to the one-try rule.

2026-10-01 — review of “ten draws per click” (Rulings board card attempt10, D: “we do not have a pity system at all… review”). WIKI ONLY; no code changed. Checked on dev and release: a plain ATTEMPT does draw up to ten attempts per click (batch10: !tk.all in upgAsk; the rb.batch10 branch of /api/roll, cap 10, stops at the first success) and charges the attempts drawn. Its origin is backlog item 7.5 and Jeff’s 2026-09-28 wiring order, not a D ruling; the note above says so. No pity mechanic exists in the server or the client, and this page never described one. Open for D.

2026-10-01 — the price row follows the item (dfmc-client review F017; Rulings board card grades, row rerolls). LIVE on release since 2026-10-23 (0.89), and it takes effect for post-wipe accounts. The worker picks the grading row from the client's category and tier, and the anchor checked only the from-grade. That let a claim name a cheaper row for the item being upgraded. After the wipe, the ledger owns an item's category and tier (see system-integrity, same date). A grade roll that names a server-held item (suid) and whose row's Category (Gear, Gem, Rune or Scroll) or Tier disagrees with that item's own row is refused as grade_identity, naming the item's category and tier, and is flagged. A successful win still writes the ledger grade, so the save door's grade freeze keeps it. Still open: a stone with no uid keys on its position (F6-042, rest), and "settled" is still proven by any newer save (F6-043). Probe: tools/probe_grades.mjs.

2026-09-30 — the attempt key is not the client’s to choose (dfmc-client review P6 F6-042). ON DEV. The #119 refresh guard keys a grade, soul-grade or enchant draw on the target plus a server-held attempt number, but the target was the client’s own iid string: leaving it out fell back to the fresh per-click idem (the pre-#119 reroll), and a varied iid (bag:U1#2, #3...) was a new attempt each time. /api/roll now refuses a targeted ask with no iid (400 iid required; every client since 0.48 sends one), and when the ask names an item the server holds a row for - the graded item’s uid, or the enchant scroll’s uid - the attempt is keyed on that row, so no iid variation reaches a new draw. Gems and runes with no ledger row (#799) keep the positional key; the grade anchor stays positional (#687). Still open, for D: the settle evidence is any later save, not the debit (F6-043).

2026-09-30 — review drift fixes: F010, F011, F012, F013, F014, F015. ON DEV. The page caught up with the code; no rule changed here. F010: the B2 7.5 up-to-10-draws click now has its own paragraph under Where the roll happens. F011: the retired upgRoll(st) seam is no longer named as live; the forge reads no RNG. F012: the ticket section now says nothing is spent until the server answers. F013: upgSweep() has no 4 s gate; an unanswered ticket waits UPG_ROLL_MS (12 s). F014: the soul-grading table printed the retired ×0.05/step SoulMult (×1.05–×1.45); it now prints ×1.11–×2.00. F015: class-soul attempts pay the fixed four-currency bundle from soul_grading.csv, every currency required — not “any combination” / “all Scrap”; table columns rebuilt from the CSV.

2026-09-30 — a late upgrade win no longer reopens or swaps the item detail panel (project review F150). ON DEV. A fix that restores what this page already describes; no rule changed. F008 stopped a late win from reopening a closed bench, but the class-soul and bag win branches still rewrote the item detail panel unconditionally: close the detail (or open item B’s) while an attempt on item A was in flight, and the win reopened A’s detail, or showed A while B’s buttons still acted on B. The panel now follows a win only when it is open on that same item; otherwise it is left as the player left it. The item still promotes where it lives. Gate: tools/probe_review_c13c09.mjs.

2026-09-29 — a plain ATTEMPT upgrades again, a closed bench stays closed, and the ALL-IN figure counts every currency (project review F007 / F008 / F016). ON DEV (0.88). These are fixes that restore the behaviour this page already describes; no rule changed. F007: since the ordinary click started settling up to ten draws in one call (dev only, never on release), every plain ATTEMPT was refused “NOT ENOUGH SCRAP FOR ONE ATTEMPT — NOTHING WAS SPENT” on any wallet, and only ALL-IN still upgraded: the server bounds those draws by the wallet the client claims, and the client sent that claim on ALL-IN only. Every attempt now sends it; the server still holds it to what the stored save allows. F008: “Opening or closing the forge settles; it never cancels” — and it settled a little too loudly: close the window while an attempt was still in the air, win it, and the window popped back open on the promoted item (or, if you had opened a different item meanwhile, swapped back to the old one). The win now lands on the item and the toast tells you, and the window you are looking at is left alone. F016: the ALL-IN confirmation’s “you can afford up to N attempts” divided only the first currency of the price; a class soul pays four, so it could promise 100 where the Dust allowed 5. It now takes the tightest one. The server always decided the real count. Gate tools/probe_upgbench.mjs in the client repo (15/28 → 28/28).

2026-09-25 — the bench promotes the instance that rolled, never the first record that looks like it (#961, D). SHIPPED IN 0.87. “I am upgrading legendary scrolls and they are jumping from legendary to divine.” The ladder was never wrong: grading.csv authors Scroll Legendary → Abyssal → Divine at every tier, and the bench paints exactly that. The jump was a transplanted uid. The bench found the bag record to rewrite by reference, else by name + grade + tier — an identity, not an instance — and wrote a copy of its own item over whichever record matched. The reference goes stale through the detail panel’s LOCK button, which swaps the record for a copy and leaves the panel’s pin on the old object; so with two Legendary scrolls of one kind, the win promoted the first one with the bench item’s uid stamped on it (the same account’s duplicate-uid flag, forty minutes earlier). The stale twin then claimed Legendary under that uid, whose anchor the win had just set to Abyssal; the door refused it and the #781 resync lifted the twin to Abyssal for free — one paid rung, two Abyssal scrolls, and Abyssal → Divine on the face of a scroll that had never rolled. The record is now found by its uid (identity only for uid-less legacy records, which cannot be told apart anyway) and the promoted record is built from the one that lives in the bag, on the win and on the resync, so no rewrite moves a uid between instances. Probe: tools/probe_scrollrung.py — 2/6 before, 6/6 after.

2026-09-16 — the detail panel’s UPGRADE button opens on the item as it now is (#902, D). ON DEV (0.81). “Started legendary, is now divine. But when I click upgrade it shows legendary → abyssal still.” Opening an item’s detail panel pins the clicked bag record, and the panel’s UPGRADE button opens the bench on that pinned record. A successful upgrade never rewrites the old record: it builds the promoted one, swaps it into the bag, follows it in the bench and redraws the detail panel from it — so the panel read Divine while the pin still named the Legendary object, and the next UPGRADE click opened the bench on Legendary → Abyssal (the button’s own can-upgrade test judged the stale record too). The pin now follows the promotion the way the bench and the panel already do, on every path that rewrites a bag or class-soul record’s grade — the win and the #781 resync. Equipped and socketed targets are unaffected: they are opened from the equipment screen, not the detail panel.

2026-09-14 — a single attempt plays the theatre, and the window stops resizing (#844, D). LIVE on release since 2026-09-14 (0.80). The attempt theatre was gated on the ALL-IN flag, so a single upgrade spent the materials and printed a line. The sequencer was always general, so a single attempt is simply one of one and lands on its own verdict: the last attempt of a burst that fails IS the burst failing. The resize was not where the last fix looked. The 0.70 pass pinned every block that comes and goes and missed the two that are always there and can WRAP — the item name and the grade line. Measured at 1423×687, an item with a long name takes a second line and the window grows 18.2px, simply by being opened on a different item. The name is one line with an ellipsis and the grade line is pinned. The gate samples the window every 90ms through a whole attempt rather than comparing before and after.

2026-09-09 — the upgrade card on a socketed stone is roughly twice the size (#809, D). LIVE on release since 2026-09-09 (0.73). The card that offers to upgrade the stone already in a socket had a 13-pixel label in a panel eleven hundred wide — the way into the whole grading economy, drawn smaller than the text around it. The label and its cost line are both close to double, the card has room to breathe, and it carries the same lit border and hover as the other things you can press. Neither half abbreviates and they do not run into each other. Gate tools/probe_upgcard.py 5/5, red 3/5.

2026-09-09 — the upgrade window tells the truth while the theatre is still playing, and says so when an item is already at the top (#800 / #805, D). LIVE on release since 2026-09-09 (0.73). The all-in theatre holds the item’s face at the grade being played, but the chance and the price underneath it were read from the grade the item had already reached, so the moment you pressed the button they jumped to the next step while the current one was still being animated — “35% chance · 80 Dust” became “20% chance · 160 Dust” before a single attempt had finished. The balance beside them already counted down one attempt at a time, which is why only those two moved. They now follow the theatre the same way the face does. And at the very top of the ladder there is no next step at all, so the window printed a zero chance, an empty price, a dash for the target and an attempt button reading “undefined”; it now says CONGRATULATIONS, THIS IS THE MAXIMUM UPGRADE POSSIBLE and offers no attempt. That state is only ever reached by an upgrade succeeding into the top grade — the window will not open on an item that has nowhere left to go. Nothing was ever spent there: an attempt at the top grade sent no request and cost nothing, before this change or after. The window keeps its one size through all of it. Gate tools/probe_upgmax.py 8/8, red 4/8.

2026-09-09 — a socket’s grade memory can no longer speak for a stone it cannot name (#799 / #792, D). LIVE on release since 2026-09-09 (0.73). The server keeps a grade anchor per grading target, and for a socketed stone that target is the socket position. Since 2026-09-02 the anchor has been stamped with the stone’s own identifier, so a different stone that auto-optimise moves into the socket re-anchors instead of being refused — but a socketed gem or rune carrying no identifier of its own falls back to its name, and one name covers every copy you own, at every grade. Stamped on the anchor, that name let the door read the next stone in the socket as the same stone: the honest attempt was refused, and the client then adopted the grade the server named, in place, with nothing spent — lifting the stone when the anchor sat above it and quietly lowering it when the anchor sat below. A name is no longer accepted as an identity. The socket’s memory is not evidence about who is sitting in it, so the attempt goes ahead and is recorded for review, exactly as an unknown item already was; every stone that does carry its own identifier keeps the forgery guard and the re-anchor unchanged, and equipped pieces were never exposed to this. The permanent close — socketed stones carrying their own identifiers, and ledger rows behind them — is still open. Gates: tools/probe_gradeident.mjs 6/6, red 4/6; tools/probe_gradeanchor.mjs 5/5.

2026-09-07 — the ALL-IN button is greyed out when you cannot afford one attempt (#784, D). LIVE on release since 2026-09-08 (0.71). It reads the same affordability the ATTEMPT button reads, so the two can never disagree, and a dimmed button does nothing when clicked.

2026-09-07 — the All-In window keeps one size, tells the truth on its counter, and counts your balance down (#779 / #780 / #783, D). LIVE on release since 2026-09-08 (0.71). The burst header used to read “ATTEMPT i / <the attempts the burst takes>” — the outcome in plain sight; it now counts toward the attempts you could afford when you pressed the button (the door’s own figure, capped at 250). The cost line used to show your balance after the whole burst; it now walks down one attempt per verdict and carries the resource icon from the shared currency set. And the window no longer resizes as the header, the message, the verdict and the ALL-IN button come and go: each sits in a slot of fixed height, and lines that would overflow shrink instead of wrapping. Gate tools/probe_allin_window.py 7/7, red 1/7.

2026-09-07 — a grade-anchor refusal resyncs the item instead of asking you to reload (#781 / #782, D). LIVE on release since 2026-09-08 (0.71). When the server’s roll memory holds a higher grade than your copy (a success whose answer never reached you), “reload to resync” could never work — the stored save is your own copy. The client now adopts the server’s grade into the record where the item lives, saves, and claims from it on the next attempt; nothing is spent or minted. Gate tools/probe_gradeanchor.py 7/7, red 2/7.

2026-09-02 — the grade anchor follows the item, not the socket; refusals say what the server said. LIVE on release since 2026-09-02 (0.67), server half included. Player report #687: an All-In on a freshly socketed gem answered “could not reach the server”. Since the #623 fix the server keeps a grade anchor per grading target so a forged claim cannot buy high grades at low odds — but the target for a socketed stone is its socket position, and auto-optimise moves stones between sockets on every pass, so the position’s anchor belonged to the last stone that sat there and the new occupant’s honest claim was refused. The client now sends the item’s own identity beside the target and the anchor is stamped with it: a different item at a position re-anchors (on the grant grade when the ledger knows the item, else on the claim, flagged), while the same item claiming a grade it does not hold is still refused with the anchor named. And the client no longer folds every refusal into a wire failure: a named refusal is shown by name, and the anchor one tells the player which grade the server records and to reload. Gates: the door against the real worker 3/5 → 5/5, the client 1/4 → 4/4.

2026-08-13 — grading rolls book their price (the economy BOOK phase's third brick). On release. Grading and soul-grading are the game's largest currency sinks, and their authored costs were debited on the bench with nothing recorded server-side. The attempt's price now books into the permanent wallet ledger the moment a fresh draw is taken — and only then: unsettled replays, repeated attempts and retried requests all answer the stored result and book nothing, so a roll costs its price exactly once, win or lose, just as the bench charges. The roll answer reports what was booked. Gate: tools/probe_rollbook.mjs 5/5 — a craft-door balance bracket proves the ledger moved by exactly one authored roll price and nothing more.