LeaguesMap
An interactive map and task tracker for Old School RuneScape's Demonic Pacts league, with RuneLite progress import and no backend.
- Stack
- React 19 · TypeScript · Vite · Leaflet
- Outcome
- Worst-case pin error cut from 189px to 61pxNo backend, runs entirely in the browser1,177 tasks pinned to the map
- Links
- Live site ↗GitHub ↗
An interactive map and tracker for the 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 in 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 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 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.