Writing · Build Notes
I built a Diablo II-style ARPG in vanilla JS and Canvas — no engine, no build, no art assets
AFGROND (die afgrond wag — "the abyss waits") is a browser isometric action-RPG in the Diablo II mold: a town hub, infinitely-descending procedural dungeons across three biomes, three classes, a loot system with rarity tiers and affixes, gem sockets, skill trees, A* pathfinding, champion packs, a hireable mercenary, difficulty tiers, and a final boss at depth 10.
AFGROND (die afgrond wag — "the abyss waits") is a browser isometric action-RPG in the Diablo II mold: a town hub, infinitely-descending procedural dungeons across three biomes, three classes, a loot system with rarity tiers and affixes, gem sockets, skill trees, A* pathfinding, champion packs, a hireable mercenary, difficulty tiers, and a final boss at depth 10.
It's vanilla JavaScript and HTML5 Canvas 2D — no engine, no build step, no dependencies, and no art assets at all. Every sprite is drawn procedurally, every icon is an emoji, and it runs fine on low-end hardware. Building a genre this dense with zero libraries taught me more about scope than any tutorial.
The heart: isometric projection
An ARPG lives or dies on its isometric renderer. The whole illusion is one coordinate transform — tile space (grid) to screen space (diamonds). With 64×32 tiles it's four lines:
export const TILE_W = 64, TILE_H = 32; // diamond width / height
export const HALF_W = 32, HALF_H = 16;
export function worldToScreen(tx, ty) {
return { x: (tx - ty) * HALF_W, // x spreads on the difference
y: (tx + ty) * HALF_H }; // y stacks on the sum
}(tx - ty) for x and (tx + ty) for y is the entire diamond grid. The inverse (screenToWorld) turns a mouse click back into a tile so you can click-to-move, and a small Camera adds the origin offset to keep the player centered. Get this right and depth-sort everything back-to-front, and a pile of 2D canvas draws reads as a 3D world.
Everything is procedural
With no art pipeline, the constraint is the same one that made my other projects portable: if you can't download it, generate it.
- Sprites are drawn to small offscreen canvases — a body, a palette, a few shapes — then cached and blitted. Monsters, the player's three classes, gear, and gems are all code.
- Floors and walls are textured procedurally per biome (crypt / cavern / inferno), with raised walls, torch lighting, and particles.
- Dungeons are generated fresh each descent — rooms, corridors, and a walkability grid that
A*pathfinds over for both click-to-move and monster chase/kite AI. - Sound is synthesized (ambient music + effects), so there's not a single media file in the repo.
The systems that make it an ARPG
The genre is really a stack of interlocking systems, and the fun was seeing how few lines each actually needs once the data model is right:
- Loot with a rarity ladder — normal → magic → rare → set → unique — rolling random affixes onto items, plus named uniques, gold, potions, and gems.
- Gem sockets: socket-aware weapons and armour; fuse a gem for slot-specific bonuses.
- Skills — fireball, frost nova, whirlwind, chain-lightning, meteor, teleport, rain-of-arrows… — as projectiles, arcs, buffs, and damage-over-time, wired to a right-click slot and a skill tree.
- Monsters: 12 archetypes with chase + ranged-kiting AI, plus champion/unique packs — glowing elites with modifiers (fast / tough / deadly / regenerating / fiery) and guaranteed drops.
- A mercenary companion you hire, who draws aggro, levels, dies, and can be revived.
- Difficulty tiers (Normaal → Helse → Hel) unlocked by clearing deep bosses, scaling foes and loot.
- Town economy: merchant (buy / sell / gamble), shared stash, waypoint fast-travel, and a story with a final boss — Die Eerste Duister — at depth 10.
What I learned
- A genre is a data model plus a few loops. Loot, affixes, sockets, and skills sound huge, but each is a small table and an update function; the combinatorics create the depth, not the code volume.
- Procedural-everything scales down beautifully. No asset pipeline meant I could add a new monster or item by writing a spec, not opening an editor — the same lesson every zero-dependency project keeps teaching me.
- Vanilla JS can hold a surprising amount. Split into
engine / entities / items / world / uimodules, an entire ARPG stays legible without a framework — the discipline is in the structure, not the tooling.
A build-story · see the live project → · more writing →