The LevelSpec 2 vector preview adds .level.svgx authoring, a local 2D/3D
inspector (npm run dev), separate wall/floor/stair meshes, automatic UVs and an
optional Blender bake. See the SVGX guide and release gates.
The v1 interfaces described below remain available until migration parity passes.
LevelSpec 2 exports GLB, glTF, FBX and Quake BSP2 from the CLI and workbench;
see export formats and tool setup.
A level is a program, not a mesh. You write down what a place is — rooms, what is next to what, where the walls and doors and stairs go — and a deterministic compiler owns every coordinate that comes out of it.
The point is that the compiler can then be held to a standard no hand-built geometry meets. Across the 9 authored levels and 380 floor plans in this repo, the compiled output has 0 coplanar-same-normal face pairs and 0 intersecting volumes, and its navmesh is 100% reachable with 0 playable islands. A naive build of the identical specs — each box individually watertight, correctly wound, and passing every half-space test — produces hundreds of both.
spec (JSON or a drawn plan)
→ compile() 11 passes; every coordinate decided here
→ validate() geometry, navigation, tactical gates
→ bakeNavmesh() independent re-derivation from the solids
→ geometry / OBJ / Quake .map / OpenSCAD / CadQuery / DXF / SVG
Writing a LevelSpec by hand means converting shapes into rectangles in your head, and you cannot tell whether you got it right until you compile and walk it. So there is a layer above: you draw the map, one character per cell.
layer ground z=0 height=5 roof=no
legend
T t_spawn "T Spawn" spawn
L long_a "Long A" lane +0.8
/ ramp_up "Ramp" ramp
end
plan
##################
#TTTTTT#####LLLLL#
#TTTTTT///////LLL#
##################
end
Two rules do most of the work:
- A letter is floor;
#,.and space are not. - Where two different areas touch, that is a doorway — exactly as wide as the run of cells that touch. Where they do not touch, there is a wall.
Openings need no declaration, because a gap you draw in a wall is the opening. Areas are not rectangles either — each letter's cells are decomposed into maximal rectangles on the way out, so an L-shaped site costs nothing.
380 floor plans in three sets. maps/cs/ holds 111 real competitive layouts
and maps/traced/ another 110: their footprints are traced from published
radar images and their elevation comes from the radar's own shading.
maps/hand/ holds 159 originals authored directly in the DSL. Every one of
them passes the same gates as a hand-authored level, because that is what they
are.
python3 tools/trace_radar.py "reference/cs-maps/Mirage/Mirage - Radar.png" \
--cells 48 --bands 5 --out maps/traced/mirage.plan # footprint
node tools/finalize.ts # elevation + gates
node tools/plans.ts # compile + reportreference/cs-maps/ is the complete source archive the traces came from — 475
images across 234 map pages, covering CS 1.6 through CS2, official and
community, including cut and unreleased maps. 140 of those are radars (122
single, plus 9 upper/lower pairs for the two-storey maps); the rest are
screenshots and logos kept as provenance rather than as inputs. index.tsv
records which map each file belongs to and where it came from.
Not every radar becomes a map. 17 are Danger Zone or open-terrain layouts whose radars have no distinguishable background, so a trace of them is one solid rectangle — they are excluded rather than shipped as a filled square.
These are topology reconstructions, not measured copies — proportions are approximate, and no assets, textures, models or map files from any shipped game are used or reproduced.
npm run compile # build + validate every authored level, write artifacts
npm run gen # the procedural generator, still used as a fallback
npm test # 244 assertionsTwo rules, applied everywhere:
- One owner per boundary edge. A wall between two rooms belongs to exactly one of them. Overlaps are impossible rather than cleaned up afterwards.
- One owner per junction square — the same rule, one dimension down.
Everything else follows. Ramps inset for a wall that already exists rather than burying a stringer inside it; risers seal every open elevation change so you cannot walk off a ledge into the void; a flight's ends live in the two areas it joins so the compiler can read their real heights.
The validators are not warnings. generateLevel and loadAuthored both throw
rather than hand back a level that fails one, so a broken floor cannot reach a
player.