- TypeScript 95%
- JavaScript 2.8%
- CSS 2.1%
- HTML 0.1%
|
|
||
|---|---|---|
| scripts | ||
| src | ||
| test | ||
| .env.example | ||
| .gitattributes | ||
| .gitignore | ||
| package-lock.json | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
| tsconfig.main.json | ||
| vite.config.ts | ||
| vitest.config.ts | ||
| vitest.workspace.ts | ||
LoL Companion
A read-only League of Legends companion — ban/pick help during champ select, and a live in-game overlay. It only reads the game; it never hovers, bans, locks, or sends anything.
Features
- Defensive Bans — champions to ban that hard-counter your teammates' hovered picks.
- Suggested Picks — champions you could pick that beat your locked lane opponent, drawn from your champion pool and/or the current meta tier list.
- In-game overlay — a transparent, click-through window showing your live CS/min vs. your tier's average and your gold difference vs. your lane opponent.
It runs as Electron windows and reads Riot's official local APIs — the LCU (champ select) and Live Client Data (in-game). Read-only is enforced at the connection boundary: only HTTP GET and WebSocket subscribe are allowed; everything state-changing is rejected.
Quick start
npm install
npm start
Requires Node 18+ (20 LTS recommended). The League client doesn't need to be running to launch — the app waits for it and shows "Waiting for League client."
Optional — Riot API key (for the overlay's tier-average comparison only). Copy the template and add your key:
cp .env.example .env
RIOT_API_KEY=RGAPI-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
Get one at https://developer.riotgames.com/ (a Personal key is permanent; the dev key expires every 24h). .env is git-ignored. Without a key, the overlay still shows your CS/min and gold difference — just no comparison target.
Overlay display mode: set League to Borderless (or Fullscreen with Windows "Fullscreen Optimizations" on — the Win10/11 default). True exclusive fullscreen bypasses the Windows compositor, so no separate-window overlay can draw over it.
How it works
The Electron main process runs a few components; the React renderer is a thin presentation layer that subscribes to main-process events over a typed contextBridge.
- LCU Connector — discovers the League lockfile, authenticates, and subscribes to champ-select/gameflow events (read-only).
- Session Tracker — extracts the live champ-select model (your role, ally hovers, bans, locked picks).
- Stats Provider — fetches counter-rating data from a third-party source (op.gg, NA Gold+) for the current patch, with on-disk caching, rate limiting, retries, and stale-cache fallback.
- Recommender — pure functions that rank ban targets by a threat score and counter-picks by a sample-weighted pick score.
- Live poller — while a game is running, reads the Live Client Data API once per second and drives the overlay.
- Harvest trigger — when you lock a champion, kicks off a background benchmark harvest for it (skip-if-fresh) so its tier-average is ready next time.
Suggested Picks reuse the opponent's counters page read in reverse: a champion that strongly counters your lane opponent sits near the top of their counters list. So the app fetches one page — your locked opponent's counters at your role — and ranks the champions that beat them, intersected with your pool / the meta list. Riot doesn't expose enemy hovers, so these appear once the enemy laner locks in.
Configuration
The Champ pool & tiers panel (bottom of the window) edits the two settings you tune most — your per-lane champion pool and which op.gg tiers feed the "Top meta" list — live, without a restart. Edits save to settings.json immediately.
settings.json lives in Electron's per-user data directory (hand-editable):
- Windows:
%APPDATA%\champ-select-assistant\settings.json - macOS:
~/Library/Application Support/champ-select-assistant/settings.json - Linux:
~/.config/champ-select-assistant/settings.json
Any missing/invalid field falls back to its default. The stats cache sits alongside as stats-cache.json.
| Field | Range | Default | Description |
|---|---|---|---|
statsSource |
u-gg | op-gg | lolalytics |
op-gg |
Third-party stats source adapter |
cacheTtlHours |
1–168 | 168 |
How long cached stats stay fresh |
threatMatchupThreshold |
-1.0–0.0 | 0 |
Counter rating at/below which an opponent counts as a "threat." 0 admits every counter past the source's game-count floor; lower toward -1 to require stronger counters |
rateLimitPerSecond |
1–60 | 1 |
Max requests/sec to the stats source |
myPool |
per-role name lists | {} |
Your champion pool for Suggested Picks, keyed by role (TOP/JUNGLE/MIDDLE/BOTTOM/UTILITY). Unrecognized names are skipped |
metaTierFilter |
op.gg buckets 0–5 (0 = OP) |
[0, 1] |
Which tiers the "Top meta" list draws from |
{
"statsSource": "op-gg",
"cacheTtlHours": 168,
"threatMatchupThreshold": 0,
"rateLimitPerSecond": 1,
"myPool": {
"JUNGLE": ["Briar", "Lee Sin", "Vi"],
"MIDDLE": ["Ahri", "Sylas"]
},
"metaTierFilter": [0, 1]
}
myPool and metaTierFilter are optional — omit myPool and Suggested Picks fall back to the meta list for your role.
Benchmark data (overlay tier averages)
The overlay's "tier average" numbers come from an offline harvest of Riot's Match-V5 API, stored per (champion, role, tier) in src/shared/benchmarks/benchmarks.json. Riot has no "matches for champion X" endpoint, so the harvester pulls whole ranked games at a tier and records every participant — filling many champions' data at once.
Automatic: locking a champion starts a background harvest for it (at the compare tier, currently GOLD) unless its data is already fresh. A harvest takes minutes, so the data lands after that game and is used on the next launch. Requires RIOT_API_KEY.
Manual:
npm run harvest:benchmarks -- --champion Briar --tier GOLD
npm run harvest:benchmarks -- --championId 233 --tier GOLD --targetSamples 200
Runs are rate-limited, checkpointed, and resumable (Ctrl+C and re-run). Output is src/shared/benchmarks/benchmarks.json.
Development
npm run dev # Vite dev server + Electron, hot-reloading UI (Ctrl+C stops both)
npm start # clean rebuild, then launch against dist/
npm test # all suites (unit, property, integration)
npm run typecheck # tsc --noEmit
npm start always clean-rebuilds first, so a stale build is impossible. Editing main-process code (src/main, src/preload) needs a npm run dev restart — only the renderer hot-reloads.
Windows gotcha: if
ELECTRON_RUN_AS_NODEis set in your environment (even to""),npm startcrashes withCannot read properties of undefined (reading 'isPackaged')— Electron runs as plain Node instead of launching the GUI. Remove it (PowerShell:Remove-Item Env:\ELECTRON_RUN_AS_NODE); emptying it is not enough.
| Script | What it does |
|---|---|
npm run dev |
Dev server + Electron, hot-reloading UI |
npm run build |
Build main + renderer into dist/ |
npm start |
Clean rebuild, then launch |
npm test |
All test suites |
npm run typecheck |
Type-check without emitting |
npm run harvest:benchmarks |
Harvest overlay tier-average data |
npm run gen:tags |
Regenerate champion tags from Data Dragon |
npm run gen:item-costs |
Regenerate item costs from Data Dragon |
The test suite includes property-based tests (read-only contract, session extraction, stats cache, retry/fallback, threat scoring, list invariants) plus integration tests against a mock LCU and mock stats source.
src/
main/ Electron main process (LCU, session, stats, recommender, live, pipeline, IPC)
preload/ contextBridge (typed, read-only message contract)
renderer/ React companion window + in-game overlay
shared/ zod schemas, serialization, benchmarks asset + loader
scripts/ Offline tooling (benchmark harvester, tag/item-cost generators)
test/ unit / property / integration
Disclaimer
Unofficial tool that reads the local League Client API. It is observation-only and issues no actions, but use it at your own discretion. Counter data comes from third-party sources and is only as current as the cached patch data allows.