Skip to content

Upgrade tree#27

Merged
flipbit03 merged 32 commits into
mainfrom
feat/infinite-upgrade-tree
May 16, 2026
Merged

Upgrade tree#27
flipbit03 merged 32 commits into
mainfrom
feat/infinite-upgrade-tree

Conversation

@flipbit03
Copy link
Copy Markdown
Owner

No description provided.

flipbit03 added 26 commits May 14, 2026 23:06
Replaces the 34-entry hand-authored upgrades panel with a sprawling,
fully-procedural DAG inspired by Path of Exile's skill tree. Everything
about each node — position, name, primitives, cost, edges, rarity — is
derived from a stable compile-time seed (`DEADBEEF_BANANA`) so every
player sees the same world. Saves only persist the set of bought lot
coords; node content regenerates on load.

Engine: each node owns a `Vec<Primitive>` (Op × Target × magnitude).
Mods stack additively for `AddPercent`/`FlatFps` and multiplicatively
for `MulFactor`; per-fingerer modifiers + a parallel-aggregate cache
keep hot-path FPS reads O(1).

UI: full-screen modal with pan/drag, click-to-focus, smooth tween
animations, plasma shimmer on the cuque-as-anchor when focused.
Touch-friendly buy/refund buttons (refund taxes 30% — exploration
isn't free). Bresenham staircase edges with rounded corners; lines
bend perpendicularly into box borders so termini never dead-end into
empty space.

Save: V4 schema drops `upgrades_earned`, adds `tree: UpgradeTreeState`.
Live migration path tested.

Old finite upgrade system fully removed (no compat shims).
`×0.91 spawn rate for Lucky spawn` read as a downgrade because the
displayed wording said "spawn rate" while the engine secretly meant
"cooldown multiplier" — a value below 1.0 actually produced *faster*
spawns. The contradiction landed on Small nodes (always-boon) and
made them look like downgrades.

Flip the semantic so the magnitude is the literal spawn rate: >1.0
means more frequent spawns (boon), <1.0 means rarer (bane). Cooldown
in sim.rs now scales as `base / mul` instead of `base * mul`. The
keystone-bane / Small-boon paths in `roll_magnitude` swap branches to
match, and `is_bane` reclassifies SpawnRateMul alongside the other
multiplicative ops where <1.0 is the nerf side.
Drag-panning moves `pan_*` without touching the tree cursor, so the
visibility harvest (which was anchored on `cursor`) kept its existing
center and ran out of nodes once the camera dragged past it. The tree
felt "ended" until the player clicked a node at the edge — that
TreeFocus moved the cursor and re-centered the harvest, popping the
next chunk into existence.

Derive the harvest center from `(pan_x, pan_y) + canvas_center` and
take div_euclid by `LOT_W` / `LOT_H` to recover the lot under the
viewport center. The harvest now follows the camera frame-by-frame
during drag, so the tree generates ahead of you naturally.
The 10-slot bookmark system was overkill for navigation that, in
practice, only needed two anchors: jump back to the cuque and jump
back to the most recently bought node. The new dynamic shortcuts
remove the modal `b`-prefix and the digit save/jump grammar entirely.

`UpgradeTreeState` drops `bookmarks: [TreeCoord; N_BOOKMARKS]` and
adds `last_bought: Option<TreeCoord>` — populated by `buy_tree_node`,
cleared by `refund_tree_node` when the refunded lot was the last
bought, and reset by `prestige_reset`. The V4 save schema picks up
the change; serde silently ignores the old `bookmarks` key on saves
written by earlier commits of this branch.

`Action::TreeBookmarkSet` and the `tree_bookmark_pending` UI flag are
gone. Two new `HelpAction` variants (`TreeFocusOrigin`,
`TreeFocusLastBought`) make the help-bar tokens `[0] root node` and
`[1] last bought` both keyboard- AND mouse-clickable, matching the
hover/click affordance of `[t/Esc] close` and `[q] quit`.
The info pane's `[Click/Enter] to buy` and `[Click/R] refund` buttons
were already clickable, but their hover state was indistinguishable
from the surrounding text — players couldn't tell they were targets
until they hit-tested by hand. Match the help-bar token hover (white
fg + dark bg tint + bold) so the action button reads as a button on
mouseover.
When a buy unlocks a neighbor, the path from the just-bought node to
each newly-reachable destination now lights up cell-by-cell over a
short window (~100ms / cell). Multiple unlocks animate in parallel —
all anims share the tick clock, so all wavefronts march simultaneously
even when their paths differ in length.

The dim base line stays drawn throughout; the wavefront is an
overpaint (head white, trail cyan → resting blue) that runs ON TOP of
the existing wire. Destination boxes stay gated as not-yet-reachable
until their incoming wavefront arrives — at that moment the anim is
removed and the familiar gold unlock_flash fires.

The path-cell logic moved to `game::tree::node::edge_path_cells` /
`bresenham_path` so the tick advance and the renderer share the same
geometry. The old `paint_h_segment` / `paint_v_segment` shortcuts in
the renderer are gone — `path_glyph` handles straight and diagonal
cases uniformly through the new unified path walker.
Previous pass flipped the WHOLE edge to the lit one-owned style the
moment the buy fired, with the wave running as a 3-cell bright overlay
that decayed back to the same lit color. The line never visibly went
from grey-to-bright — it was bright everywhere, with a slightly
brighter blob moving on top.

Snake-game model instead: cells AHEAD of the wavefront keep the dim
both-not-owned grey (the pre-energize state), and cells BEHIND the
head settle into the lit resting style and STAY there. The wave
literally paints the line from grey to lit, segment by segment,
until the head reaches the destination box.
Two bugs reported on the unlock wavefront:

1. Edges to ALREADY-REACHABLE neighbors weren't animating — the
   `was_reachable` check skipped them entirely, so the edge popped
   from grey to lit instantly while sibling edges animated. Now every
   procedural edge from the just-bought node animates; the
   `gates_destination` flag (newly-reachable destinations only)
   continues to drive purchase gating + the gold unlock_flash on
   completion, while already-reachable edges animate decoratively.

2. The wave appeared to "lag" 1-2 ticks across directions. Cause:
   the wave's logical head started at path index 0 (inside the source
   box), and the visible portion only appeared once the head reached
   cells outside the box silhouette — which costs ~7 cells for
   horizontal/diagonal edges (14-wide Small box) but only ~1 cell for
   verticals (3-tall box). Fix: stamp each anim with its path's
   `leading_inside` count at push time and seed `head_cell_index`
   past it, so the visible wave starts at cell 0 of the rendered line
   on tick 0 across every direction. `trailing_inside` mirrors this
   at the destination end so the anim finishes when the head reaches
   the cells INSIDE the destination box, not the path end.

