← Work

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 map
Worst-case pin error cut from 189px to 61px
No backend, runs entirely in the browser
Links
Live site ↗
Contents
  1. The problem
  2. What I built
  3. The hard part
  4. What it shows
  5. Status

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.

LeaguesMap on desktop: a sidebar with progress, region and difficulty filters and a task list, next to the Old School RuneScape world map covered in orange task pins.
The full app. Filters and the task list on the left, task pins on the wiki's world map on the right.

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.

Two phone screenshots. Left: the map fills the screen with a collapsed bottom sheet. Right: the sheet pulled up to the task list, with checkboxes and region tags.
On a phone the map stays full screen. Filters and the task list live in a bottom sheet.

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.

Zoomed-in map of Civitas illa Fortis in Varlamore, with task pins on its buildings and an open popup listing three tasks at one location.
Civitas illa Fortis in Varlamore. Each pin sits on the place its task names, and tasks at the same spot share one pin.

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.