Upgrade tree#27
Merged
Merged
Conversation
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.
[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.
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).
2 tasks
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.