Renderer also walks paths in anim direction so the leading/trailing
offsets (computed against `edge_path_cells(from, to)` at push time)
match the cell order the renderer iterates.
User reported the wave's staircase shape differing from the grey base
line at (-2, -9). Root cause: `bresenham_path(A, B)` and
`bresenham_path(B, A)` produce different cell sequences for the same
endpoints, and the previous commit had the renderer compute the path
in anim.from → anim.to order when an anim was active. When the
renderer's iteration order (a, b) didn't match the anim direction, the
post-buy wave painted a Bresenham mirror of the pre-buy grey line.

Fix: make `edge_path_cells` canonical (lo→hi by lot lex coords),
independent of call-site argument order. Pre-buy renderer, post-buy
renderer, the unlock-anim's tick completion check, and the
buy_tree_node push all share the same deterministic path.

The anim no longer caches `leading_inside` / `trailing_inside` — those
counts now derive lazily from the canonical path at draw / tick time,
since the canonical direction stays stable but the wave's notion of
"source side" depends on which end is anim.from. Renderer branches
dist_from_head on `wave_at_start` (whether anim.from is the canonical
lo); tick mirrors the same split when computing the visible-cell
stretch for the completion check.
[M3] Three tree primitive targets had no downstream consumer — players
paid cuques for "×1.15 effect on Frenzy reward", "×1.10 effect on Buff
duration", "×1.15 Green Coin strength" etc and the aggregates were
computed but never read. Wire `powerup_reward_mul`, `powerup_duration_mul`,
`green_coin_strength_mul` into `catch_powerup` so Lucky cuques, Frenzy
duration, Buff (PurpleCoin) duration + mul-factor strength, and Green
Coin AddPercent are all multiplied by their tree contributions on catch.

