Quake In 13kb Of Javascript
Based on The PrimeTime's video on YouTube. If you like this content, support the original creators by watching, liking and subscribing to their content.
Procedural texture generation replaces external PNG assets to avoid blowing the zipped size budget (31 textures drop from ~150KB PNGs to ~1.3KB zipped definitions).
Briefing
A 13KB JavaScript build of Quake (“q1 K3”) pulls off a full FPS experience—textures, sounds, music, weapons, enemies, and two classic-style maps—by generating most assets in code and aggressively compressing both data and logic. The core takeaway isn’t just nostalgia for the 1996 original; it’s a set of practical size-golf techniques that make a complex 3D game feasible inside the JS13k constraint, where the final submission must fit in a 13KB zipped package.
Instead of shipping pre-made art, the project generates its 31 textures on the fly using a tiny canvas-based texture DSL. Early attempts relied on external PNG textures, but that immediately blew the budget (about 150KB just for textures). The solution was to build a miniature “texture library” with only a handful of drawing primitives—embossed rectangles and grids, noise, and text—then compose those primitives to create grunge-like materials for grass, sky, water, and more. A custom texture editor lets the author draw one texture onto another (including onto itself), producing a full texture set whose definitions zip to roughly 1.3KB.
Map data follows a similar “store less, compute smarter” philosophy. The famous E1M1 layout is represented using axis-aligned bounding blocks (AABBs). That choice sacrifices slopes and fine detail—making the world look more like Minecraft than original Quake—but it slashes geometry storage and simplifies collision math. Collision then becomes tractable through a bitmapped occupancy grid (128×128 cells), so collision checks reduce to looking up which grid cells an entity’s bounding box overlaps, then resolving movement direction-by-direction to allow sliding along walls and floors.
Models and animations are also compressed with Quake-like reuse. Only a few animated model types exist (a basic humanoid, a dog, and a torch), and enemies are created by scaling/stretching the same base geometry and swapping textures. Animated frames are stored compactly, and vertex positions use bit-level packing to improve zip behavior. Geometry is further trimmed by removing unseen faces; even the Quake logo on the title screen is optimized by stripping most sides.
Audio and music are handled with size-first tooling. Music leans on a heavily modified tracker (Sonitent X / ATT-style approach), with oscillator lookup tables generated at startup and music stored in flat arrays rather than JSON objects. Sound effects use a simple spatial audio model: distance and angle to the camera drive volume and stereo panning. The result is a moody, minimalist soundtrack that compresses well, plus spatialized effects that still fit the budget.
All assets and data are packed to leave room for the engine. Two levels total 563 blocks and 188 entities, with level data around 4.5KB uncompressed (3.2KB zipped). Models and textures together land in the low single-digit kilobytes, and the entire game—including unpacking, rendering, physics, AI hooks, and gameplay logic—fits by using minification, zip-friendly data ordering, and an extra compression step via a JavaScript packer (“Road Roller”) that effectively frees about 1.2KB.
The broader message is that “game development” inside JS13k becomes less about authoring everything by hand and more about generating data, choosing representations that compress well, and building tiny tools that provide fast feedback. The payoff is a surprisingly faithful Quake-like experience delivered under extreme constraints—proving that careful engineering can beat the size limit without turning the game into a toy.
Cornell Notes
q1 K3 recreates a Quake-like FPS under the JS13k rule by generating assets in code and compressing data aggressively. Textures are produced with a tiny canvas-based DSL (noise, embossed rectangles/grids, and text) and composed via a small editor; the full 31-texture set is about 1.3KB zipped instead of ~150KB of PNGs. Levels are stored as axis-aligned bounding blocks (AABBs) to simplify collision and reduce geometry, then collision uses a bitmapped 128×128 occupancy grid. Models are minimized through reuse (few animated base types, scaled variants for enemies) and bit-packed animation/vertex data to improve zip compression. Music and effects are also size-golfed using a modified tracker with lookup tables and flat arrays, plus a simple spatial audio model.
Why does generating textures in code matter so much for a 13KB zipped limit?
What tradeoff does the project make by using axis-aligned bounding blocks (AABBs) for maps?
How does collision detection become feasible without storing complex geometry?
How are models kept small while still supporting multiple enemy types and animations?
What size-golf techniques make the music and sound fit?
How does extra compression beyond minification help meet the JS13k package limit?
Review Questions
- Which representation choices (textures, maps, collisions, models) most directly improve zip compression, and why?
- How do AABBs and the 128×128 occupancy bit map work together to keep collision logic both small and playable?
- What procedural generation primitives and composition steps are used to build the texture set, and how does that compare to shipping PNG assets?
Key Points
- 1
Procedural texture generation replaces external PNG assets to avoid blowing the zipped size budget (31 textures drop from ~150KB PNGs to ~1.3KB zipped definitions).
- 2
A tiny texture DSL plus a texture editor (noise, embossed rectangles/grids, text, and compositing) is enough to create a full material set under extreme constraints.
- 3
Map geometry is reduced to axis-aligned bounding blocks (AABBs), trading away slopes and fine detail to simplify storage and collision math.
- 4
Collision uses a compact 128×128 occupancy bit map, so entity-vs-world checks become grid lookups plus direction-by-direction sliding resolution.
- 5
Models are minimized through reuse (few animated base types) and transformations (scaling/stretching) rather than unique geometry per enemy.
- 6
Music is size-golfed by using oscillator lookup tables and storing tracker output in flat arrays instead of JSON objects.
- 7
When minification isn’t enough, a JS packer (Road Roller) can free ~1.2KB by compressing source and adding decompression code at runtime.