LeaguesMap
An interactive map of Old School RuneScape's Demonic Pacts league: 1,177 of its 1,592 tasks pinned to in-game locations, RuneLite progress import, no backend.
- Role
- Solo engineer
- Stack
- React 19 · TypeScript · Vite · Leaflet
- Outcome
- 1,177 of 1,592 tasks pinned to the mapWorst-case pin error cut from 189px to 61pxNo backend, runs entirely in the browser
- Links
- Live site ↗
An interactive map and tracker for the 1,592 tasks in Old School RuneScape's "Demonic Pacts" league, built for players running that league. Tasks tied to a place get a pin on the map; the rest stay in a searchable list.

The problem #
OSRS leagues are time-limited game modes with hundreds of tasks tied to specific in-game locations, and the wiki's text-based task lists don't show where any of them actually are. Demonic Pacts shipped 1,592 tasks with no visual way to plan a route or track progress against a live map.
What I built #
A Leaflet-based map app: React 19, Vite, TypeScript in strict mode, with a custom tile pyramid generated from the OSRS wiki's world map at six zoom levels, up to 8192x6059px at native resolution, served as real map tiles rather than an embedded iframe or static screenshot. Tasks are filterable by region, difficulty, skill, and pact, and can be marked complete by hand or by importing a save file from a RuneLite plugin. Built solo, 57 commits, timed to the league's live run, then shipped to its own domain with a sitemap and SEO metadata.

The hard part #
Two decisions mattered.
First, the map calibration. The wiki's world map image is a stitched composite: a western continent block pasted onto mainland Gielinor with a wider-than-uniform ocean gap between them. A single linear transform from game coordinates to pixels, the obvious first pass, put every western task pin 50 to 80 game tiles off. The fix, in src/lib/calibration.ts, is a piecewise affine transform: one shared slope, a different X origin per map segment. The first fit shifted the western origin by 65 tiles; after a regression showed up in southern Varlamore during actual play, I re-measured against the wiki's label-text centers and refined the shift to 44 tiles, cutting the worst-case residual from 189px to 61px.

Second, the RuneLite import. The obvious way to sync a player's completed tasks is the wiki's own sync API, but it blocks third-party use at the infrastructure level and publishes a policy against using it outside the wiki itself. I reimplemented the open-source bitfield decoding a community tool already uses, client-side, against an export file the player saves. No backend, and nothing touches the wiki's API.
What it shows #
Fast, scoped, solo shipping against a real deadline (the league itself), and solving a data-access constraint with a client-side design instead of a backend. The repo was built iteratively with Claude Code, with me driving requirements and QA in near-real time against a live league.
Status #
Live at leagues-map.karpinsky.io.