[H1] pt_BR locale leaked English text in the tree info pane. Localize
the rarity tags, the primitive blurbs (connectors "to", "cost on",
"spawn rate for", "effect on", "flat to"), every target label (all
fingerers, click power, prestige multiplier, Green Coin strength, plus
"{kind} spawn/reward/duration"), the rarity-tag-and-cost lines, the
unreachable / empty / owned / refund hints, and the buy/refund button
labels. Node titles intentionally stay English ("magical incantation"
flavor — like FF spell names being Latin/Greek across all locales).

[H2] `fold_primitive`'s catch-all silently no-op'd unhandled
`(Op, Target)` combos. Replaced with a `debug_assert!` so a typo in
`pick_op` or a future enum variant fails loud in dev and tests
instead of charging players cuques for effects that don't fold.

[M1] `EdgeUnlockAnim::visible_advance()` did plain `u32 * u32`. Switch
to `saturating_mul` so a runaway-tick anim can't overflow into a
wraparound head index.

[M2] `roll_cost` returned `f64::INFINITY` at manhattan distance ≈ 1232+
(growth=1.75 overflows past `f64::MAX`). Clamp the raw product to
`1e300` so cost rendering and the affordability check both get a
finite number to compare against, even at absurd depth.

[L1] Removed dead `fn _used(spec: &NodeSpec)` stub in ui/tree.rs.
[L2] Removed `let _ = (PowerupKind::ALL,);` placeholder in aggregate.rs.
[L3] Dropped unused `tree_agg` parameter from `draw_box` signature +
     callsite (renderer never used the aggregate it received).
[L4] (subsumed) — `wave_at_start` branch is correctly skipped when no
     anim is active; the unused-when-no-anim head_path_index is benign.
[L5] `dump_cost_table` test now `#[ignore]`d — diagnostic-only output,
     not a real assertion. Run explicitly via:
     `cargo test --release dump_cost_table -- --ignored --nocapture`.

L6 (refund help-bar token visibility) intentionally left as-is — the
help bar advertises KEYBINDINGS, not currently-available actions; the
info-pane action button already context-switches between Buy and Refund.
User flagged a case at lots Penumbral (-5, 0) ↔ the_Crimped (-5, +1)
↔ Heir's Reckoning (-4, +1) where all three pairs had edges (two
orthogonal + one diagonal), creating a 3-cycle. The diagonal is a
redundant shortcut — you can already reach Heir's from Penumbral by
walking through the_Crimped — so it "defeats the purpose" of the
tree's intended branch-and-commit feel.

Add a second suppression check in `edge_exists` for diagonal pairs:
if a 2-hop orthogonal path through either L-bend corner lot already
connects A and B (both ortho hops `edge_exists`), drop the diagonal.

Anchor diagonals are safe — the `anchor_of() == Some(...)` early-return
fires before the new check, so the connectivity guarantee that gives
every non-origin node a parent toward the cuque survives unchanged.

The recursion is bounded: the new `edge_exists(a, mid_h)` / `(mid_h, b)`
calls are ORTHOGONAL queries which skip the diagonal branch entirely.

Test: `diagonal_edges_dont_close_triangles_with_existing_orthogonal_path`
walks every diagonal king-pair in a 60×60 lot region and asserts no
surviving diagonal closes a triangle with two existing orthogonal hops.
…nal-only

