Full article:
Pixel dungeon dev log
I spent the last few weeks building a roguelite dungeon crawler using nothing but vanilla JavaScript and HTML5 Canvas. No React. No Phaser. No build tools. Just 6 JS files, a 16×16 pixel art tileset, and a growing list of bugs I didn't see coming.
The game has procedural dungeon generation, 5+ enemy types with AI state machines, a full camp system with crafting stations, procedural Web Audio music, and an equipment/inventory system. It runs from a single HTML file.
Here's what actually went wrong — and the fixes that made the biggest difference.
1. The Enemy That Disappeared When You Got Close
The most absurd bug I encountered: enemies became invisible when the player walked toward them.
The cause? State transitions. When an enemy switches from patrol to chase (triggered by proximity), there's a window of 2-3 frames where the state variable updates but the sprite reference hasn't resolved yet. Canvas drawImage() with an undefined source doesn't throw an error — it just silently draws nothing.
Takeaway: Canvas fails silently. Unlike DOM rendering, you get zero feedback when drawImage receives bad input. Defensive fallback chains are mandatory.
2. Players Couldn't Figure Out the Camp
After clearing floor 1, players enter a camp with 9 interactive stations — bonfire, workbench, anvil, furnace, alchemy table, etc. I thought it was self-explanatory.
The first person who played it immediately asked: "What is this? What am I supposed to do?"
The problem: in 16×16 pixel art, a bonfire is ~12 pixels of flickering orange. An anvil is a gray blob next to a different gray blob. Without explicit labels, it's all noise.
The fix was information layering at three distances:
- Screen-level: Welcome banner with instructions
- Room-level: Persistent icon + name labels on every station
- Close-up: Pulsing highlight + "[E] to interact" + description
Takeaway: If you built it, you already know what everything does. That knowledge makes you blind to your own UX problems.
3. The Canvas Resolution Trap
Every Canvas tutorial starts with:
For pixel art, this is a disaster.
Pixel art requires integer scaling. A 16×16 tile at 1753px window width creates fractional pixel offsets. Sprites shimmer. Grid lines misalign. The whole thing looks "off" in a way players feel but can't articulate.
The fix is separating internal resolution from display size:
But then there's a follow-up trap: mouse coordinates break. event.offsetX maps to CSS display size, not internal canvas size. Every click-based UI element silently stops working.
And another follow-up: HUD elements using position: fixed anchor to the browser window, not the game canvas. When the canvas is centered with dark borders, the HUD floats off into the corner.
Solution: wrap everything in a position: relative container, use position: absolute for overlays.
4. Tile Caching is Non-Negotiable
Early builds re-drew every tile every frame. On a 40×40 map: 1,600 fillRect calls per frame, plus decorations like torches, cracks, and wall details.
Switching to an off-screen canvas tile cache — render the map once, blit it as a single drawImage — dropped draw calls from 2,000+ to under 200. On mobile, this was the difference between 60fps and slideshow.
5. Atmosphere is Just Math
The difference between "atmospheric dungeon" and "I can't see anything" is one float:
Same engine. Same radial gradient. Different emotional experience.
I also bumped the camp floor tile colors from rgb(68+v) to rgb(95+v) — a +27 shift. The camp instantly felt warmer, cozier, more safe. Atmosphere isn't a vague artistic quality. It's concrete numbers in your rendering pipeline.
6. Procedural Audio is Underrated
Zero audio files in the entire project. Every sound — sword slashes, footsteps, healing chimes, background music — is synthesized using the Web Audio API.
A sword attack is a filtered noise burst. A healing potion is an ascending C-E-G arpeggio. The dungeon BGM is a looping minor pentatonic pattern in square waves.
The best part: sounds become tunable. Boss hits play at lower frequency. Critical hits add a pitch sweep. Camp music shifts the filter for warmth. Sound becomes data, not static assets.
The Full Story
This post covers the highlights, but there's a lot more in the full writeup — the procedural dungeon generation algorithm, floor difficulty scaling, the complete camp station system, and how the entire architecture fits in 6 files with zero dependencies.
👉 Read the full dev log on InstantGames
🎮 Play the game (browser, no install)
Built with vanilla JavaScript + HTML5 Canvas. No frameworks. No build tools. The whole game is a single HTML file with 6 JS modules. If you're doing something similar, I'd love to hear about it in the comments.







Top comments (0)