DEV Community

Cover image for I Built a Tower Defense Game With Zero Dependencies — AI Art, Procedural Audio, and ~3,200 Lines of Vanilla JS
Jason Guo
Jason Guo

Posted on

I Built a Tower Defense Game With Zero Dependencies — AI Art, Procedural Audio, and ~3,200 Lines of Vanilla JS

No React. No Phaser. No Pixi.js. No npm install. Just <canvas>, vanilla JS, and the Web Audio API.

🎮 Play it here · 📖 Full dev log

What I Built

Arcane Bastion is a crystal tower defense game with:

  • 5 elemental towers(Fire / Ice / Lightning / Vine / Arcane)× 3 upgrade tiers
  • 10 enemy types with status effects(freeze, burn, poison)
  • AI‑generated art — every sprite and map background
  • Procedural audio — every sound synthesized from oscillators, zero audio files
  • Dynamic tower rotation — smooth tracking with recoil feedback

The entire thing is 9 script files loaded via <script> tags in dependency order. Old school, but it works.

The Architecture

arcane-bastion/
├── js/
│   ├── sprites.js    ← async image loader (18 assets)
│   ├── map.js        ← 20×14 tile grid + AI backgrounds
│   ├── tower.js      ← 5 types, targeting, smooth rotation
│   ├── enemy.js      ← 10 types, waypoint pathfinding
│   ├── projectile.js ← homing missiles, chain lightning
│   ├── audio.js      ← Web Audio synthesis (~400 lines)
│   ├── ui.js         ← HUD, panels, bestiary overlay
│   ├── game.js       ← 60fps loop, wave state machine
│   └── demo-mode.js  ← scripted auto-demo + MediaRecorder
Enter fullscreen mode Exit fullscreen mode

The Hardest Bug: Chrome's Autoplay Policy 🔇

This one wasted hours. My audio engine worked perfectly in local testing, but was completely silent on Chrome after deployment.

Root cause: Creating an AudioContext before the first user gesture puts it in suspended state — permanently. My Game.init() was calling AudioEngine.init() on page load.

The fix:

// ❌ BAD: Creates AudioContext on page load
function init() {
    ctx = new AudioContext(); // suspended forever!
}

// ✅ GOOD: Lazy-create on first user interaction
function init() {
    document.addEventListener('click', () => {
        if (!ctx) {
            ctx = new AudioContext();
            // now it works because user clicked first
        }
    });
}
Enter fullscreen mode Exit fullscreen mode

And crucially — every sound function must call ensureContext() before the if (!ctx) return guard, not after. Get the order wrong and the context never gets created.

AI Art: Not Plug-and-Play

I generated all 18 visual assets(5 towers, 10 enemies, 3 maps)using AI image generation. Sounds easy, right?

The reality:

  • Consistency — Every prompt needed the same style anchor: “dark fantasy, pixel art, top-down, glowing magical effects”
  • Transparency — Early sprites had visible rectangular backgrounds. Each needed manual cleanup
  • Contrast — Sprites that looked great on a white canvas disappeared against dark map backgrounds. Had to add glow auras and brightness boosts
  • Scale — A beautiful 512×512 sprite becomes a blurry 50px blob at game scale

AI art saved weeks of illustration work, but the integration “last mile” still required significant manual effort.

Procedural Audio: Math → Sound

The most satisfying system to build. Every sound in the game is synthesized from OscillatorNode waveforms:

// Frost tower attack: crystalline "ping"
function frostShoot() {
    playTone(1200, 0.15, 'sine', 0.12);    // high shimmer
    playTone(800, 0.2, 'triangle', 0.08);  // harmonic
    playNoise(0.08, 0.1);                  // ice crackle
}
Enter fullscreen mode Exit fullscreen mode

Fire, ice, lightning, vine, arcane — each element has a unique audio signature created by combining oscillator types(sine, triangle, sawtooth, square)at different frequencies and durations.

The background music is a multi‑oscillator ambient drone that evolves over time. The total audio system is ~400 lines — zero bytes of audio assets loaded.

Three Changes That Made the Biggest Visual Difference

  • AI map backgrounds replacing procedural tiles — went from “programmer art” to “atmospheric”
  • Glow auras on tower sprites(radial gradient circles behind each tower)— instant depth
  • Smooth tower rotation with Math.atan2 + lerp interpolation — made everything feel alive

None of these changed gameplay. All of them changed how the game feels.

Final Stats

Metric Value
Lines of JS ~3,200
Code modules 9
AI-generated assets 18
External dependencies 0
Audio files 0
Build tools 0

I wrote a much deeper technical breakdown covering grid systems, pathfinding, chain lightning implementation, tower synergies, and how I recorded the demo video with audio using the browser's MediaRecorder API.

📖 Read the full 3,000+ word dev log

🎮 Play Arcane Bastion

Top comments (0)