• v3.9.2 6b0440acc9

    v3.9.2
    All checks were successful
    CI / build-and-test (push) Successful in 1m53s
    Stable

    joeyr released this 2026-07-29 18:17:03 -05:00 | 59 commits to main since this release

    Fixed

    • Champion display names stopped being unique, and it silently cost 35% of the
      roster its stats.
      Patch 16.15 shipped the Classic Rift mode (map 453, queue
      4300) and with it 60 alternate champion entries in the client's
      champion-summary.json — alias Jade_<Base>, id 60000 + base id. Every one
      carries its base champion's display name, so "Garen" now names both id 86 and
      id 60086.

      ChampionNameResolver built its name→id maps last-write-wins, and the asset is
      listed id-ascending, so the classic id always landed last and won. All 60 shared
      names resolved to a 60xxx id — Ahri, Annie, Garen, Lux, Teemo, Wukong, Master
      Yi, Miss Fortune, Nunu & Willump and 51 more. Nothing in the published artifacts
      is keyed by those ids, so the in-game overlay, the scouting report, and both
      op.gg adapters quietly lost benchmarks, damage splits, and counters for a third
      of the roster, with no error surfaced.

      Champ select was never affected: it passes numeric championId straight
      through, and ids stayed unique throughout. Only name→id resolution was
      ambiguous.

    Changed

    • Name→id resolution now takes the roster it means, because breaking the tie
      the obvious way would have been worse.
      The Live Client Data API identifies
      participants by display name, so simply preferring the lowest id would have
      fixed normal games and started answering classic ones with live-roster stats.
      Jade Annie has 460 HP and 550 attack range against live Annie's 560 and 625, on
      a reworked kit — that is a confident wrong number where an unresolved id
      honestly renders "—", and showing nothing is the recoverable failure.

      So the lookups are split per roster and the in-game poller identifies the game
      before asking. rawChampionName is the primary signal, being per-participant
      and exact; mapName is the fallback. gameMode is deliberately ignored —
      ordinary Summoner's Rift reports the literal string "CLASSIC", so it
      identifies the wrong thing. Anything unidentified stays live, which is both
      the safe default and the overwhelmingly common case, and the verdict is logged
      once per game (live.roster_detected) with the raw strings beside it so a real
      classic game confirms the mapName spelling.

      op.gg adapters stay pinned to the live roster: op.gg publishes no classic pages,
      so a classic subject 404s to "no data" until it does.

    Downloads
  • v3.9.1 d5aca47349

    v3.9.1 — scouting report: team damage rollup only
    All checks were successful
    CI / build-and-test (push) Successful in 2m18s
    Stable

    joeyr released this 2026-07-28 22:39:20 -05:00 | 61 commits to main since this release

    Changed

    • The scouting report shows the team damage rollup only — the per-champion bar
      is gone.
      3.9.1 reverses 3.9.0 on this point. That release dropped the bars
      from the champ-select board but deliberately kept them on the report, on the
      reasoning that "one player is the point" there. In use that turned out to be
      wrong: a champion's damage split is a property of its kit, so the bar reads the
      same on the card as it does folded into the column rollup above it, and the
      report is the surface where that costs most — it is read mid-game under time
      pressure, and ten repetitions of a fixed number were pushing apart the rows
      that genuinely differ per player (rank, mastery, on-champ form).

      Both surfaces now follow one rule: damage is a team question, answered once per
      column.

      ChampionDamageBar and splitTooltip are deleted rather than left unused —
      neither surface renders per-champion splits now, and the coverage they had
      (thin sample, previous-patch fallback, off-patch aggregate) is asserted on
      profileTooltip, which is what the rollups actually use.

      No mid-game refinement is possible here for now: Riot's Live Client Data API
      exposes no damage figures for any player (scores carries only kills, deaths,
      assists, creep score and ward score), so the rollup stays kit-derived until the
      post-game match data lands.

    Fixed

    • A test that passed on the day it was written and failed every day after.
      stats-provider-artifact.test.ts hardcoded a fixture fetchedAt while leaving
      the provider on the real clock. The provider persists the payload's own
      fetchedAt rather than a write timestamp, so once that literal aged past
      cacheTtlHours the cached entry stopped counting as fresh and a test asserting
      a cache hit began asserting against a genuine cache miss. The clock is now
      pinned to the fixture. Test-only — the provider's lookup order was never wrong.
    Downloads
  • v3.9.0 cdcadff8be

    v3.9.0
    All checks were successful
    CI / build-and-test (push) Successful in 1m47s
    Stable

    joeyr released this 2026-07-27 20:51:18 -05:00 | 62 commits to main since this release

    Changed

    • Rune pages now come from the nightly crawler instead of being scraped during
      champ select.
      Recommended runes were the last thing this app fetched from
      op.gg at the moment it could least afford to: lock-in gives you about thirty
      seconds, the request queued behind stats and tier-list fetches on the shared
      rate limiter, and the result was cached in memory only — so every restart
      re-scraped every champion you had looked at.

      lol-companion-data now crawls those same pages nightly, off the same
      tier-list index it already fetches for counters, and publishes
      data/champion-runes.json. Lock-in reads it out of memory.

      op.gg stays wired as the fallback and nothing about the feature changes when
      it is used.
      A miss for any reason — the artifact not fetched yet, a patch
      mismatch, a champion op.gg does not list at that role — falls straight through
      to the scraper, exactly as before. The artifact is refused off-patch
      (servesOffPatch: false), because a recommended build is a claim about
      this patch's meta and the crawler is a night behind on patch-drop morning.

      The published file stores board rows as bare perk ids against a shared perk
      dictionary rather than repeating names and icon URLs on every option: measured
      over the same data, 212 KB against 1.9 MB. Which rune each row picked is
      derived positionally from selectedPerkIds — deliberately not by testing
      membership, because stat shard ids repeat across rows (5008 in rows 1 and 2,
      5001 in rows 2 and 3) and a membership test would highlight two shards where
      the page picked one.

      Perk hover descriptions are unaffected. They come from the running client's
      perk catalog over the LCU, which a headless crawler has no access to, so they
      are still attached on this side for both paths.

    • The champ-select board no longer tells you what to build. Each team's
      damage rollup used to end in an instruction — "Mostly AP → build MR". The same
      rollup renders for both columns, and that advice is only ever right about one
      of them: under YOUR team it read as "your team is AP-heavy, so build MR",
      which is backwards. The headline now names the composition and stops there
      ("Mostly AP"), which is true of whichever column you are reading. The scouting
      report's rollups carried the same line and lost it for the same reason.

    • Enemy lane dropdowns state the lane outright. They used to be prefixed
      with their provenance — "Guess · Jungle", "Auto · Support" — which spent the
      widest label in the row on a qualifier, clipped under the dropdown arrow at
      the longest lanes, and told the reader something the row's position already
      told them. The dropdown now reads "Jungle". The lane it defaults to is also
      dropped from the list below it, so the open menu no longer offers the same
      lane twice.

    • "Support" is now "Utility" on the board, matching the role name the ally
      column has always shown. The off-role tier tag follows (SUP → UTIL). Other
      surfaces are untouched: the settings panel and the rune/item import headings
      still say Support, and imported item-set names (LoLC: Lux Support) keep
      theirs so already-imported sets are not orphaned in the client.

    Removed

    • Per-champion damage bars on the champ-select board. Ten of them, one per
      cell, restated in ten thin bars what the two team rollups above them already
      say in words — and the team number is the one a player actually itemises
      against. The rollups stay. The per-player bar is unchanged on the scouting
      report, where a single player's profile is the point of the surface.

    Fixed

    • The two team columns line up row for row again. The ally column renders
      its role as plain text and the enemy column as a dropdown, which is several
      pixels taller, so the two sides drifted further apart with every row — by the
      middle lane they were visibly offset, in a layout whose whole purpose is
      reading TOP against TOP. Both sides now reserve the same height for that line,
      and a cell with no tier badge (an unfilled lane, or a champion op.gg has no
      data for) holds its badge row open instead of collapsing it, so the columns
      stay aligned mid-draft as well.
    Downloads
  • v3.8.0 b64798d696

    v3.8.0
    All checks were successful
    CI / build-and-test (push) Successful in 2m11s
    Stable

    joeyr released this 2026-07-27 16:47:36 -05:00 | 64 commits to main since this release

    Changed

    The overlay now compares against the nearest measured tier instead of nothing.

    Every source of benchmark numbers is banded well inside the ladder: the nightly crawler publishes GOLD/PLATINUM/EMERALD, and the seed shipped in the app is GOLD-only. An exact-tier lookup therefore answered nothing at all for an Iron, Bronze, Silver, Diamond, Master, Grandmaster or Challenger player — the CS/min and gold/min comparison lines simply stayed blank until their own harvest slowly filled a bucket in, which is exactly the Riot budget this app exists to stop spending.

    A miss now walks outward along the ladder and takes the closest tier any layer does have: Bronze and below compare against GOLD, Diamond and above against EMERALD. Every seeded champion has a comparison at every rank from the first launch.

    The substitution is reported, not hidden — the overlay captions itself with the tier that actually answered ("vs GOLD avg"), never with the tier it asked for. Showing Gold numbers under a Bronze label would misreport what you are being measured against, which is worse than showing nothing.

    Exact hits are untouched: your own tier is tried across all three layers before any neighbour is considered, so a real Bronze bucket from your harvest still beats the artifact's Gold one. Equidistant ties resolve downward. Nothing hardcodes the band, so if the crawler's tier range is ever widened, real buckets start winning on their own.

    Downloads
  • v3.7.0 ade8384b29

    v3.7.0 — counters, tier lists and benchmarks from the nightly crawler
    All checks were successful
    CI / build-and-test (push) Successful in 1m47s
    Stable

    joeyr released this 2026-07-27 13:52:01 -05:00 | 65 commits to main since this release

    Counters, tier lists and the overlay's benchmark targets now come from the nightly crawler in lol-companion-data instead of being re-fetched from op.gg while you are in draft.

    Counters and tier lists

    The crawler already fetches every op.gg counters page and all five tier-list pages each night — it has to, since op.gg has no endpoint that enumerates which champions are played at which role. It now publishes them, and the app reads that one file.

    A champ select used to cost one op.gg page fetch per enemy champion, queued through the shared rate limiter, plus five more per patch for the tier badges. Those are now map lookups against a file fetched once on connect.

    The scraper is still there and still correct. Anything the artifact does not cover — a champion op.gg does not list at that role, a patch the crawler has not caught up to, an unreachable Forgejo — falls straight through to it, with no retry delay in between. On patch-drop morning the crawler is a night behind, and an off-patch tier list is a claim about the wrong meta rather than a slightly old one; the app recognises that and scrapes for the day instead.

    Both sources score a matchup through the same module, so a defensive-ban suggestion cannot depend on which one answered. The artifact publishes only what op.gg reported — games, wins, win rate — and never a counter rating: that number is tuned against this app's threat threshold and belongs on this side.

    Overlay benchmarks

    The in-game CS/min and gold comparisons had two sources: your own harvest, which spends your Riot key competing with the scouting report for the same budget, and a benchmarks.json committed to the repo that only moves when a release ships one. The published artifact is a third, covering 173 champions across Gold/Platinum/Emerald and refreshing nightly for free.

    Lookups now resolve per (champion, role, tier) rather than per file. A harvested benchmarks.json used to replace the bundled seed outright, which was backwards — the harvest fills in one champion at a time as you play, so for weeks a returning player had fewer comparisons than a fresh install.


    Install: LoL-Companion-3.7.0-setup.exe (installer) or LoL-Companion-3.7.0-portable.exe (no install).

    Downloads
  • v3.6.0 a168d2dbce

    v3.6.0
    All checks were successful
    CI / build-and-test (push) Successful in 1m40s
    Stable

    joeyr released this 2026-07-26 22:41:22 -05:00 | 66 commits to main since this release

    Added

    Champion damage-type splits on the board and the scouting report. Every drafted champion now carries a physical / magic / true bar, and each team carries a weighted rollup that names the call — "Mostly AD → build armor".

    The numbers come from the nightly crawler in lol-companion-data, which has been publishing them for a while with nothing on this side reading them. The app now fetches the published aggregate at runtime, strict-parses it against a mirrored copy of the producer schema, and caches the last good copy under userData so it survives a failed fetch, an offline session, or a cold start. No Riot key and no League client are needed — the splits are ready before the client is even up.

    The rollup weights each champion by its role share of team damage, using the weights the artifact ships. A flat mean would count a support magic split as heavily as the ADC, and the resulting armor-vs-MR call would be wrong for exactly the players relying on it. Both teams are rolled up separately on both surfaces, and a rollup covering only part of a team says so rather than implying full coverage.

    Per-champion splits are read at the lane the champion is shown in, so re-assigning an enemy lane on the board re-reads their split for that lane. A role bucket is only preferred over the champion overall average once it has enough games behind it, since the producer deliberately publishes buckets as thin as a single game; a thin split is marked with * rather than hidden, and a champion the crawler has never seen shows no bar at all rather than a zeroed one.

    A patch mismatch is treated as expected, not as an error: the crawler needs a night of games on a new patch before it can publish for it, so on patch-drop day the previous patch aggregate is shown and labelled rather than blanked.

    Notes

    • Read-only against the League Client (LCU) — never writes to the client.
    • The in-game overlays on-screen render has not yet been verified in a live match.
    Downloads
  • v3.5.8 048a3925bd

    v3.5.8
    All checks were successful
    CI / build-and-test (push) Successful in 2m1s
    Stable

    joeyr released this 2026-07-26 17:21:50 -05:00 | 67 commits to main since this release

    Changed

    • Rune Import now shows at most four combos. The list emitted one row per distinct primary+secondary tree pair op.gg reports, with no cap — so a champion that runs two keystones could push past the intended four. Briar jungle listed five: Press the Attack in three pairings, Hail of Blades in two, the fifth a 161-game page whose 64% win rate is precisely the low-sample-high-winrate trap the importer is supposed to avoid. The list is now capped at the top four by total games, matching the item importer's existing MAX_ITEM_OPTIONS and the two-per-row board grid. The cap trims combos only — every build still rolls into its combo's stats and the pick-rate denominator is unchanged, so the four surviving rows read exactly as before.
    Downloads
  • v3.5.7 e73e8ca44a

    v3.5.7 — renderer lifecycle logging + dead-renderer recovery
    All checks were successful
    CI / build-and-test (push) Successful in 1m40s
    Stable

    joeyr released this 2026-07-23 19:19:16 -05:00 | 68 commits to main since this release

    Fixed

    • A dead renderer is now reloaded instead of leaving a permanently blank window. If a window's renderer process exits abnormally nothing repaints it, and for the transparent in-game overlay that is indistinguishable from "no game running". Renderers now reload automatically on render-process-gone, capped at 3 attempts.

    Added

    • Renderer lifecycle logging for all three windows (companion, overlay, scout): window.loaded, window.load_failed, window.preload_error, window.console_error, window.renderer_gone, window.renderer_reloaded. Renderer-side failures previously never reached the log at all.
    • overlay.shown / overlay.hidden — visibility flips right after the first successful live-metrics publish, separating "the poller never got data" from "data arrived but the panel painted nothing".
    Downloads
  • v3.5.6 0ad8a4bd4c

    v3.5.6
    All checks were successful
    CI / build-and-test (push) Successful in 1m36s
    Stable

    joeyr released this 2026-07-21 18:40:41 -05:00 | 69 commits to main since this release

    Fixed

    • Existing installs now actually get the 24h counter-data TTL instead of staying frozen for a week. v3.5.4 lowered the default cacheTtlHours from 168 (1 week) to 24, but only for fresh installs — it shipped no migration, so anyone who had used the app before kept the old 168h value persisted in their settings.json. That TTL governs healthy (non-thin) matchup data, so the "Defensive Bans" counters kept serving snapshots up to a week old. Because op.gg's per-patch game counts accumulate daily, a multi-day-old cached entry showed far fewer games than op.gg currently reports (e.g. a jungle matchup cached at ~150 games while op.gg had already reached ~360) — which read as "the game counts are still really low." Loading settings now migrates a persisted cacheTtlHours equal to the old default (168) to the current default (24). The field has no in-app control, so a stored 168 can only be the inherited old default: only that sentinel is rewritten, and a value hand-edited anywhere else in the 1–168 range is left untouched. The max configurable TTL stays 168.
    Downloads
  • v3.5.5 24279baa81

    v3.5.5
    All checks were successful
    CI / build-and-test (push) Successful in 1m38s
    Stable

    joeyr released this 2026-07-21 18:33:18 -05:00 | 70 commits to main since this release

    Fixed

    • The in-game Scouting Report no longer gets stuck showing empty team columns for the whole game. Opening the overlay during load-in — while you are at level 1 in the fountain, before Riot's Live Client Data API has populated the 10-player roster — read an empty player list and published a report with zero players, which rendered as bare "Your team / Enemy team" headers with no cards. Worse, that empty run was misread as a fully resolved one (an empty player list trivially has no unresolved identities), so it cleared the retry timers and latched the "already gathered this game" flag — meaning neither re-opening the overlay nor the live poller later seeing a full roster ever re-triggered gathering. The report now bails without publishing when the feed exposes no players yet, leaving the honest "Gathering report…" state and the retry path armed, so the next open — or the game becoming active with a populated roster — fills it in.
    Downloads