Version history for Rust & Roots
/api/art-manifest via Godot's HTTPRequest. nginx (stock Ubuntu has a global gzip on) gzip-compresses that proxied JSON response. Godot's HTTPRequest defaults accept_gzip = true, so it sends Accept-Encoding: gzip and receives Content-Encoding: gzip. On the web export the browser's fetch layer already transparently decompresses the body — but Godot also honors the Content-Encoding header and tries to gunzip the already-plaintext bytes a second time. That fails in stream_peer_gzip.cpp, hands garbage to the JSON parser (Parse JSON failed at line 0), and the manifest handler bails down its parse-error path: art_sync_done flips true with zero downloads, so every card renders the bundled-in-PCK fallback (the old placeholder art) instead of the commissioned art on the server.http.accept_gzip = false on all five art-sync HTTPRequest nodes in scripts/database.gd (manifest fetch + card-art batch download, web + native paths, plus the decorative-art fetch). Godot now requests identity encoding; nginx returns plaintext; no double-decompress; the manifest parses; all 214 commissioned PNGs sync into the in-memory texture cache and override the bundled art on every page load, exactly as designed.[ART-SYNC]/[ART-LOAD] diagnostic prints are kept for this release to confirm the fix in the console; a follow-up patch strips them once verified clean.404s for faction_logo_grove/neutral/syndicate and ui_button_end_turn/settings are unrelated and expected — those decorative assets haven't been finalized in the art-commission tool yet. They fail soft (procedural fallback) and never blocked card art.@onready path in scripts/settings_menu.gd pointed at $SettingsPanel/SettingsList/..., but the scene wraps SettingsList inside a Body VBoxContainer (added when the Visual Style theme picker landed). Every slider, toggle, and value label was null on scene init; _load_settings() hit one of them, threw, and the WASM eventually fell out of bounds. Paths corrected to $SettingsPanel/Body/SettingsList/.... Settings opens cleanly again from both the main menu and the in-game pause overlay.scripts/database.gd requested https://rustandroots.io/art/... via SERVER_BASE_URL even when the game was loaded from rustandroots.dev. Cross-origin → no Access-Control-Allow-Origin header → 300+ TypeError: Failed to fetch errors per second → WASM heap exhaustion. Now mirrors the card-art sync pattern: web build reads window.location.origin via JavaScriptBridge so the fetch stays same-origin; native build keeps absolute SERVER_BASE_URL._install_title_video_and_wordmark() helper has been deleted along with the call site, and both assets removed from the repo (.pck shrinks back ~10 MB).ffmpeg -c:v libtheora -q:v 8 -an in.mp4 out.ogv.logo.png backdrop. Loops seamlessly; the existing 15%-black DarkOverlay still tints on top so the menu buttons stay legible._ready() time in scripts/main_menu.gd without touching main_menu.tscn. Graceful fallback: if the video or wordmark asset is missing for any reason, the screen reverts to the v1.5.0 static background without crashing.token_006 Bishop (1/1 Neutral). Spawned only by the Bishop Artifact; carries a _grow_per_round flag the engine reads each combat iteration.app_version — legacy APKs see only units + spells and continue working unchanged.[Artifact] prefixed line lands in the combat log.start_of_each_turn, start_of_each_combat, end_of_each_combat, end_of_each_turn, passive_run_buff) on Artifact records.artifacts_state message replaces the local inventory wholesale when you rejoin a room. Stacked counts preserved.scripts/database.gd). One roster, one HTTP fetch loop, one cache, one decorative_art_ready signal. Any consumer that wants commissioned art reads Database.get_decorative_texture(subdir, id) with a graceful null fallback.app_version. Three call sites in network_manager.gd (Discord, username/password, dev fallback) read from the UpdateLoader.BUILD_VERSION autoload constant so we have one source of truth. Server stores it on the player record and uses it to gate v1.5.0 features per-client.generateShop takes a player ref so it can check that client's app_version. Pre-1.5.0 clients never get the artifact shop slot, regardless of ARTIFACT_SHOP_CHANCE.dealDamage now applies target.damage_reduction to non-combat damage sources too. The combat-damage path already had this — closing the loop so Iron Resolve really does mean “-1 from all sources” including surge, thorns, cleave, and ability damage.scripts/minion.gd). The _on_any_unit_sold handler runs the Compost effect block (1 + Compost Pile count) times per sell event. Stacking is unbounded — 2× Compost Pile = each Compost effect fires 3× per unit sold.api/multiplayer.js applyArtifactAction: apply_keyword (with kicker_faction / kicker_attack / kicker_keyword), apply_status (enemy target), passive_damage_reduction (Iron Resolve), damage_modifier_vs_status (Cold Hearth kicker), mark_first_attacked_for_status (Worn Bola), multiply_keyword_trigger (Compost Pile / Long Echo), strip_random_keyword (Mortar), summon_token (Bishop).passive_run_buff — fires on artifact_acquired and at start of every combat. recomputePassiveArtifactModifiers bridges player-level flag bags to per-unit fields the engine reads, so game-engine-v2.js stays owner-agnostic (reads unit fields, never player state).end_of_each_turn in both Duel + Rumble combat paths. Coffee Pot's transient buff lands here.game-engine-v2.js combat loop. A “round” is one full pass through both boards. On round-2 entry, _transient_round_1_keyword stamps strip — Tolling Bell's Guard expires correctly. Bishop's _grow_per_round bumps +1/+1 each iteration.hatch_multiplier=2) fires every Hatch twice. Mother Eternal × Long Echo = ×4 (multiplicative, intentional snowball).tools/repair-artifact-id-permutation.sh moved all 24 file pairs + 24 DB rows via a two-phase rename to avoid clobber. Backup tarball preserved on the VPS.themes/junkyard/foil_metallic.gdshader had a return in void fragment() for the early-out path; Godot's fragment processor disallows return statements. Inverted to if (is_foil) { ... } else { COLOR = vec4(0.0); }./play/rustandroots-alpha.{apk,zip}) that were retired with the 600mmr severance on 2026-04-24. The actual artifacts have lived at /heroes/heroes-alpha.apk and /heroes/heroes-windows.zip since the v1.0.0 cutover — links now point there. site/play/heroes.html was already correct; only the downloads page was stale. Also corrected the "Drop it at site/play/" instruction in the export guide.art/artifacts/<id>.png, but /api/art-manifest only reads the top level — so the Godot client could never sync artifact art. Routing now writes artifact full-art top-level alongside cards. A one-time boot-time sweep relocates any pre-existing files. Latent bug that would have hit at v1.5 ship time.RNR_ART_DIR/<id>.png, so every Godot client downloaded them as if they were card art — wasted bandwidth + memory, plus collision risk. They now route into per-category subdirs (artifact_icons/, keyword_icons/, faction_logos/, ui_icons/, backgrounds/, title_art/, splash_art/) reachable via /art/<subdir>/<id>.png. The card-art manifest stays card-only. Boot-time migration sweeps any existing top-level pollution into the new subdirs.grok-2-image references in api/xai-image-client.js header and docs/art-commission-tool.md swapped for the live model name (grok-imagine-image-pro / grok-imagine-image). Pricing constant + dropdown were already correct.ART_ATTEMPT_RETENTION_DAYS (default 30, 0 disables). GET /api/art-commission/retention/preview dry-runs; POST /api/art-commission/retention/sweep executes. Scheduled run kicks off 5 min after boot, then every 24 h./api/art-commission/cards. Frontend polls every 5 s; now passes ?since=<hash> and the server short-circuits to {cards: null, unchanged: true} when state matches (computed from max attempt_id + finalized_at + counts + anchor stamp + active presence). Idle browsers stop tearing down the 220-tile grid every 5 s.GAME_VERSION remains at 1.4.8 — admin-tool-only change./cards changed (added hash, plus cards: null on unchanged); a stale tab on the old client would set CARDS = null and break.art/artifacts/ dir can be removed once boot logs confirm zero skips.rustandroots.io and rustandroots.dev records a session (UUID in localStorage), heartbeat-tracked time on page, and final-duration beacon on unload. Geolocated server-side via geoip-lite (no external API, no rate limit). Four new SQLite tables: analytics_sessions, analytics_pageviews, analytics_logins, analytics_admin_actions.analytics_logins INSERT after issuing the JWT. Failed login attempts (unknown_user, bad_password) are recorded with IP + geolocation for failed-burst detection.requireAdmin middleware now records every admin-gated endpoint hit (who, what, when, IP, country, truncated params). Full forensic trail if the admin allowlist is ever compromised.rustandroots.dev/analytics.html with six sections: Live (5-min map via Leaflet + CARTO dark tiles), 24h summary cards, .dev access log (RED-flagged on non-admin visits), recent logins, admin actions audit, top pages. Auto-refresh every 15s.DISCORD_WEBHOOK_URL. Three triggers with per-key cooldowns: non-admin pageview on .dev (5-min cooldown per IP+path), failed-login burst (5+ in 5 min from one IP, 10-min cooldown), admin login from a new IP or country (60-min cooldown, looks back 90 days).nav.js: "This site uses minimal anonymous analytics. No third-party sharing." No banner, no consent prompt. .dev is admin-only and has no public-facing disclosure..dev only (admin paths return 404 on .io), _dev-gate.js blanks the page until JWT verifies admin, and requireAdmin rejects non-admin API mutations server-side. The beacon fires before the gate hides the page — even a probe that never authenticates leaves a forensic row._dev-gate.js injection on .dev.analytics.html off the public domain. Already-blocked admin pages (effects / factions-admin / tracks-admin / etc.) remain blocked.GAME_VERSION remains at 1.4.8 — no Heroes version bump for site infrastructure.CLAUDE_ENABLED is false (the default).assets/textures/bg_wood_dark.jpg) replaces the phase-specific scrapyard / battlefield paintings. Layered on top: parallax mouse tracking (background drifts ±15 px opposite the cursor), GPUParticles2D dust motes (warm amber, fade-in / sustain / fade-out alpha curve, ~80 motes), and a high-amount Reactor Sparks particle field (orange-yellow HDR pinpoints). All wired through a new WorldEnvironment with additive glow at HDR threshold = 1.0 so only the HDR-coloured sparks / dust / damage-flashes bloom.ui_hover click on hover, position.y +4 px and scale.y → 0.95 on button_down, spring-back on button_up with a ui_heavy_click mechanical clack. New ui_heavy_click SFX key in AudioManager.Visuals child (scale ↑5% from zone-fit, position.y ↑6 px, rotation tilted toward local cursor X). The HBox layout slot stays put — siblings don't shimmy. slam_to_position() helper added on minion + spell cards using TRANS_SPRING + EASE_OUT for placement weight.trauma value with per-frame decay. apply_damage_trauma(damage) normalises against 30 HP and applies a pow(_, 2.5) curve with a 0.1 floor — a 1-damage tap rattles, a 30-damage hit shakes hard. execute_hit_stop(duration, severity) drops Engine.time_scale for real-time milliseconds. flash_card_damage(card) bursts modulate to HDR Color(5, 5, 5, 1) then eases back — reads as a real bloom on glow-enabled builds. game_manager._process samples the trauma noise offset and applies it to self.position (no Camera2D in this scene tree).scale.y 0→1 over 0.1 s (TRANS_EXPO + EASE_OUT) like a CRT scan line filling in. Cancels cleanly if the cursor leaves before the dwell completes.themes/junkyard/foil_metallic.gdshader — blend_add canvas_item shader with noise_tex uniform + scrolling metallic highlights. Tints pulled from the Colors autoload (T6 brass → copper-hi at intensity 0.40, T5 taupe → bone at 0.25). Attached automatically to a new FoilOverlay ColorRect under Visuals/Overlays/ via _apply_foil_for_tier().parent.size.y. Both minion.gd and spell_card.gd now use a shared MAX_BOARD_HEIGHT = 210.0 constant; every card on the board renders at exactly 152 × 210 px regardless of how many cards are in the row. Stops the "one card upscales massively" bug at its root. Spell card scene refactored to mirror minion (Control root + Visuals Panel child) so both card types share an identical slot/visual decoupling.SalvageYard / OpponentBoard / PlayerBoard HBoxContainers now have custom_minimum_size = Vector2(0, 240). OpponentBoard.size_flags_vertical bumped from 0 → 3 to match PlayerBoard — was the actual root cause of opponent cards rendering at near-native size during combat.StyleBoxFlat at bg_color = Color(0, 0, 0, 0.35) + 1-px warm metal border + 4-px corner_radius. The new wood background reads through every zone with a subtle dark wash.Chat button (60 × 32 px, no panel chrome). Click to expand into the full Log / Chat tabbed panel; click − to collapse back. Stylebox swap + anchor collapse handle the visual transition.role_label subtitle (AI title for AI slots, "▸ CONNECTED" for the human). Slot height unchanged; name vertically centered in the available row._screen_shake call sites now call MatchManager.apply_damage_trauma(damage) instead of the older linear tween-based shake. Multi-target combos scale with their aggregate damage._ZONE_BOTTOM_MARGIN = 20.0 (since rolled into the hard-ceiling math) so card bottoms keep a safe margin off the divider line. v1.4.7's NameLabel min-height made the rendered card slightly taller than NATIVE_HEIGHT × scale; this catches that overflow._NATIVE_HEIGHT was 210 (didn't match the actual 232 scene native) and _SCALE_MAX was 1.00 (vs minion's 1.80). Plus spell card was a single Panel with no Visuals child, so the HBox slot stayed at full native while the visual scaled smaller. Refactor + unified math = identical slot dimensions for both card types.shop_manager.spawn_card_in_container and online_bridge._spawn_card_from_server_data now await get_tree().process_frame between add_child and _apply_scale_to_zone — the previous call_deferred could still fire before the combat-phase layout had settled..png extension — Godot's PNG importer wrote valid=false in the .import sidecar so load() returned null. Renamed to .jpg and updated the load path to try .jpg first then .png with a legacy fallback.z_index = -1 from the legacy CPU dust it replaced — pushed it behind the opaque background texture. Removed the override and bumped sustain alpha 0.22 → 0.45 (later HDR-bumped to Color(1.6, 1.3, 0.85) when WorldEnvironment glow landed)._icon entries) were renumbered to the v1.5 plan canon. The Bench / Old Lantern / Coin Purse now write to the file the in-game client actually loads.data/card_database.json; player.artifacts state + 4 hooks + apply_damage_trauma handlers in api/multiplayer.js; seven new REST endpoints (POST/PATCH/DELETE /api/artifacts/:id) in api/server.js; admin Card Editor extended to create / edit / delete artifacts. No player-facing effect this release — flips on when the Godot v1.5.0 client ships its inventory bar UI.RoadRage-Regular.ttf (graffiti-style display). Card names and Curio ability descriptions render all-caps via .to_upper() in _set_name_autoshrink + ability_label.text assignment in scripts/minion.gd and scripts/spell_card.gd.custom_minimum_size = Vector2(0, 25) + size_flags_vertical = 3 on NameLabel in both scenes/minion_card.tscn and scenes/spell_card.tscn, plus pure-opaque white font_color, pure-opaque black font_outline_color, and outline_size = 8 (was outline 3 with 0.7-alpha black, which bled into light card art)._SCALE_MIN = 0.50 in scripts/minion.gd + scripts/spell_card.gd forced cards ≥ 116 px tall (50% of 232 native), but the shop zone's clip mask is shorter — so the bottom 30% of each card (the NameBand strip) was clipped off-screen. Dropped _SCALE_MIN to 0.25, allowing cards to shrink to fit zone height; name band now visible.scenes/main_board.tscn rebuilt: TopHUD horizontal scoreboard and BottomHUD horizontal control row are gone. Their content (8-player scoreboard, economy, action buttons, sell zone) consolidated into a 240 px vertical RightBar. The play area gets the full vertical (~744 px) for three full-width rows: TopRow (Shop in shop phase / Opponent in combat phase, content swap by visibility), MidRow (Battlefield), BottomRow (Hand). MidZone wrapper that conflated Shop+Opponent overlap is removed; sibling-visibility swap inside TopRow's ZoneContent handles the phase transition.ScoreboardSection (was a horizontal strip across the top of the play area). Same health_bar_panel.gd tile builder — the existing horizontally-flexible tile shape stacks vertically without rewrite. HPGauge component (scenes/components/hp_gauge.tscn) reused as-is.SellDropZone at the bottom of RightBar. Same drag-drop logic in scrap_heap.gd, just lives in the bar's flow. Recovers ~28 k pixels of play-area real estate.scripts/minion.gd _NATIVE_HEIGHT 210 → 232 (matches actual .tscn custom_minimum_size.y — the 22 px discrepancy was distorting every scale calculation). Pivot moved from size / 2 (center) to Vector2(size.x / 2, 0) (top-center) so visible cards anchor flush with HBox top instead of shifting downward by (1-scale) * half_height — this was pushing the bottom past the zone's clip line, hiding the NameBand. New _zone_fit_scale member tracks the computed scale; hover, hover-out, breathing, and _stop_breathing tweens now multiply against it instead of resetting to absolute 1.0. shop_manager.gd fan-in tweens to the same zone-fit target instead of Vector2(1.0, 1.0). spell_card.gd hover captures _hover_base_scale on enter for a matching hover-out target. All combined: cards scale to ~73% in the new ~184 px-tall HBox slots and stay at zone-fit through every state change.scenes/rumble_lobby.tscn: SlotsVBox grid columns = 2 → 4 (8 contestant tiles in 2 rows of 4 instead of 4 rows of 2 — saves 240 px vertical), VBox offset_top = 30 → 48 for breathing room above the "MATCH BRIEFING" eyebrow header. scripts/rumble_lobby.gd: contestant name + role labels gain clip_text = true + OVERRUN_TRIM_ELLIPSIS so long usernames truncate gracefully in the narrower 4-column tiles instead of forcing column expansion.project.godot: restored window/stretch/aspect="keep" (the prior session unstaged-deleted it). Canvas now letterboxes when the window aspect doesn't match 1280:760 instead of stretch-distorting.Vector2(1.0, 1.0) scale, overriding the per-zone _apply_scale_to_zone calculation. Center pivot pushed visible cards downward past the HBox bottom regardless of scale. _NATIVE_HEIGHT = 210 mismatch with the actual 232-tall card meant even when scaling worked, cards were ~10% larger than the formula intended. Fixed in concert with the layout refactor: cards now anchor top-center, scale to fit each zone, and hover/breathing keep the zone-fit scale instead of resetting to 1.0.MainHorizontalLayout has explicit 8 px offsets on all four sides + 8 px separation between PlayArea and RightBar; row stylebox content_margin_top/bottom matches plate_9slice.png's 14 px texture margin so children no longer render on top of the visible metal frame border.letter_spacing = 4 at offset_top = 30 looked clipped at the top of the viewport — bumped to 48.scenes/combat.tscn and scenes/salvage.tscn — standalone scaffold scenes from the prior FUCKIT/8b4dc1c session that never got wired into the game loop. The TopRow phase-swap obsoletes them.FactionSplash overlay in main_board.tscn + the corresponding @onready vars in game_manager.gd. Was unconditionally hidden + skipped on every match start — pure dead UI, ~50 lines of .tscn noise._create_notepad_button() in game_manager.gd. NotesButton now lives directly in main_board.tscn's ActionsPanel with a [connection] entry — same behavior, less indirection.@onready var hand_container declaration in game_manager.gd (declared, never used, pointed at a node that no longer exists).mobile_layout_manager.gd path was simplified to a near-no-op for v1.4.6 because desktop's RightBar already provides the right-column UI consolidation that the prior mobile path created from scratch. Phones get the desktop layout with safe-margin scaling. Full mobile UX polish — tighter card sizing, narrower right bar, optional hand fan — is deferred to a follow-up session.ATK | HP stat pill in faction-tinted Oxanium; top-right faction tag (3-letter abbreviation, faction-color text on dark chip) plus tier mark with grade-colored dot (matte/brass/silver/gold for tier I–III/IV/V/VI) and Roman numeral; bottom name band with faction color2 gradient and Oxanium auto-shrink name (22/19/16 px depending on length). Card slot size grew from 125×165 to 168×252 to accommodate the new layout. Curio variant uses the same shell with a cost pill in place of the stat pill and a CURIO tag instead of a faction tag, plus ability text stacked below the name.ColorRects (Shader/Aura/Holo/Guard) used by status shaders (frostbite, poison, plating, sick, burning, cursed, marked) continue to apply on top of the new frame — combat behavior unchanged.scripts/player_board.gd:163 (Godot 4's max(Variant, Variant) -> Variant static signature was triggering “variable type inferred from Variant” warning — treated as error in this project) and several Variant-flow lines in scripts/minion.gd's rebuilt apply_faction_color() now carry explicit type annotations or as casts so script load is clean.shaders/sick_miasma.gdshader (sickly green miasma + slow vapor wisps), shaders/burning_flame.gdshader (orange-red flame licking up from the bottom edges with fast flicker), shaders/cursed_violet.gdshader (slow violet inward gradient + rotating wisp), shaders/marked_target.gdshader (sharp red crosshair reticle with crosshairs + tick marks + edge red wash). All canvas_item shaders rendered on the existing ShaderOverlay ColorRect — never on the PanelContainer.minion.gd now slots Sick / Burning / Cursed / Marked into the status shader picker between Frostbite and Camouflage. Negative statuses always read over positive ones. Frame border colors muted to match each status (sickly-green / ember / violet / bright-red).combat_engine.gd handles new server events: sick, burning, cursed, marked, status_strip, sick_tick, burning_tick, sick_spread. Each spawns a floating-text damage-number variant and posts a tagged combat-log line ([Sick], [Burning], [Cursed], [Marked]).applied_by arrays added for Burning / Cursed / Marked entries in data/keyword_definitions.json so admin tooling can list which cards apply each.tools/apply-phase5b-ability-text.js for the full rewrite list.)_apply_static_status_visuals() now blends weak per-status tints for Sick / Burning / Cursed / Marked, with the shader doing the dominant visual lifting.rig_007's Marked applier moved from end_of_turn (shop phase) to surge (start of combat). Marked is combat-only, so applying it during the shop phase was a no-op — the engine cleared it before combat started. The +1/+1 EoT buff to the leftmost Rig is unchanged.CanvasLayer (layer 60) and the panel itself sets z_index = 100, guaranteeing it draws above all board content.StyleBoxTexture 9-slice, whose inner area has alpha. Replaced with an opaque dark StyleBoxFlat (97% alpha, warm-amber border, drop shadow) so card art behind it no longer bleeds through.MainVerticalLayout/BoardZone and MainVerticalLayout/HandZone had no explicit z_index, leaving them at the same depth as cards rendered inside their zones. Bumped to z_index = 10 so the labels stay readable.modulate.a = 0.12, bright enough to read through cards. Dropped to 0.05 (texture style) and 0.04 / 0.10 (flat fallback). Slot container also now sets z_index = -1 so cards always draw above the skeleton.apply_status and strip_status in effect-interpreter.js. Cards now apply / strip statuses through standard effect arrays. Frostbite still uses its own action for backwards compat with the Frostbarrow lock interaction.cloneBoard() now initializes is_sick / is_burning / is_marked / is_cursed per unit, plus tick counters for Sick's two-phase decay (_sick_atk_ticks + _sick_hp_ticks) and Cursed's restore amounts (_cursed_atk_loss + _cursed_hp_loss). Cursed values carry over from client-submitted card state because Cursed is persistent.death cause: status event.cleared_by and applied_by arrays for tooling lookups.NetworkManager.current_active_factions stores the list, populated from room_created / room_joined payloads. Programmatically inserted into the existing lobby VBox — no .tscn churn. Requires a fresh Godot export to land on the live web client.active_factions in room_created and room_joined messages so the lobby can display the selection (lobby UI follows in Phase 3b).docs/heroes-rename-art-needs.md classifies all 152 renamed cards into 4 tiers (no change / existing art works / touch-up / new art required). About 105 cards in Tier 3 (new art needed), driven mostly by the six full faction reskins.docs/heroes-syndicate-balance-review.md. Pure research, no code changes. Flags 3 imbalance hotspots (Wharf Heavy unbounded scaling, Floor Boss snowball, Right Hand of the Boss canon-violating coin flip) and recommends narrowing the Syndicate from 6 mechanic families down to 2 pillars (Hoarding + Wager)."spell" type identifier is unchanged — this is a visual rename only. A full code rebrand is deferred to a later phase.docs/heroes-rename-session.md; plain-English summary at docs/heroes-rename-overview.md.data/keyword_definitions.json static name lists for Plating / Frostbite / Guard / Camouflage / Poison applied_by · can_be_granted_by · can_be_stripped_by arrays refreshed to current names. Sandbox starter deck in scripts/setup_automation.gd updated. Doc drift cleaned up across docs/01_factions.md, docs/02_card_roster.md, docs/03_spells.md (and the synced site/data/docs/ mirrors).tools/apply-heroes-rename-phase1.js applies the 151-card rename map to the local data/card_database.json in one pass. tools/push-heroes-renames-to-vps.js is the live counterpart: pushes all 151 renames to the live VPS via PATCH /api/cards/:cardId/rename?draft=true in batch, then POST /api/drafts/publish for a single version-bump. Equivalent to opening the Card Editor and applying every rename by hand, but seconds instead of an hour. Idempotent.api/game-engine.js deleted (was ~3,000 lines / 231 hardcoded card-id switches). api/test-parity.js deleted (entirely a v1↔v2 comparator). The POST /api/combat-snapshots/:id/compare endpoint and the Compare button on Studies were retired with it. v2 (game-engine-v2.js + effect-interpreter.js) is now the only engine. Removed 231 hardcoded card-id references before the rename pass began — meaningful cleanup for future ID-rename work.scenes/minion_card.tscn + scripts/minion.gd got the locked Tin Variations spec applied. Top-left faction chip in faction-color with 3-letter abbreviation (PCK / SYN / RIG / GRV / PAR / THW / HVE / DFT / APO; neutrals are tabless per locked design). Top-right strip of 6 tier dots, brass-tinted on T5+ for high-tier readability. All four corners now carry rivets (was just top two). Subtle warm-amber top edge highlight + dark bottom shadow give the card body proper embossed pressed-tin depth. Drop shadow strengthened (size 14, alpha 0.75, +8 offset) so cards lift cleanly off the board.game_manager._show_phase_banner() was a literal no-op for as long as anyone can remember; now it instantiates themes/fx/phase_title.tscn for every phase transition. Copper-stenciled 96px Teko title with 8px letter-spacing on a consistent warm-dark wash, atmospheric subtitle pulled from a per-phase table (“Power floods the wires.” / “The yard runs red.” / “The Salvage Yard pays its winner in rust.”). Title tint comes from the calling phase color; bg stays consistent across phases for atmosphere coherence.scenes/rumble_lobby.tscn + scripts/rumble_lobby.gd. Dramatic 56px copper Teko title, “◆ MATCH BRIEFING ◆” eyebrow, atmospheric “Eight walk in. One walks out. The Salvage Yard pays its winner in rust.” subtitle. Contestant slots rebuilt as styled 2-column grid of plate-framed tiles — copper P-badge for the human player with green “CONNECTED” status, dim AI-badge with character title underneath, copper slot index on the right edge. Background gets a subtle plate-grunge texture overlay + soft vignette.tools/generate_junkyard_assets.py got a make_plate_9slice() function that produces the 48×48 9-slice plate_9slice.png with a solid warm-dark body, 1px brass perimeter, warm-amber inner highlight at the top, dark inner shadow at the bottom, and a sprinkle of grunge speckle. Replaces an earlier version whose center was transparent — the symptom was tooltip and zone panels letting whatever was underneath bleed through.make_rivet() generates a 7×7 rivet PNG with a 3-stop radial gradient (highlight at 30%/30% → mid → dark) supersampled 16× and downsampled with Lanczos for smooth shading. Rivets read as actual rounded studs instead of flat dots.battle_speed default and saved-settings multiplier both bumped 1.40 → 1.61. Existing saved settings get the speed-up automatically without flipping a slider.SceneTransition autoload swapped from the old generic transition_dissolve.gdshader to the spec-correct themes/transitions/rust_wipe.gdshader (diagonal noise mask dissolve with rust splatter, per HANDOFF §6.10). Affects every SceneTransition.change_scene(...) call — menu → lobby, lobby → board, etc.combat_engine.trigger_all_surges() now scans both boards for live units with the Surge keyword before showing the banner, log line, or signal. If nothing surges, the round flows straight from setup into the combat phase._name / _faction / _atk / _hp / _tier / _keyword on the instance before add_child instead of relying on a post-add set_card + await ready dance. Avoids a dual-refresh where the first pass renders defaults.MatchManager.initialize_game() to lobby-time so factions are determined before the board scene loads. Flagged in code.)NameOverlay node is preserved but not rendered.plate_9slice.png had a transparent center, so anything stretched on it (notably the Card Detail tooltip) let the chips / tier dots / frames of cards underneath bleed through. New texture has a solid interior and the bleed is gone.Panel instances sharing [sub_resource] StyleBoxes via theme inheritance. Workaround in script: pre-populate fields before add_child. Live MinionCard spawn flow is unaffected; themes/junkyard/preview_cards.tscn still reproduces the issue and is filed as a future investigation.z_index = 4 which let them render above the Card Detail tooltip overlay. Removed; tooltip now properly covers them.z_index = -1 so the stat numbers cleanly overlap them when the layout pushes labels toward the corners.themes/junkyard/preview_cards.tscn had theme_override_fonts/font_sizes/font_size = 12 — nested under fonts/ rather than the correct theme_override_font_sizes/font_size. Godot interpreted it as a font override with a numeric value and threw Required object "rp_font" is null. Path corrected.nav.js + nav.css rewritten to emit the same cinematic nav used on the landing hub and /play/* wrappers (logo mark + colored-dot game links + Downloads + online count). The old pill-button nav on Downloads, Account, Changelog, admin tooling, Chronicles chapters, etc. is gone; all pages now share the same look.account.html now uses the Cinzel / Crimson Pro / Inter font stack, atmospheric city background, ember-glow accents. Existing forms (display name, password, delete account) unchanged.<head> so there’s no flash-of-admin-content.<a> to /player/, visually competing with the real floating bar player that has actual prev/play/next controls. The oval is gone everywhere; the bar player stays.nginx gzip_static on was preferring a frozen _play.css.gz from April 23 over the fresh _play.css written by the current deploy, so browsers that accept gzip (i.e. all of them) got yesterday’s layout CSS for hours. deploy.sh now purges any .gz whose source .css/.js/.html is newer across both domain roots before the service restart./opt/rustandroots/api/ with all paths env-driven (RNR_DATA_DIR, RNR_CONTENT_DIR, RNR_ART_DIR, RNR_SOUNDTRACK_DIR). No more hardcoded /opt/600mmr/* assumptions in api/server.js, api/game-engine-v2.js, or api/multiplayer.js. Shared game content consolidated at /opt/rustandroots/content/.PATCH /api/effects/:cardId, PATCH /api/cards/:cardId, PATCH /api/keywords/:keywordName, and PUT /api/tracks now use the requireAdmin middleware consistently (previously had inline Discord-ID checks mixed with middleware-based ones). Admin allowlist remains ADMIN_DISCORD_IDS env var./rnr/* paths on 600mmr.com now return HTTP 410 Gone (JSON) or redirect to rustandroots.io/downloads.html (browser Accept header). Compatibility symlinks at /opt/600mmr/rnr/play/rustandroots-* deleted. Legacy /opt/600mmr/rnr/ and /opt/600mmr/rnr-api/ trees renamed to *.retired-2026-04-24 suffix. A legacy v0.97 APK still trying to hit 600mmr.com will receive a clear retirement message; user must re-download from rustandroots.io./api/chat, /api/tracker/:id/ai-assist, /api/tracker/:id/suggest-actions, fire-and-forget combat log analyzer) gated behind a CLAUDE_ENABLED env var, default off. Calls return HTTP 503 ai_disabled. Code preserved for zero-friction re-enable once API credits are funded again. ANTHROPIC_API_KEY removed from the systemd unit..github/workflows/godot-export.yml triggers on heroes-v*.*.* tag pushes and runs Godot headless export on a self-hosted Windows runner that holds the release keystore. Setup script scripts/setup-gh-runner.ps1 provisions a runner host in one command. Full runbook at docs/17_ci_pipeline.md. No client-side changes.themes/junkyard/ ships plate.tres, 4 button 9-slice StyleBoxTexture variants (normal / hover / pressed / primary), HP gauge component, and a full theme.tres registering Button / Label / Panel / HSlider / CheckButton / LineEdit / Separator / PopupMenu defaults so the cascade reaches everywhere not explicitly overridden inline.scenes/minion_card.tscn + scenes/spell_card.tscn rebuilt with plate texture frame, copper faction bar across the top edge, grunge overlay, top-corner rivets (bottom rivets removed because they obstructed stat numbers), brass ATK + danger HP labels in Teko Bold with outlines. Top-right keyword badge slot (Plating / Frostbite / Poison / Camouflage) so the unit’s dominant intrinsic ability reads at a glance.PhaseOverlay autoload listens to SignalBus and fires the spec-aligned full-screen overlay (Teko 80pt, scale-in + 1.2s hold + scale-out, copper-tinted background) for every phase transition: Surge, Battle (with opponent name as “vs Voltaire” subtitle), Victory, Defeat, Draw, Round N, Eliminated, Champion, Wager. Replaces the old _show_phase_banner + _show_combat_overlay “VS X” system that was duplicating overlays.shaders/transition_dissolve.gdshader rewritten as a Junkyard rust-wipe (copper edges, plate-toned cover instead of pure black, no static-flicker grain). themes/transitions/ additionally includes crt_shutdown.gdshader (Terminal theme variant) and ember_swipe.gdshader (Ruin Glass variant) ready for theme switching when those themes ship. Default transition duration 0.35s → 0.55s for a slower industrial read.themes/theme_manager.gd tracks the active theme (junkyard / terminal / ruin_glass), broadcasts theme_changed on swap, persists choice to user://heroes_ui_theme.cfg. Junkyard is currently the only ready-to-ship theme; d2 Terminal + d3 Ruin Glass slots are placeholders pending design tokens.tools/generate_junkyard_assets.py produces 10 PNGs (button 9-slices, FX overlays, transition mask + ember particle) so any of them can be re-tuned by editing the relevant make_* function and re-running. tools/generate_backgrounds.py regenerates the warm-copper-industrial scrapyard background and the cold-tense combat arena background._refresh_keyword_icon, sized to 18×18 with KEEP_ASPECT scaling so the source 32×32 PNG fits the badge slot.themes/session_b/ ships standalone scenes for Match Results, Run Summary, Settings (with a working theme picker hooked to ThemeManager), Card Detail, and a Junkyard Victory layout (CHAMPION + spotlight beam). Accessible from the new preview hub.themes/preview_hub.tscn — a navigation surface with buttons for theme components, the Heavy Plate card lineup, phase title test, damage number test, transition test, settings, match results, victory, run summary, card detail, plus “Run Game” back to the main menu.battle_speed default bumped from 1.0 → 1.40 (15% retool baseline + 25% follow-up after live playtest). Saved player settings are also multiplied by 1.40 on load so existing players feel the speed-up immediately without flipping the cycler. All combat tween timings, lunge / impact / return durations, and animation arcs scale through this single knob.RUST_ORANGE #C45A2C → copper #b06a3a, WARM_CHARCOAL → plate #1a1612, WARNING_AMBER → brass #d4a54a, BLOOD_RED → danger #c0302a, TOXIC_GREEN → ok #6fa84a. Cascades through every script that references these constants (tooltips, hover glows, splash titles, faction tints, etc.).style_button_primary / secondary / tertiary now load the new btn_*.tres 9-slice resources instead of building flat-color StyleBoxFlats. make_panel_style returns Junkyard plate aesthetic regardless of caller-passed colors. apply_to(root) loads themes/junkyard/theme.tres directly so every scene that calls it picks up the design system._spawn_impact_burst particle count 12 → 6, particle size 12px → 8px, distance range 30-80 → 15-40. _spawn_flash_impact peak scale 0.8 → 0.45. Death pre-shrink expand 1.25 → 1.10. Faction-specific particle bursts untouched.0.30 + edge_bias * 0.45 + break_up * 0.25 (max ~0.78) to 0.10 + edge_bias * 0.30 + break_up * 0.10 (max ~0.55). Edge bias starts later so the center of the card reads through clearly.settings_menu.gd now loads plate.tres for the panel background, delegates buttons to UITheme.style_button_secondary, and clears its own slider / toggle overrides so the theme cascade wins (plate-track + copper fill).GameOverPlate PanelContainer using plate.tres, GAME OVER label in Teko Bold 64pt copper with 5px outline, stats line in taupe.themes/junkyard/plate_9slice.png with neutral modulate so it reads as a Heavy Plate panel instead of generic UI metal.plate.tres shadow_size 20 → 6 and shadow_offset (0,4) → (0,2) so card shadows no longer bleed into the next zone’s plate frame and create a fake section-divider look across the bottom of cards.ui_scrapyard_background.png rebuilt as warm copper-tinted industrial yard with overhead light bloom, scratch lines, vignette. ui_board_background.png rebuilt as cold tense arena with horizontal centerline rim + dust particles + heavy vignette.main_menu.tscn now sets theme = junkyard.tres; ThemeManager registered as autoload; DESIGN button added top-right.MatchManager.resolve_ai_matches() was a deprecated no-op stub left over from the server-cutover (“AI combat is now resolved server-side via game-engine.js”). For offline / single-player Rumble in the Godot editor without a joined room there is no server, so 6 of 7 AI players never lost HP from each other — only when the human personally beat them. Added a local board-strength approximation (sum of attack + health decides winner; damage = sum of winning tiers, min 1) and gated it behind NetworkManager.current_room_code == "" so it can’t double-apply on top of server results in online matches.combat_started while game_manager._show_phase_banner("SURGE PHASE", ...) was firing the old in-game banner at the same time. _show_phase_banner is now a return-only no-op and combat_engine drives the BATTLE overlay manually with the opponent name as the subtitle._show_combat_overlay("VS X", ...) overlay rendering below the new BATTLE phase title is gone. Combat opens with one clean “BATTLE / vs Voltaire” overlay (subtitle now 30pt copper with outline) instead of stacked text.combat_engine._spawn_attack_trail was spawning 3 portrait-sized ghost copies of the attacking card (125×165 each) along the lunge path. Now a no-op. The lunge motion + windup + impact still fires; just no more cardboard-cutout trail._refresh_status_decoration now only mounts the frost overlay (the only true debuff state) and skips the others.Decor (a plain Control) and switched stretch_mode to KEEP_ASPECT so the badge renders at the spec’d 18×18 in the top-right.parent="." attribute, which Godot interpreted as a second orphan root and refused to load the file. Fixed.play_attack_arc / play_hit_flash / play_death methods are still exposed on card.gd + minion.gd for preview scenes and any future generic card use.themes/session_b/match_results.tscn — Live combat outcome flow is tightly integrated with combat_engine’s per-attack loop. Replacing it with the standalone Session B scene would unwind that integration. The Session B scene remains available via the preview hub.main_board.tscn works fine; replacing with a 1.4× pinned card detail panel needs a UX call on hover-vs-pin trigger.Database.get_card_art_texture(card_id) per-card. The handoff flagship PNGs were mock placeholders only.com.rustandroots.game is unchanged so existing installs still auto-update. Version bumped to 1.0.0 across api/server.js, main_menu.gd, update_loader.gd, export_presets.cfg.tools/nginx/ in the repo.site/play/ → site/heroes/ (git mv preserves history). Every hotfix update URL in main_menu.gd points at /heroes/heroes-*. 600mmr.com/rnr/play/*.pck|.zip|.apk will be symlinked to the new paths in Session 2 so legacy APK installs can self-upgrade to 1.0.0 via the old proxy; the first self-update then flips their baked URL to rustandroots.io./play/:game shells for each game with glass title pill, fullscreen toggle (F key), and per-game download panel. Cinzel + Crimson Pro + Inter typography, ember particles, film grain, smoke pulse, parallax drift. User-provided Rust & Roots hero mark now sits behind every page at 28% brightness.heroes-alpha.apk, 335 MB), Heroes Windows full bundle (heroes-windows.zip, 344 MB), and the 311 MB PCK-only hotfix files for each platform. Fresh web export too. Served from rustandroots.io/heroes/heroes-* for new installs. Legacy 600mmr.com/rnr/play/rustandroots-* paths now symlink to the new files, so an existing v0.97 APK self-update will pull the v1.0.0 PCK via the old URL — and once that PCK loads, the game's SERVER_BASE_URL flips to rustandroots.io for every request after. First update walks the user to the new URL, no manual reinstall required.600mmr.com/rnr/, /rnr/chronicles/, /rnr/runner/, etc. now return 404. The only paths under /rnr/ still served are /rnr/api/* (for legacy-APK auth + matchmaking), /rnr/ws (multiplayer WebSocket), and /rnr/play/rustandroots-*.pck|.zip|.apk (symlinked native artifacts for the self-update bootstrap). R&R's public front door is https://rustandroots.io/./assets/brand/logo-runner.png (visible on the hub's Runner banner and the /play/runner shell).GET /api/auth/discord and /api/auth/discord/callback handlers with CSRF state, origin validation against ALLOWED_ORIGINS, and cookie-based sessions. New Discord application “Heroes of Rust & Roots” with redirect URIs for both new domains. Existing Discord users on the legacy 600mmr flow continue to work via the kept-alive /rnr/api/* proxy./api/health now returns SHA256 hashes for each native artifact (heroes-windows.pck, heroes-windows.zip, heroes-android.pck, heroes-alpha.apk). deploy.sh writes a heroes-manifest.json at deploy time. main_menu.gd verifies every download against the advertised hash before installing. Corrupt downloads are refused; clients fall back to the next update strategy.[PLAY] [DUEL] [SETTINGS] [QUIT]) at the bottom-center of the screen. Creative Mode is hidden from the main menu (kept as a node for the admin flow that toggles it). New “Heroes of Rust & Roots” title card serves as the main menu background.scenes/splash_screen.tscn, scripts/splash_screen.gd, and assets/video/opening.ogv (2.3 MB) all deleted. Game launches straight to the main menu.OneDrive/Desktop/rnr2/ (stale February git clone that was silently confusing cross-machine work) and OneDrive/Desktop/RnR/ both deleted. The canonical repo path is C:\Dev\RustAndRoots\ on every machine..png extensions but had JPEG magic bytes. Browsers rendered them fine but Godot’s PNG importer rejected them silently, leaving logo.png with a stale texture and main_menu.tscn throwing “invalid UID” in the debugger. All offending files re-encoded to real PNG.feral_001, bloom_002, etc.), art filenames, and CSS variable names stay the same. Only display names change. Nothing breaks in the card-linking, notepad, or art sync pipelines.rustandroots.io/player/. Background playback with lock screen controls (Media Session API), shuffle, repeat (off/all/one), seek bar, volume control. Like/dislike tracks with localStorage persistence, favorites-only filter, auto-skip disliked tracks. Sleep timer (15/30/60 min). Track feedback sends to Discord. Crossfade transitions between tracks. Deep linking via #track-N. Playlist update detection. Share track links. Dark theme with rust/amber accents, mobile-first design.POST /api/track-feedback lets listeners send one-way feedback per track. Rate-limited (1 per track per minute), fires Discord webhook with track title and message. No auth required.PATCH /api/tracks/:index now supports added_date (ISO date string). Tracks added within 7 days show a "NEW" badge in the player.full_version field for display.useV2 is not defined error that spammed server logs during multiplayer matches._jumper_cables_plating property to MinionCard.effects array. 168 with active effects, 10 keyword-only, 5 tokens.keyword_definitions.json. New Keywords page under Dev menu.dealDamage() function that checks Plating and Immunity in one place, eliminating the source of most recurring bugs.cloneBoard() re-initializes all flags each round.has_guard boolean flag — Guard now has a proper boolean flag like Plating, Poison, and Camouflage. Targeting logic uses the flag instead of checking the keywords array directly.kw.splice() to remove Guard/Plating/Camouflage from the keywords array during combat, making v0.52’s post-combat fix useless. Now all stripping sets boolean flags only.has_guard flag instead.buffUnit() helper — Centralized buff function that handles both combat and permanent bonuses in one call, preventing the “permanent buff is actually temporary” bug class.permanent_*_bonus.temp_plating flag.[MM:SS] match-relative timestamps. Server logs include millisecond precision for forensic analysisMAX_HAND_SIZE constantassets/art/cards/ into site/art/window.rnrNavUser set by nav-authgame_manager.gd (2,985 lines) into four focused modules using the Facade pattern. All public-facing methods remain on game_manager as thin proxies so external scripts are unchangedstart_game message_spawn_floating_text calls that overlapped with damage_number.tscn system. Hits now show one damage number instead of threeadd_child()SceneTransition autoload. Every scene change fades through black (0.3s each way) with input blockingfade_to() and fade_out() for smooth music transitionsFIGHT! 0:40 with live countdown_do_attack() in headless_combat.gd returned bare null on Parasite-Wasp pre-kill path. Now returns proper Dictionary to match function signatureferal_010 — Only triggered as defender. Now also triggers when Grizzly attacks and takes counter-damage. Also fixed false trigger on 0 damage (e.g., 0-attack unit). Logs board-full warning if no space for Cubferal_002 — Only triggered as defender. Now also triggers when Raccoon attacks and takes counter-damage. Removed incorrect survival check (Raccoon can buff allies even if it dies). Added 0-damage guardferal_006 — Headless/JS engines still used pre-v0.12 trigger (Feral death OR Last Stand). Fixed to Last Stand onlyneutral_008 — Headless/JS engines still used pre-v0.12 logic (+2/+2 when kills attacker). Fixed to +1 Atk (this combat) when any friendly killsvolt_002 — Headless/JS engines still chained full attack damage. Fixed to 1 damageneutral_010 — Headless/JS engines still dealt 1 thorn damage. Fixed to 2spell_009 — Discount mechanic was never implemented. Now costs 1 less Scrap for each friendly unit on your boardget_combat_snapshot() now uses .duplicate(true) (deep copy) to prevent shared reference corruption between rounds. Adds diagnostic logging for save/restore unit counts[Board] color tag (dim blue) to colorizerspell_010 — Simplified text to “The highest-Attack enemy sits out the next combat.”game_manager.gd: Added last_atk_dmg / last_def_dmg instance variables, set during damage resolution for use in post-attack triggersheadless_combat.gd: _do_attack() now returns {atk_dmg, def_dmg} dictionary; _trigger_post_attack() accepts damage parametersapi/game-engine.js: Same refactor — doAttack() returns damage values, triggerPostAttack() receives themgenerateShop() now includes guaranteed spell slots (1–2 per shop), matching offline shop generation_spawn_card_from_server_data() now detects spell card IDs and spawns them correctly using spell_sceneEvery card’s database text compared to its code implementation. All 184 units, 13 spells, and 6 tokens verified and corrected.
feral_007 — v0.11.1 moved to summon hook with +2/+2. Card says “Whenever a friendly Feral dies, gain +1 Attack.” Moved back to death reactions with permanent +1 Attackferal_012 — v0.11.1 changed to permanent buff. Card says “for the remainder of combat.” Reverted to combat-only buff, now gives stats to a random friendly Feral (not self)feral_013 — v0.11.1 changed to combat-only. Card says “permanently.” Restored to permanent +2/+2 in token spawn hookvolt_002 — Chain damage was using attacker’s full attack value. Card says “deal 1 damage.” Fixed to 1amp_013 — Shockwave was giving +1/+1 to all Amplifiers. Card says “+3/+1.” Fixed to +3/+1amp_011 — Splash was dealing fixed 1 damage. Card says “half its Attack damage.” Now deals max(1, attack/2)neutral_010 — Was dealing 1 thorn damage via direct health subtraction. Card says “takes 2 damage.” Fixed to 2 damage via take_damage()volt_015 — Was only triggering on its own attacks. Card says “Whenever a friendly Volt attacks.” Now triggers for any friendly Volt attack when this unit is on boardneutral_008 — Was only triggering when it killed the attacker, giving +2/+2. Card says “When a friendly unit destroys an enemy, +1 Attack for this combat.” Completely rewrittenferal_006 — Was triggering on any Feral death OR Last Stand death. Card says “friendly unit with Last Stand dies.” Narrowed to Last Stand onlysyndicate_007 — Could trigger unlimited times per combat. Card says “Limit once per combat.” Added per-unit trackingbloom_012 — Attack sap only reduced current attack (lost on restore). Card says “permanently loses 1 Attack.” Now reduces base_attack tooferal_009 — Was using direct health subtraction. Now uses take_damage(1) so Plating, damage reduction, and death triggers fire correctlyvolt_001 — Was targeting a random enemy. Card says “deal 1 damage to the enemy directly opposite.” Now targets by board indexvolt_006 — Surge set no flag; cleave never happened. Card says “gain Cleave for this combat.” Now sets cleave flag; attacks deal full damage to enemies adjacent to targetferal_015 — Same issue as Arc-Welder. Surge now sets cleave flag for adjacency damageamp_010 — Surge only printed a log message. Card says “adjacent friendly units attack a random enemy.” Now actually triggers those attacksrig_008 — Overdrive only printed a log message. Card says “deal 2 damage to a random enemy.” Now actually deals 2 damagesyndicate_008 — Wager coin-flip never deducted Scrap. Card says “Wager 2 Scrap.” Now deducts 2 Scrap before the flipalch_005 — Was applying Poison to itself. Card says “give a random friendly unit Poison.” Now targets a random allyalch_015 — Mega-Brew combine produced a brew with no stats. Now sums attack/health from all brews into a single Mega-Brew with combined statsalch_014 — Plague damage counter accumulated but never fired. Card says “deal 2 damage per brew to random enemies at combat start.” Now deals queued plague damageneutral_017 — Upgrade reward only gave 1 free roll. Card says “gain 3 free rolls.” Now grants 3bloom_016 — Was only absorbing attack from killed targets. Card says “absorb its Attack and Health.” Now absorbs bothglacier_007 — Frostbite Last Stand was targeting up to 3 random enemies. Card says “adjacent enemies.” Now targets adjacent by indexswarm_015 — Was spawning random Swarm units. Card says “summon 2 copies of the first friendly unit that died.” Now tracks and copies first deathswarm_009 — Was not in token spawn hooks. Card says “+1/+1 permanently when a friendly unit Hatches.” Added to spawn hookspell_008 — Was giving a flat +1/+1 buff. Card says “give an Amplifier Aura: adjacent units gain +1/+1.” Now grants actual Aura keyword with adjacency buffspell_003 — 50% miss chance only applied to the first enemy that attacked. Card says “all enemies.” Each enemy now independently has a 50% miss chance on first attackscanBareNames() matches all 165+ card names by word boundary without needing [bracket] syntax. Logs page calls processCardNamesRaw() to linkify every card name in combat log detail views<textarea> was skipped by the card-link TreeWalkerscanBareNames() and public processCardNamesRaw() API for bare card name matchingis_online flag. All offline features (drag-and-drop, hand zone, freeze, roll, upgrade, combat animations, tooltips, auras) work identically online[Card Name] as interactive hover-popup links/api/auth/me fetch for reliable username resolutionisYou comparisons now use discord_id instead of display nameslastByeId trackingmain_board.tscn instead of deprecated online_game.tscn/api/auth/me directly for guaranteed correct display name[Card Name] into the play page's Testing Notepad. The linked name renders as a hover-popup when submitted to the trackerRUST-XXXX code and play head-to-head against another player in real-timemultiplayer.jsGET /api/stats/:id for player stats, GET /api/stats for leaderboard, GET /api/match-history/:id for recent matcheshttp.createServer() to share the HTTP server with WebSocket connections on port 3847[ in any text input on the site to get a dropdown of matching card names. Filter as you type, select with arrow keys/Enter/Tab, or click. Inserts [Card Name] which renders as a hover-popup linknav-auth.js adds a Discord login button (or user badge when logged in) to the navigation on every page. Logged-in users see avatar, name, and a dropdown with logoutinitialize_game() now accepts a config dictionary for custom starting values[Card Name] text renders as styled hover-popup links across the entire site. Hover shows card art, stats, and QA status. Click navigates to the Compendium entrycard_status database tableloan_shark_debt negative, but round-start only processed positive debt. Added elif branch to pay out bonus Scrapexecute_deployment() and emits unit_deployed signaloverdrive_used flag per combat, preventing infinite Plating refresh loopclip_contents from hiding cardsend_combat_phaseclip_text and text overrun behavior to prevent layout overflowtemp_buff() changed to buff() so stat gains survive between roundsclip_contents to stay within their allocated spaceupdate_stats_ui() after applying Frostbite, showing the icy blue tint immediatelyupdate_stats_ui() calls after every Surge ability that modifies status flags: Hailstorm Caster, Ice-Age Mammoth, Scrap-Cannon Walker, EMP-Grenadier, Peacekeeper, Casino Leviathan, Jumper Cables, Golden Elixircontinue statement in server.js (outside loop context)expand to keepamp_008; corrected to amp_007rig_001 and feral_003 to headless combat Overdriveworkshop_level + 1 tier unitsbloom_008, bloom_011, bloom_015 now actually apply stat buffs to other Blooms (previously only logged)/images/edits endpoint, feeding the locked anchor (parent / shipped / character-portrait art) to Grok as a source image. New art is conditioned on what already exists instead of guessed from text — "another Doyle that matches the shipped Doyle" actually works now. Generations with no reference still use the text-to-image path.rustandroots.dev/chronicles-art-commission.html, behind the same Omega + Supe gate as the Heroes art commission tool. Same xAI Grok Imagine integration (grok-imagine-image-pro at $0.07/img), same voting + auto-finalize pipeline, same solo-mode env flag — sibling tool, parallel API surface (/api/chronicles-art-commission/*).data/chronicles-art-commission.json — the hand-curated source of truth. Adds three concepts the Heroes registry doesn't have: characters{} (canonical descriptions for every named character that appears in art, with optional anchor attempts), scene_groups{} (a location rendered from a fixed camera, shared by multiple variants), and per-asset parent_id + mood_state + characters_present. Mood variants of the same scene/portrait inherit the parent's subject_block and the scene_group's framing, so the same room stays the same room across variants.final_path_relative exists on disk (io / dev / repo-site, with .jpg fallback for the ch06/ch07 mixed-extension assets) and surfaces shipped + shipped_url on the response. Tiles render the canonical image directly via nginx with a gold SHIPPED pill (no center overlay — you should SEE the art so you don't drift). The detail-view canvas does the same with a SHIPPED · filename marker./opt/rustandroots/io/{final_path_relative} AND /opt/rustandroots/dev/{final_path_relative} so chapter HTML's relative art/chXX/*.png references resolve on both subdomains with no deploy step in between.docs/chronicles-art-commission-tool.md — setup, schema editing, SHIPPED-state semantics, FAQ.xaiImage.generate() with the wrong arg shape — would have thrown on every gen call. Corrected to generateImage(prompt, {model}) with a transcoding pass through ensurePngBuffer() so JPEG bytes returned by Grok land on disk as real PNG.data/chronicles-art-commission.json to /opt/rustandroots/content/ alongside the Heroes equivalent. Previously the server would silently fall back to a path that didn't exist on the VPS.signal-gives-coordinates node — the Signal’s “then disclose one thing” branch off secrets-1. Signal frames its disclosure as protective-curator (not the old “quietly amused” posture), shares the address, and notes that “most people will tell you the address is abandoned. Most people will be wrong.” This is the new Ch12 unlock gate.gin + eli — Gin in soft sepia Crimson Pro (#b89868); Eli in muted bone Crimson Pro with subtle letter-spacing: 0.2px for the slightly cramped feel (#9a9088). Solomon’s existing Teko gold (#c8a84a) preserved for his Ch12 lines.chapter-complete: ch10 + node-visited: ch10/signal-gives-coordinates. The discovery path runs through the Signal, not through the frames thread. The 6/6 frames thread now feeds the Frame 87 examine inside Ch12; it no longer gates the chapter itself.site/chronicles/ARC_ONE_NARRATIVE.md.gatedDescriptions rewritten in quiet Chronicler voice — the post-Ch12 deeper text on the frame examines in Ch01 (Maren’s shelf), Ch03 (Doyle’s crooked frame), Ch04 (Solomon’s desk), Ch06 (the Cathedral vestry), Ch09 (Breck’s bent-frame straightedge), and Ch11 (Avra’s control sample) had been written in a thesis voice that invented canon — “the mechanism placed it on Day Zero,” “the image was issued, not selected,” “Frame 87’s degradation is the mechanism wearing through,” “a performance written for him by something he has never named.” All replaced with observational language that notes the pattern without naming the cause. The recognition is preserved; the speculation is removed.data/chronicles-art-commission.json — Solomon Cross: mid-60s → 53; Thomas Alder: 40s → 60s (with silver-grey hair and close-cropped silver-grey beard); Gin: mid-60s → 59; Eli: 33 → mid-40s.physical_anchor filled on 23 characters — 5-trait comma-separated visual cues (Maren through Eli, plus the Ch05–Ch11 supporting cast). The schema v2.1 field is no longer empty; the prompt builder appends these to every generation that names the character, lifting cross-asset character likeness consistency.site/chronicles-art-commission.html bound an addEventListener on #char-editor-overlay at script parse time, before the overlay <div> existed in the DOM. The null.addEventListener threw a TypeError, aborted the script, and bootstrap() never ran — so the page sat on its initial “Checking…” text whether the API returned 401 or 200. Listener wiring now defers to DOMContentLoaded with defensive null checks.dialogue-phase (default, talking with Avra), trial-phase (notebook fills as Colm speaks), post-erasure (Avra and her creations purge from the scene via [data-avra="true"] hiding + lab-empty.png art swap).#b0a090). Avra's existing rose tone (#c07a8a) preserved.research-3 beat (the molecular-lock explanation) now retroactively unlocks the previously-orphan gates in Ch06 Dara (What if the Event is still happening?), Ch07 Nessa (An Apothecary has made a compound that thins the memory barrier), and Ch08 Issa (Avra has proof. The Event was done to us.). The gates existed; they referenced a Ch11 node that didn't exist. Now they do.ch11/avra-barrier, a node that did not exist in any version of Ch11. They are now gated on ch11/research-3, which is the new chapter's framework-reveal node and exists in the dialogue tree.rustandroots.io/play/chronicles.html dropped its top title block (“THE INTERACTIVE NOVEL / Rust & Roots Chronicles / tagline”). Back and Fullscreen now live in a narrow right-side sidebar; the iframe grows to its full 16:9 aspect. The hub + chapter content fit above the fold on desktop viewports.QUERY: DRIFT_NODE_INTERACTION (gated on Ch05 sevi-task-2 — Sevi reveals the tones / home is key) reveals the Signal’s DRIFT-1 through DRIFT-7 surveillance log, including DRIFT-7’s 11:43-minute listening window matching the Day 0 silence. QUERY: GOSS (gated on Ch09 breck-wrench) reveals the only flagged death in 30 years and 4,147 deaths catalogued, plus the unidentified frequency captured four seconds before the killing blow. QUERY: GROVE_ANOMALY (gated on Ch08 issa-grove-1) reveals the Grove’s 340m mycelial mat extending under the Syndicate’s commercial district and the Signal’s read of the Grove as a parallel networked intelligence. QUERY: HIVE_FREQUENCY (gated on Ch07 nessa-dissolution) reveals the 0.4 Hz harmonically rich waveform on the eastern margin matching the Signal’s own multi-node coordinated processing.walls deeper-examine text and Ch09’s “The Signal flagged his death. The first one it flagged.” choice were both gated on Ch10 nodes (drift-construction and signal-first-flag) that did not exist anywhere in the codebase. Both gates referenced phantom IDs and could never trigger. The new Ch10 nodes use those exact IDs, so the long-standing dead links now resolve.chronicles-progress.js GATED_DISCOVERIES registry gains a 'ch10' block listing the four new gated queries, so the hub flags Ch10 with a “new content available” pulse and adjusts its discovery percentage when the player completes Ch05/07/08/09 source nodes.drift-construction, signal-first-flag, signal-first-flag-2, grove-anomaly, grove-anomaly-2, hive-frequency) added to discoveries.dialogueNodes. Discovery % recalculates downward on first revisit until the player works through the new branches.VOICE_STYLES in chronicles-effects.js.breck-wrench to already-shipped Ch08 Hadley receiver gates on Goss and the tournament, plus a walk-home / tuesday-nod forward to Ch02 Thomas and a tool-wall examine forward to Ch12 (wedding-frame seed on the leftmost tool-rack post).water-station.png (Ch08, Grove oil + Rigs charcoal) and extended across Ch09 in broken-wall.png (Rigs charcoal + Pack woodcut meeting at the wall itself) and plaza.png (primarily Rigs charcoal with Parish stained-glass halation accents in the crowd, Pack woodcut at the right-edge treeline).site/chronicles/CH09_DIALOGUE_SCRIPT.md and site/chronicles/CH09_ART_GEN.md) — the pre-port screenplay with full dialogue/mood effects/cross-chapter gating, and the canonical art-generation reference recording faction visual canon decisions for future chapters to inherit.CONTINUITY_BIBLE.md: no antler / horn / skull-mask / totem iconography..fx-drift CSS animation. Rigs is specifically oily-charcoal / Käthe Kollwitz industrial-worker register. Canon now explicit.baseDiscoveries for Ch09 — Set to 56 (52 dialogue nodes + 4 examine items).shop-cooling.png variant when mood reaches open. The Grove’s tidal mood pattern (Ch08) becomes the Rigs’ linear arc here — Breck moves forward through her moods, she does not return to baseline.VOICE_STYLES in chronicles-effects.js; Issa was already registered from Ch07 prep.issa-grove-1 and issa-soil to already-shipped Ch06 and Ch07 receiver gates.Chronicles.Core.on(’dialogue’) handle the three scene changes (garden ↔ water station ↔ clearing ↔ boundary). Returning to the garden re-applies Issa’s current mood variant via Chronicles.Core.getMood(’issa’), so the art always reflects the emotional state the player has earned.site/chronicles/CH08_DIALOGUE_SCRIPT.md) — The pre-port screenplay: full line-by-line dialogue, mood effects, gating, canonical corrections against the manuscript. Lists the baseline’s canon errors that this ship corrects.site/chronicles/CH08_ART_GEN.md) — Per-asset prompts for the nine images in the baroque-expressionist mode, plus a coherence-check table ensuring the three religious-chapter art languages (Parish / Hive / Grove) stay visually distinct.70% 30% with .narrative-panel { display: none } (the corrected pattern from the 2026-04-21 Ch06/07 hotfix), not the baseline’s 1fr 400px two-column which left no room for the art panel. .char-portrait, .msg-portrait-img, and .found-area CSS are present from the start — no need for a follow-up fix commit.rooted (default, the calm that is attunement), attentive (a guest has been noted), expressive (the rare permission to speak the private things — Pell’s trace, the barrier). The chapter opens rooted, rises to attentive around the water station, returns to rooted in the clearing, and ends attentive or expressive at the boundary depending on the player’s choices. The Grove does not escalate; the garden does not permit the grievance escalation would require.baseDiscoveries for Ch08 — Set to 73 (69 dialogue nodes + 4 examine items). Ch05’s missing baseDiscoveries remains a separate tension; not touched in this ship.chronicles-effects.js VOICE_STYLES (with a manual NAME_PATTERNS regex for the “Bram” diminutive), all bramble-* dialogue nodes, scene-swap handlers, discovery arrays, portrait filename, continuity bible, and this changelog.chronicles-effects.js with lavender-grey color tuned to sit beside Nessa\u2019s deeper violet without competing.nessa-dissolution to Ch06 and Ch10.VOICE_STYLES in chronicles-effects.js with distinct amber-palette colors.GATED_DISCOVERIES entries.DEVELOPMENT_PLAN.md, ENHANCEMENT_PLAYBOOK.md, CH06-12_BUILD_MAP.md, and MASTER_SUMMARY.md. Plus two decision docs: FRAME_CANON_REVIEW.md and CH11_BRIEFING.md.manuscript/chapters/The_Good_Shepherd.md.baseDiscoveries for Ch06 — Set to 69 (64 dialogue nodes + 5 examine items), enabling the adjusted-discovery-percentage math for retroactive unlocks to fire correctly once the chapter is complete.rustandroots.io/play/runner.html dropped its top title block. Back and Fullscreen moved into a narrow right-side sidebar alongside a short “Web only” PWA hint. The iframe now claims the full 16:9 aspect within the wrapper.staminaRestorePerRest benefits on all 8 safehouses and "Stamina −N" text on pit-fight choices were still in the data. Cleaned up to match the current HP-only model./runner/changelog.html has been removed. RUNNER version history lives in the RUNNER tab of the main changelog.Stash Vault meta upgrade now doubles vault capacity from 3,000₡ to 6,000₡ instead of the old carryover bonus.final cash + (days survived × 50) rewards both wealth and longevity. Previously only Daily Challenge scores were recorded.Max-Age), forcing re-authentication every time the PWA was closed. Auth token is now also persisted to localStorage and validated by JWT expiry on return visits — no re-login required for up to 30 days.vice: true flag: Frostline Brew (Thaw, heat 5), Apothecary Pills (Alchemists, heat 6), Grove Smokes (Grove, heat 4), and Drift-Touched Wire (Drift, heat 8). Vice commodities are source-locked — each can only be purchased at 2 specific faction districts (e.g., Smokes only at Greenstrip & Yards). At all other districts the Buy button shows "Not sourced" and is disabled; you can only sell. Vice rows get a deep-purple VICE badge. Checkpoint events scale heat by +5 per vice unit carried (up to +30), even when bypassed with False Papers (though Papers reduce it). Arriving at the Cathedral District while carrying vice triggers an automatic +10 heat & −1 Parish standing with a slide-down warning toast.state.stats.signets. One can be equipped per run for a passive bonus. Earned via:
SWEEP_POOL in data.js: Fuel Drought, Harvest Glut, Coin Crackdown, Silk Drought, Crystal Freeze, Ration Surplus, Med Shortage, Scrap Glut. Every new run seeds one sweep on day 1. Each day there is a 35% chance a new sweep fires (max 2 active at once); expired sweeps are pruned at day advance. A ⚑ SCARCE (red) or ⚑ GLUT (green) badge appears on affected commodity rows. District travel buttons show a small ⚑ icon when any sweep is active there. Slide-down toast fires on each new sweep: “⚑ Fuel Drought sweeping 2 districts”..mood-badge pill shows in the district panel when you are there. Unvisited districts hide their mood unless Whisper Network is owned.charges/max) in the fence and can be re-bought up to their maximum. A new 🎒 Use Item button appears in the district panel whenever you have charges. Scrap Car adds a 🚗 Drive button (disabled after use).
DISTRICT_RELATIONS table in data.js drives the adjacency/rival graph: the Pack are tight with the Sprawl and the Hollow but loathe Gilded Row and the Cathedral; the Drift are at war with the Cathedral, Gilded Row, and Frostline; the Hive sits comfortably between Gilded Row and the Sprawl. Daily challenge runs roll their starting standings under the seeded RNG so every player gets the exact same opening on the same calendar day. The same-shaped opening was the #1 staleness complaint — runs should feel different from minute one now.state.stats.factionStanding, which meant a single bad run could poison the next ten openings. freshRun() now calls a new rollStartingStandings() helper for every mode that wipes the standings table to zero, then immediately re-rolls the randomized opening from above. Carryforward invariant locked in code, not just docs.computeDebtOwed() helper so the principal stays clean in state. A new state.run.debt field tracks { principal, takenOnDay, dueDay, faction, escalated }. Normal-mode loans are due on day 14; Endless-mode loans get a day 30 ceiling. Past due, the Banker takes a one-time +25 heat shock on the first overdue day (with a slide-down toast: "Your loan is past due. The Banker will not forget.") and then a quiet +5 heat tax every day after that, on top of the still-compounding principal. Repaying in full clears the debt and grants +5 Syndicate goodwill for paying clean..debt-badge chip joins the lookout badge on the right side of the top bar whenever state.run.debt is non-null. Shows the live owed amount (recomputed every render off the compounding helper) and the due day (e.g. 💰 142 ‰ owed · day 14). When the loan goes overdue the badge swaps to a red OVERDUE variant with a soft pulsing red box-shadow so the player can't miss it. Tooltip nudges the player to "visit Gilded Row to repay."applyDayAdvance() — A new applyDebtDay() hook fires alongside applyLivingCosts() and applySafehouseDay() at every day flip. Handles the first-time overdue heat shock, the per-day heat tax thereafter, and the log/ticker/toast bookkeeping. Past-due interest still compounds because computeDebtOwed() reads the elapsed-days math directly from state.run.day instead of accruing into a stored field.actions[] field on DISTRICTS entries in data.js, rendered as new buttons under the existing fence button on the district panel using the existing .fence-btn class (no new CSS patterns). A new runDistrictAction() dispatcher in runner.js looks up the handler name and calls the matching modal. Districts that already had fences (Sprawl · Geargrinder's Yard, Hive Market · Whisper Stall) still satisfy the audit through their fence; districts that had nothing now have a destination of their own:
state.run.usedActions map). The cold counted you..fence-btn styling so the layout doesn't shift on either width.CACHE_NAME in site/sw.js bumped to rnr-202604101100 so returning players get the new modals, action buttons, and HUD badge without a force-refresh.endReason: 'starved'). Both bars are clickable — tap for a full breakdown of what drains them and how to recover.destructibleBy faction flag for a future burn-it-down mechanic. The Yards shack is cheap (450 ‰, 18 ‰/day, 20 cargo) and Pack-destructible; the Gilded tenant room is expensive (2500 ‰, 60 ‰/day) with a daily Syndicate goodwill drip. Greenstrip's greenhouse has the best stamina regen, Frostline's boarded unit has preservedNoSpoil, the Cathedral cell has the best HP restore plus Parish goodwill drip, the Hive market room gives a 5% global vendor discount, and the Hollow room has a daily drift blessing. Owning one unlocks a Rest at … button on the district panel that advances a day without travel.endReason: 'foreclosed'). Lockers have 2 grace days before the keeper takes what you left inside.rollEventChance() — The flat 55% travel roll is gone. Early-game (day 1–7) rolls a 75% event base, mid-game (8–20) 60%, late-game (21+) 55%. Add +3% per faction at standing ≤ −5 (hostile amplifies), add +1% per heat-mid point of the destination, and hard-cap the result at 95%. Soft-force cap: if you've travelled 5 times in a row with no event, the next travel is guaranteed to fire one. state.run.travelsSinceEvent tracks the streak.state.run.ticker, max 6 buffered, 3 visible). The bottom log panel still exists untouched — the ticker adds, doesn't replace. Fixes the long-standing feedback that the bottom log is "too out of the way."state.stats.seenTutorials. The very first time profit→enmity drops a player's standing, a gold-trimmed "They Keep A Ledger" tutorial modal pauses the game and explains the system (what just happened, why, how to recover, and a pointer at the clickable pills). It never fires again. Subsequent drops use the ticker + slide-down toast path.STAMINA_CONFIG and HP_CONFIG.empty_road. Four bad, three good, three neutral, two weird.
runner-YYYY-MM-DD) derived from the date; everyone starts in the same district, with the same market pulse, the same per-district price jitter, and the same initial prices. After that, player choices branch the run. Finished daily runs submit to a new server leaderboard. Click the Daily Challenge button on the menu to see today's top 10 with a Start today's challenge button. The game HUD shows a DAILY gold badge during an active daily run. Daily runs skip stash carryover so everyone starts equal.Math.random monkey-patch) for the entire lifetime of a daily challenge run. Seeded from a string hash so the same runner-2026-04-09 seed produces the same sequence on every machine. Restored to native on run end. Regular runs are unaffected.runner_daily_runs SQLite table with a unique index on (user_id, seed_id). Three new routes: GET /api/runner/daily-seed returns today's UTC seed string, POST /api/runner/daily-runs submits a finished run (auth required, keeps best score per user per day), and GET /api/runner/daily-leaderboard?limit=N&seedId=... returns the top N rows sorted by final cash then days survived.buy() so the stamina markup flows through the max-affordable and actual-cash-deduction math in one pass (no double-dip, no rounding bugs).<span> to <button> — Same visual language, hover lift, and now they open the standing breakdown modal when clicked. No regressions to the existing layout.state.stats.seenTutorials is on the account-level stats object so a player only ever sees each tutorial once, not once per run./api/runner/stats and /api/runner/mail because the Runner's fetch calls only sent Authorization: Bearer, not the X-User-Info header the main-site auth middleware requires for Discord tokens. Added an authedFetch() wrapper that fetches the current user from /api/auth/me once on boot and attaches both headers on every authenticated call.v0.4.1 tag at the bottom of the menu card so you always know what build you're on.heatRange and only revealed after travel, not before. Each district's mood (Quiet · Calm · Tense · Hostile · Volatile) is shown as a soft signal derived from the heat range midpoint./runner/ in standalone mode. Web-only distribution gets a real install path./runner/ in any social app now previews with the hero image, a proper title, and a description.DEPLOY_SSH_KEY secret was never set. Now it skips cleanly with a notice-level log instead of a hard failure. If the secret is added later, auto-deploy just starts working with no further changes.#c0302a) for better readability at small sizes.buy() now tracks a running average per commodity, used by sell() to compute net gain for profit→enmity. Not visible to the player; feeds the new system./play/ while the engine loads. Auto-dismisses on a godot-ready postMessage (reserved for a future Godot bridge), 10 seconds after iframe load, or via a manual Minimize × button./runner/ — nav.js was not treating the runner directory as a subdirectory, so Chronicles / Play / etc. resolved relative to /runner/ and 404'd. Fixed the isSubdir detection.rustandroots.io/runner/. 14-day trading loop across 8 faction-themed districts, 8 commodities, 12 travel events, heat/bust system, persistent stash and faction standings.runner_stats table on the API. GET/POST /api/runner/stats endpoints merge progress across devices (monotonic counters, Discord-auth'd).