• v3.13.0 abea86a9fb

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

    joeyr released this 2026-07-31 20:30:34 -05:00 | 22 commits to main since this release

    Added

    • The champ-select board says which mode it is showing. A badge beside "In champ select" names the map — muted for an ordinary game, accented for Classic Rift. Two boards for two modes were pixel-identical while being backed by entirely different champions, so the only way to tell a Classic Rift board from a Summoner'''s Rift one was to recognize the Jade roster by its champion art.

      The mode comes from the LCU gameflow session'''s map.id, read once on entering champ select. That is the only signal available at that point: the Live Client Data API the in-game paths use does not exist until the game process launches, and by then there is no pick left to advise on.

      Keyed on map.id === 453 and never on a mode-name substring. Queues 2450 and 3280 report a KIWI_JADE mode ("ARAM: Mayhem Classic-ish") on map 12, so an includes('''JADE''') test would call those Classic Rift; a test pins that case. A session that cannot be read, or reports no map, is simply not badged — it is never guessed at.

    • game-mode, a retained IPC message carrying the roster, map id and map name, re-delivered on markReady() like the other retained state so a reload mid-champ-select repaints the badge.

    Notes

    • This does not yet restore tier badges or automatic enemy lane placement in Classic Rift. Both read the op.gg tier list, which is still fetched as type=ranked and keyed by live champion ids, so every 60xxx lookup misses. A per-role tier list is the one op.gg request that cannot derive its queue from a champion id, which is exactly why the session-level mode above had to exist first. See docs/classic-opgg-plan.md.
    Downloads
  • v3.12.0 1e2ea09731

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

    joeyr released this 2026-07-31 08:54:22 -05:00 | 23 commits to main since this release

    Added

    • Classic Rift champ select reads the published counters artifact instead of
      scraping op.gg.
      3.11.0 made the live path work for the mode; this puts the
      nightly file in front of it, the same short-circuit ranked has had — a map
      lookup instead of one page fetch per enemy champion against a draft timer.

      It is a second file, not a second key in the existing one. op.gg indexes
      classic pages by base champion id, so id 24 exists in both queues meaning two
      different kits, and one map holding both would answer a classic Jax with
      ranked Jax's matchups: correctly shaped, plausibly ranged, confidently wrong,
      and undetectable by anything downstream. countersArtifactRouter picks the
      file from the id block and has its own tests for exactly that reason.

      A missing classic file is an expected state rather than an outage — the
      producer's classic pass is opt-in and the mode is limited-time — so the router
      reports nothing and the request falls through to op.gg, behaving exactly as it
      did before the file existed. It never substitutes the other queue's
      artifact
      .

    • rosterForChampionId, for the places a champion arrives as a bare number with
      no alias or map beside it.

    Changed

    • CountersArtifactAdapter now takes the champion id when asking for its
      artifact, since which file answers depends on which queue the champion is
      from.
    Downloads
  • v3.11.0 08a99e107a

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

    joeyr released this 2026-07-30 23:18:34 -05:00 | 24 commits to main since this release

    Added

    • Classic Rift champ select gets real advice. v3.10.0 fixed the in-game
      overlay; the board was still blank, because every op.gg request went out as
      type=ranked with a jade_* slug that 307-redirects. op.gg does publish the
      mode — counters, runes and item builds — under type=classic, so the adapters
      now ask for it.

      The champion id decides, not a flag. A 60xxx subject can only have come
      from a Classic Rift champ select and a live id can only have come from
      anything else, so opggSubject derives the queue and the slug id from the id
      itself. Nothing is threaded through the pipeline, and there is no state that
      can disagree with the id it describes.

      Only the CHAMPION slug needs translating (60028evelynn): op.gg keys
      classic items and runes in the 77xxxx / 775xxx blocks, which is what the
      live client reports, so those ids pass straight through — including into the
      item-set import, which needed no change at all.

    Fixed

    • Every classic counter would have silently failed to apply. op.gg names
      opponents by base display name, so "Jax" resolves to live id 24 — while a
      Classic Rift board is holding 60024. The matchup and the board would never
      have matched, with no error raised anywhere: the advice simply never fires.
      Matchups are now re-keyed into the subject's namespace, and the fix is
      idempotent so an already-classic id is never shifted twice.

      Caught end-to-end against a captured live page rather than by reading, which
      is the only way this one surfaces — every layer looks correct in isolation.

    Notes

    Champ-select roster detection (rosterForMapId) keys on the gameflow map id
    453, never on the mode string. Queues 2450 and 3280 report gameMode
    KIWI_JADE — "ARAM: Mayhem Classic-ish" — on map 12, so any substring test
    against the mode name routes those ARAM games to classic pages. The four real
    Classic Rift queues are all on map 453, which also survives Riot adding a fifth.
    The Live Client Data API cannot help here: port 2999 is not listening until the
    game process starts, and champ select has picks to advise on before that.

    op.gg does not segment this mode by rank — the tier parameter is accepted and
    discarded, with every band and a deliberately invalid value returning identical
    rows. Classic figures therefore describe all ranks, and nothing should caption
    them with a tier.

    Downloads
  • v3.10.0 dda79c2c24

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

    joeyr released this 2026-07-30 20:28:26 -05:00 | 27 commits to main since this release

    Everything here came out of one real Classic Rift game, watched live through the
    Live Client Data API. The mode was already detected correctly (v3.9.2); what
    this release fixes is everything downstream that quietly assumed live-roster
    data — most of which reported a confident wrong number rather than staying quiet.

    Fixed

    • Every lane's gold difference read +0 for an entire game, and the team-gold
      bar with it.
      Classic Rift namespaces items exactly the way it namespaces
      champions: 770000 + base id, so Doran's Ring is 771056. The committed cost
      table was generated from patch 16.12.1 — three patches before the mode shipped
      — so not one of the 69 items seen across a 34-minute game had a known cost.
      Every inventory summed to 0, so every difference was 0 − 0, and the overlay
      stated "dead even" on all five lanes while the game was decidedly not.

      The root cause was not the stale table, which is merely how it surfaced.
      getItemCost returned 0 for both "this item is free" and "no idea what this
      item is", and itemGold summed straight through that. Absence now means
      unknown: the generator keeps zero-cost entries (>= 0, not > 0) so a
      trinket is a real 0, and itemGold returns null if any held item cannot be
      priced. A partial sum was the worst available answer — it reads as "this player
      is behind" rather than "we don't know" — so it is all-or-nothing, and null
      propagates to on the lane rows, the gold/min metric, and the team-gold bar,
      which now hides rather than drawing a 50/50 split beside a meaningless +0.

      Regenerating against 16.15.1 also picked up three live items whose prices had
      drifted since 16.12.1 (Bastionbreaker 3200 → 3000, Protoplasm Harness
      2500 → 2600, Yun Tal Wildarrows 3100 → 3000), which had been skewing normal
      games too.

    • The benchmark harvester ground a full game at 0/150 against a classic
      champion id.
      Match-V5 reports live ids, so a 60xxx subject matches nothing
      it will ever return. HarvestQueue.enqueue now refuses those ids outright and
      logs harvest.skipped_classic; the Riot key is shared with the nightly crawl,
      so the wasted budget was not free.

    Changed

    • Roster detection gained two stronger signals, and the reasoning behind
      ignoring gameMode was wrong.
      The previous comment dismissed the field
      because ordinary Summoner's Rift reports "CLASSIC" — true, but it does not
      follow that the field is useless. Classic Rift reports "JADE", matching
      the Jade_ champion aliases, which makes it a clean mode-level discriminator.
      Detection now tries rawChampionNamegameModemapNumbermapName,
      strongest first, and mapNumber: 453 is preferred over "Map453" because it
      is the id rather than a rendering of it. All four were observed together in the
      real game; the redundancy is for the payload shapes Riot ships next. CLASSIC
      is still explicitly rejected, with a test pinning it.

      The Map453 spelling that v3.9.2 guessed at is now confirmed rather than
      assumed.

    Added

    • The in-game tracker sits 120px higher in Classic Rift. Its HUD is the
      taller 2009-era layout, so a bottom-anchored panel at the normal 12px margin
      overlapped the scoreboard and champion frame. The detected roster now travels
      on the live-metrics IPC message, and only the bottom edge moves — side margins,
      top-anchored corners, and live games are all untouched.

    Known gaps

    • Champ select still resolves nothing in Classic — counters, tier badges,
      defensive bans, runes and item builds all fall through to op.gg, which is
      queried with type=ranked and the jade_* slug. op.gg does publish the mode
      under type=classic with the base slug, so this is fixable and is not a
      limitation of the source; it is simply not in this release.
    Downloads
  • 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 | 28 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 | 30 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 | 31 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 (SUPUTIL). 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 | 33 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 | 34 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 | 35 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