Earlier attempt only suppressed DIAGONALS that closed orthogonal-2-hop
triangles, and gated suppression on `edge_exists` recursion which
returned `true` for anchor diagonals first. That left the user's
example (Penumbral / the_Crimped / Heir's Reckoning) still triangulated
because Penumbral's anchor parent IS Heir's Reckoning — anchor selection
fell through Pass 1/2 (the orthogonal-strict-smaller candidate (-4, 0)
is a gap) and returned the diagonal in Pass 3. The actual redundant
edge in that triangle is the ORTHOGONAL Crimped ↔ Heir's, a procgen-
rolled edge with no anchor responsibility.

New rule, applied to BOTH orthogonal and diagonal edges: after the
probability roll, scan common king-neighbors of A and B. If any
populated neighbor `C` is anchor-connected to BOTH A and B, suppress
this edge. The 2-hop A → C → B uses anchor edges (permanent), so the
direct edge is a redundant triangle closer. Anchor edges themselves
get an early `return true` before this check so they're never the
ones suppressed.

Anchor-only criterion avoids the "all three edges of an all-procgen
triangle suppress each other and the graph disconnects" failure mode
that a recursive `edge_exists` check would create.

Test renamed + rewritten: `non_anchor_edges_dont_close_anchor_triangles`
walks every king-pair in -30..=30 lots and asserts no non-anchor edge
has a 2-hop anchor path through a common neighbor.
User found a 2-node island at (+12, +12) Crimped-Friend / (+12, +11)
Gibbous Lus, completely disconnected from origin. Trace of the anchor
chain revealed a 2-cycle:

  (12, 12) -> anchor (12, 11)   ← Pass 1 strict-smaller orthogonal
  (12, 11) -> anchor (12, 12)   ← Pass 3 fallback (no strict-smaller
                                   neighbors populated, picks the
                                   only populated neighbor — cycles)

The Pass 3 "any populated neighbor" fallback in `anchor_of` doesn't
require strictly-smaller manhattan, so it lets the chain cycle or
plateau without ever reaching origin. The previous anchor-redundancy
fix exposed this pre-existing bug because some procgen edges that
used to bridge isolated clusters back to the main tree were now
suppressed as triangle-closers.

Two-part fix:

1. Drop Pass 3 from `anchor_of`. Pass 1 (orthogonal strict-smaller)
   and Pass 2 (diagonal strict-smaller) both guarantee monotonic
   manhattan decrease, so the chain can't loop. When neither pass
   produces a candidate, return `None` — the lot is truly orphaned
   from origin.

2. Add `anchor_chain_reaches_origin(lot)` that walks the chain up to
   `manhattan(lot) + 2` hops and asserts termination at origin. Used
   by `node_at` to suppress orphan lots: if the chain doesn't reach
   origin, the lot is part of an island and `node_at` returns `None`.
   Player never sees unreachable nodes.

Test: `every_node_has_anchor_chain_to_origin` walks every populated
lot in -30..=30 and asserts the chain terminates at origin.
Reverts d6f036a, 290bbdf, 0d71bed.

The triangle-suppression rule (suppress non-anchor edges that close
anchor 2-hop triangles) combined with the orphan-island culling
(suppress lots whose anchor chain doesn't reach origin) over-pruned:
- Tree felt no longer infinite — many populated regions were culled
  because their procgen-rolled bridges to origin were stripped first
  by the triangle rule, then the anchor chain check axed the now-
  orphaned regions.
- Topology shifted noticeably left-oriented and sparse.

Triangles are accepted as the price for keeping the procgen open and
the tree feeling alive. The anchor-of Pass 3 fallback (which closes
the 2-cycle bug the orphan-island fix targeted) is restored — that
fallback's pathological case (Crimped-Friend + Gibbous Lus mutual
anchor) is rare enough to live with vs. the wholesale culling.
H-NEW-1: pt_BR i18n leaks. Five remaining English-only strings on the
tree info pane (the anchor's [Root Node] tag + blurb + footer, plus
the two refund-rejection reasons) now go through `Lang` and have
pt_BR equivalents.

H-NEW-2: i32::MIN coord wrap. `x.abs() + y.abs()` panics in debug at
extreme coords (i32::MIN's abs overflows) and wraps to 0 in release —
making far-out lots look origin-cheap. Switch every site to
`x.unsigned_abs().saturating_add(y.unsigned_abs())` via the existing
`TreeCoord::manhattan` helper (which now uses saturating_add too).
The diff-of-coords inside `anchor_of::is_orthogonal` uses
`wrapping_sub(...).unsigned_abs()` since the diff itself only needs
to be ≤ 1 for the king-neighbor check.

H-NEW-3: Action-button click race. The render-time cursor was being
captured-by-reference: `(action, rect)` got published, then the click
handler read `ctx.current.tree.cursor` which could differ by one
frame after a keyboard nav. Bake the cursor into the tuple at render
time: `Option<(TreeButtonAction, Rect, TreeCoord)>`. Click handler
uses the captured coord, not the live one.

M-NEW-1: refund_tree_node ghost-lot cleanup. If `node_at` returns
None for a lot still in `tree.bought` (corrupted save / future
procgen change), the refund used to return 0 and leave the ghost
entry in place. Now removes the lot from `bought` (and clears
`last_bought` if it matched) before bailing out, so cleanup happens
on first refund attempt instead of never.

M-NEW-2: `x * LOT_W` overflow at |x| > 5.96e7. `saturating_mul` +
`saturating_add` clamp instead of wrap; the renderer's
canvas_to_screen still culls off-screen coords cleanly.

M-NEW-4: prestige_earned_total saturation. A corrupted f64
lifetime_cuques near INFINITY produced u64::MAX prestige, blowing
prestige_mult to ~1.8e17. Clamp the result to 1_000_000 papers
(equivalent to ~1e16 lifetime cuques — far past any legitimate run).

M-NEW-5: edge-anim tick-then-prune ordering. The retain that drops
empty-path anims now runs BEFORE `anim.ticks += 1`, so a now-invalid
anim doesn't linger one extra tick gating its destination as
unlock_pending.

M-NEW-6: Removed dead `pub const N_PRIMITIVE_TARGETS: usize = 24`
(stale value, no callers).
…uard

Previous cap at 1_000_000 papers was too aggressive — a long-haul
legit player already exists at 500k papers and could legitimately
reach the cap, where their progression would silently truncate. The
adversarial review's concern was about INFINITY/NaN poisoning the
math, not about real players reaching extreme totals; the
`is_finite()` + `>= 0.0` guards alone catch those corruption paths.

`raw as u64` saturates at u64::MAX (1.8e19 papers), which corresponds
to `lifetime_cuques > 3e44` — outside the reachable f64 range for any
legit save. So the unguarded cast IS the implicit ceiling, and it's
many orders of magnitude past any realistic playthrough.
A single misfired `[r]` keystroke (or stray click on the in-panel
hint) used to wipe the entire run — fingerers, tree, buffs, gone.
Add a `prestige_confirm_pending` flag on `UiState`:

- First trigger (keyboard `[r]` in Prestige mode OR click on the
  in-panel `Press [r]` line OR click on the help-bar `[r] reset &
  claim` token) ARMS the pending state. No reset fires.
- Confirm: `[r]` again, `[y]/[Y]`, `Enter`, or click the now-rendered
  `[Y/Enter] Yes, reset` line → emits `Action::PrestigeReset`, clears
  pending, returns to Game.
- Cancel: `[n]/[N]`, `Esc`, click the `[N/Esc] No, cancel` line, or
  navigate away (`[p]`, `[s]`, `[t]`, `[a]`, help-bar mode-toggle) →
  clears pending without firing the reset.

Renderer (`prestige::draw`) returns `PrestigeRects { reset, yes, no }`
— `reset` populated when armed and prestige-available, `yes`/`no`
populated when mid-confirmation. The mouse-hover lift the previous
single-confirm line had is now applied uniformly across all three.

i18n: new keys for the confirm question, the warning text, and the
two button labels. EN + pt_BR translated. `s` ("Sim") is NOT
accepted as a Yes shortcut because it collides with the Stats-mode
toggle handler — the pt_BR label still uses `[Y/Enter]` so the
keybinding is identical across locales.

The existing `prestige_reset_rect_available_emits_action` test was
renamed `prestige_reset_rect_arms_confirmation_then_yes_emits_action`
and updated to walk both halves of the two-step flow.
Click rects for Yes / No are computed from Vec line indices assuming
1 line = 1 visual row. The previous warning string was too long for
the prestige panel's ~34-char usable inner width — `\n`-split into
two chunks, sure, but the first chunk ("This wipes ALL fingerers,
tree, and buffs.") was still 42 chars and soft-wrapped to 2 visual
rows. That shifted everything below it down by one, so the Yes-
button click rect landed on the blank row ABOVE the rendered "[Y/
Enter] Yes, reset" text — and the hover lift highlighted the row
above the button.

Trim both locales' warnings so every `\n`-separated chunk fits in
~34 chars. Each chunk now renders on exactly one visual row, and
Vec index == visual row again.

The `lines()` push loop (added in the previous commit) is still the
right structure — it'd silently break the rect math if any future
chunk grew long enough to wrap, so the comment now flags that
constraint explicitly.
@flipbit03 flipbit03 self-assigned this May 16, 2026
flipbit03 added 3 commits May 15, 2026 21:57
[HIGH] Yes/No click rects misaligned with rendered button text whenever
ANY line above them soft-wrapped. The previous "trim the warning text"
patches only protected one specific line; pt_BR's longer
`prestige_owned_label` ("Possui atualmente") + currency name pushed
the panel's first content line over the inner-width threshold,
wrapping it to 2 visual rows and shifting every Y/N rect index off
by one. EN was vulnerable too once `prestige >= 1000` brought the
"k" suffix into the same line.

Restructure `prestige::draw` to render via TWO separated paragraphs
inside the bordered block:
- Top "info" sub-area uses `Wrap { trim: false }` and is allowed to
  consume any space above the action strip — wrapping here is
  cosmetic, never structural.
- Bottom "action" sub-area is a fixed-height strip carved off via
  `Layout::vertical`. Its lines render WITHOUT wrap (each Vec line
  is exactly one visual row) and click rects derive from
  `(action_area.y + offset, height = 1)`. Wrap on info lines can no
  longer shift the button rects.

[MEDIUM] `(owned + 1).pow(2) * 1_000_000` now uses `saturating_pow` +
`saturating_mul`. With the prestige cap removed the formula could
overflow u64 around `owned > 1.36e7` (debug-panic, release-wrap).
Reaching that threshold legitimately is impossible; the fix just
keeps the displayed `next_threshold` from going garbage.

[LOW] `save_to_string` now sanitizes non-finite f64 fields (`cuques`,
`lifetime_cuques`, `best_fps`) to 0.0 before serializing. serde_json
refuses to serialize NaN / INFINITY, and the native + wasm save
paths historically `let _ =`-swallowed the resulting `Err` — a save
that hit a non-finite value would silently stop being written until
the player noticed they'd lost progress. Sanitization keeps the save
flowing; reaching a non-finite value still requires save corruption,
so prefer "lose the corruption" over "lose subsequent saves".

[MEDIUM] Trim cosmetic-wrap on `prestige_not_enough` (EN + pt_BR) and
`prestige_owned_label` (pt_BR) so the info area doesn't wrap on
default panel width. Click rects don't depend on these now, but
clean text reads better.
Pressing [r] when prestige_available() == 0 was a silent no-op, but
the help bar still advertised the action — misleading. Add a
parallel  Lang field (EN + pt_BR) and switch
to it in ui::draw whenever the prestige count is zero.
Previously the prestige reset was a true two-step confirm: first [r]
armed, second [r] (or [y] / Enter / click Yes) committed. The user
spotted the obvious fat-finger hazard — a quick double-tap of [r]
(or holding the key past auto-repeat) wipes the whole run before the
warning has time to register.

Now [r] ONLY arms the pending state. A second [r] is a no-op (no
re-arm, no cancel — the user stays armed and has to actually pick
Yes or No). Confirming requires a deliberately-different keystroke:
[y], Enter, or a click on the Yes button. Symmetric change in the
help-bar [r] reset & claim click handler so a double-click there is
also safe.

The existing keyboard [n] / Esc / click No / mode-toggle cancels
still work for backing out.
flipbit03 added 3 commits May 15, 2026 22:11
Two fixes that go together:

1. [r] keyboard only ARMS the pending confirm now — a second [r] (or
   holding the key past auto-repeat) is a no-op. Confirming requires
   a deliberately-different keystroke: [y], Enter, or click Yes. The
   in-panel "Press [r]" hint is the only entry into pending state;
   it can't double-tap-fire the reset.

2. The help bar's `[r] reset & claim` token is gone entirely from
   Mode::Prestige. The token was redundant with the in-panel hint,
   it didn't update visually when the confirm prompt appeared, and
   a click on it was a second path into the (now-armed-only) reset
   that the user had to think about. Now the help bar just shows
   `[p/Esc] back  [q] quit` — navigation only — and the in-panel
   purple "Press [r] to reset and claim" + Y/N buttons own the
   action UX.

Drops the `help_prestige_no_claim` Lang variant (no longer needed
since the help bar never carries the conditional [r] token), the
`HelpAction::PrestigeReset` enum variant + its click handler in
input.rs, and the `(Mode::Prestige, "r")` arm in `map_help_token`.
…arse

CI runs `cargo doc -- -Dwarnings` and rustdoc parsed literal `[Small]`,
`[Pequeno]`, and `[r]` inside doc comments as broken intra-doc
links. Wrap each in backticks instead of double-quotes so rustdoc
treats them as inline code and skips link resolution.
wasm32 CI failed with E0502: passing `web.ui.prestige_confirm_pending`
(immutable read) alongside `&mut web.ui.tree_render` (mutable borrow
of a sibling field) as function-call arguments tripped the borrow
checker on wasm32 even though both fields are disjoint. Native
build's borrow checker accepts the same pattern.

Likely a stricter-disjoint-fields treatment on wasm32 — or the
compounding fact that the function call's RESULT writes back into
`web.layout`. Either way, hoisting the `bool` flag into a local
sidesteps the disagreement at zero runtime cost (bool is Copy).
@flipbit03 flipbit03 merged commit e6a8a44 into main May 16, 2026
7 checks passed
@flipbit03 flipbit03 deleted the feat/infinite-upgrade-tree branch May 16, 2026 01:18
flipbit03 added a commit that referenced this pull request May 16, 2026
The hardcoded `UPGRADES` catalog was retired in V4 (#27) when the
procedural skill tree shipped. README still pointed players at an
[u] Upgrades panel that no longer exists; the actual hotkey is `t`,
and the feature is an infinite Path-of-Exile-style skill web, not the
old double-the-output unlocks. Update the keybinds table and the
features bullet to match the live game.
flipbit03 added a commit that referenced this pull request May 16, 2026
…t] Upgrade tree

- Keybind table: drop the dead `[u] Upgrades panel` row (the hardcoded
  UPGRADES catalog was retired in V4 / #27); add `[t] Upgrade tree`.
- Features section reworked end-to-end:
  - Ten tier names listed in order so the reader sees the full ladder.
  - Tree bullet describes the actual interaction (pan with hjkl /
    arrows / mouse-drag) and the same-seed-for-everyone invariant.
  - Powerups now lists all four kinds (Lucky / Frenzy / Buff (Purple
    Coin) / Green Coin) — Green Coin was missing entirely.
  - New "Truly unbounded numbers" bullet covering the Mag log-magnitude
    bignum + infinite alphabetic suffix tail shipped in v1.1.0.
  - New "Casino-style animated border" bullet for the chromatic
    pulse-on-activity chrome.
  - Mention the four ASCII zoom levels and the keyboard/mouse parity
    explicitly.
  - Save path mentions the platform equivalents (macOS Library, Windows
    %APPDATA%) instead of just the Linux XDG path.
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