Doom in the browser, in pure JavaScript — no framework, no build step, no server side.
Play it: doom.spipu.net
Spipu-Doom (index.html) ships as a PWA and converts any Doom-format WAD on the fly, entirely in the browser: WAD files are stored in IndexedDB, parsed in JS (geometry, textures, doors, lifts, switches, monsters, weapons, sounds and music) and turned directly into in-memory engine objects — no server-side conversion, no generated files. Everything game-specific lives in game profiles (Doom, Freedoom, Heretic…) auto-detected from the WAD content, so other Doom-engine games plug in without touching the converter.
It runs on Spipu3D (js/engine/), the 3D engine written for it: renderer, FPS physics, entities, inputs and audio, with no knowledge of Doom whatsoever. The demos in _examples/ drive that same engine on other scenes.
Nothing to download and nothing to configure — the game runs in the page.
freedoom1.wad or freedoom2.wad with Local file. Add by URL takes any Doom-format WAD reachable over HTTP with CORS enabled. Doom, Doom II and Heretic WADs work the same way, if you own them.Once it is installed and a WAD is stored, it needs no network at all: the app files are Service-Worker cached and the WAD lives in IndexedDB.
It also updates itself: every launch checks for a new version and, when there is one, re-downloads the app whole before starting — nothing to reinstall, no cache to clear by hand. Stored WADs and saved games are untouched by an update.
A modern browser (Chrome, Firefox, Edge) and any static HTTP server (Apache, Nginx, python3 -m http.server) — no build step, nothing server side:
cd website
python3 -m http.server 8080
Then open http://localhost:8080 and follow steps 2 and 3 above.
ESC freezes the game under a translucent overlay — resume, load, save, options, leave the level.js/doom/wad/profile/): everything game-specific — things, specials, weapons, monsters, progression, skies, sounds, HUD — is profile data, auto-detected from the WAD content. Doom, Doom II, TNT: Evilution, The Plutonia Experiment, Freedoom and Heretic are recognised and playable, each with its own level names; unknown WADs fall back to the doom-format baseline.js/doom/wad/convert/): geometry, textures, animations and movers are instantiated directly into in-memory engine objects — no generated file, no server. Middle textures, missing textures and the known defects of the original maps are handled the way the hardware ports do.UMAPINFO overrides honoured) through a tally modal — time, enemies, items, secrets — followed by the game's own chapter texts (from the WAD when it tells its own story, else the translated catalog).Intl.Keyboard defaults below are physical key positions (WASD = ZQSD on an AZERTY layout) and every one of them can be remapped in the Options modal (from a WAD's menu), one key per action — except ESC, the fixed pause key. The lights, game and world demos share this input stack, at the default keys: they answer to the gamepad and to the touch pad the same way, each keeping only the controls it has a use for.
| Keyboard / mouse | Gamepad | Action |
|---|---|---|
| WASD | Left stick | Move / strafe (analog on the stick) |
| Mouse (click canvas first) | Right stick | Look around |
| Left Shift | Button 1 | Jump |
| Left Ctrl | Button 0 | Crouch — careful: holding it with the key that types q is Ctrl+Q, which quits Firefox (a browser-privileged shortcut); remap if it bites you |
| E | Button 3 | Interact (open door, trigger lift or switch) |
| Left click / Q | Button 2 / right trigger | Fire the active weapon |
| ESC | Button 9 | Pause menu over the frozen game (not remappable) |
| F / G | Buttons 4 / 5 | Previous / next weapon (wrapping) |
| H | — | Toggle the game HUD ↔ debug overlay (keyboard only) |
| Tab | D-pad up | Show / hide the automap over the game |
| Left Alt | — | Walk slowly (sticks do it through partial deflection) |
| IJKL | — | Look around — keyboard fallback when the mouse / Pointer Lock is unavailable |
| O | — | Debug cheat (not remappable): grant the full kit |
The gamepad is only visible to the page after a button has been pressed on it (browser privacy rule); it then takes priority over keyboard+mouse. Touch-only devices select the virtual gamepad (see Inputs above). On iOS the touch mapping and the menus stay aligned with the display across device rotation.
js/engine/ is a standalone 3D engine with no external dependency: it renders textured, lit 3D objects entirely in the browser through the HTML5 <canvas> API, and carries a full FPS physics engine (collision detection, gravity, jumping, crouching, animated objects) for one or several users sharing a world, each moved by its own command and blocking the others. It never depends on js/doom/: it exposes parameterisable primitives (depth shading, per-instance light and render offset, external forces, screen sprites…) that the game layer feeds with its own constants.
Four rendering modes are available, selectable in the game from the Display options page and via the Renderer selector on _examples/objects.html:
| Mode | Description |
|---|---|
webgl | WebGL — GPU shaders, z-buffer, texture mapping (default, falls back to full if unavailable) |
full | Per-pixel z-buffer with Gouraud shading and texture mapping |
flat | Painter's algorithm with flat shading — one colour per face, the average colour of its texture dimmed by the sector light — on a neutral grey backdrop without sky |
fast | Wireframe — no lighting, canvas 2D paths only, on the same grey backdrop |
In the two textureless modes the game paints enemies red, every pickup blue — keys included, and the exploding barrel counts as a body — every moving part (doors, lifts, floors, stairs) yellow and the switches magenta, so a scene stays readable without its sprites. Both draw the whole frame in one depth-sorted pass, so a body behind a wall stays behind it instead of being painted over the map.
Whatever the mode, instances are frustum-culled in camera space before any per-vertex work; the static level map is one single object, always drawn whole.
| Page | Description |
|---|---|
_examples/index.html | Home page — links to all demos |
_examples/objects.html | Object viewer — pick an object, resolution and renderer |
_examples/example.html | Static render of the Lotus F1 |
_examples/lights.html | Coloured light sources demo — move them around |
_examples/game.html | Interactive van — drive it |
_examples/world.html | First-person navigation inside a 3D labyrinth |
_examples/pairing-test.html | Network test bench — devices pair by QR code (or two tabs in loopback), link over WebRTC, measure their ping and exchange chunked bursts |
_examples/pairing-game.html | Network game — the main drives the van through the synchronous turn cycle (a state to every sub, a command back from each, then the next turn), every paired sub watches it live with its ping and the turn rate |
Lights, game and world run in a 16:9 letterboxed fullscreen and take keyboard, gamepad or touch pad, exactly like the game.
Demo objects (cube, sphere, lotus, van…) and the labyrinth world live in _examples/assets/.
The static collision geometry is indexed once per level in a uniform XZ spatial grid, so every floor/wall/ray query only tests the triangles of the cells it touches. Dynamic movers stay on a linear scan.
website/
├── index.html Spipu-Doom shell (PWA)
├── appServiceWorker.js Service Worker — cache-first, offline (must stay at webroot: SW scope)
├── ping.json Install/update/start tracking json — hit only
├── css/ Shell + menu styles
├── assets/uzdoom/ UZDoom impact-decal graphics + finale texts (GPL v3 — own LICENSE.md + README.md)
├── assets/spipu/ Our own graphics: the generic splash masks, colourised at level load
├── _examples/ Spipu3D demos + their assets and bootstrap definitions
└── js/
├── webapp/ Generic webapp layer — bootstrap/versioning, IndexedDB wrapper, translation catalog, content hash, wake lock
│ ├── net/ Peer-to-peer network layer — WebRTC and loopback links, compact signals, pairing codes and flows, messages, ping, star sessions
│ └── qr/ QR code writing, camera scanning, camera probe and the pairing view (code shown, code read)
├── lib/libadlmidi/ Vendored libADLMIDI-JS OPL3 synthesizer (LGPL v3 — own LICENSE.md + modification README.md)
├── lib/zxing-wasm/ Vendored zxing-wasm QR code reader and writer (MIT, Apache-2.0, BSD-3 — own LICENSE.md + README.md)
├── doom/ The Spipu-Doom game
│ ├── libBootstrap.json Doom bootstrap definition (version + file lists)
│ ├── doomGame.js Level lifecycle, game loop, menus of the running game, tally
│ ├── doomUser.js Player equipment state
│ ├── doomPlayer.js One player across its levels: its body, its weapon, the equipment it enters and carries
│ ├── doomPlayerBody.js A player's visible body in cooperative, as the others see it
│ ├── doomInertInstance.js An instance born in play that nothing touches (body replica, shot, effect, decal)
│ ├── doomSessionNotice.js The message a session shows over the game (waiting, host paused, respawn prompt, player left)
│ ├── doomPlayerRoster.js The players of a game, the local one among them
│ ├── doomLevelLoader.js The level build every device runs: the converted world and the visual banks it shows
│ ├── doomSimulation.js The level's world and the tic that moves it from one command per player: monsters, projectiles, saves
│ ├── doomMainRole.js The role of the device that simulates: the simulation and what only that device does
│ ├── doomSubRole.js The role of a device that follows another's game: applies each turn state and answers it
│ ├── doomLevelStats.js The level's statistics: secrets, kills and items found against their totals, per player too, the frags, level time
│ ├── doomTurnEvents.js Every one-shot event of a turn (sounds, effects, decals, teleports), emitted by the simulation
│ ├── doomTurnEventPlayer.js How a device plays those events
│ ├── doomItemRules.js What pickups, the starting loadout and the cheat give to a player
│ ├── doomPickupSpawner.js Brings a map pickup back into the level, the same on every device
│ ├── doomItemRespawnQueue.js The deathmatch items waiting to come back
│ ├── doomGameRules.js The mode-dependent questions (things built, spawn spots and keys, death menu or respawn, saves, cheat, screen sharing, cooperative, items staying, friendly fire, frags, players on the map)
│ ├── doomSinglePlayerRules.js Their single-player answers
│ ├── doomCoopRules.js Their cooperative answers
│ ├── doomDeathmatchRules.js Their deathmatch answers
│ ├── doomPresentation.js What a device shows of the game through one player: screen, HUD, weapon overlay, view effects, sound
│ ├── doomSettings.js Persistent settings (IndexedDB)
│ ├── doomTranslations.js Every user-facing text (en + fr + it + es)
│ ├── doomFinaleTexts.js Finale-text catalogs of the games (loaded from assets/)
│ ├── doomImageAssets.js Source pixels of every PNG drawn from outside the WAD (decals, splash masks)
│ ├── main.js Entry point
│ ├── save/ Save slots + level snapshot (deterministic rebuild + state patch)
│ ├── sound/ Game audio: WAD sound loading, logical-name catalog (profile SNDINFO tables), music orchestration
│ ├── object/ Immutable definitions (weapons, ammo, items, decorations, thing and item catalogs)
│ ├── monster/ Monster system: defs, 35 Hz driver, locomotion, senses, attacks, damage, boss deaths, the Icon of Sin, and the drawable body views with their renderer
│ ├── automap/ Level map: line model, state, and the vanilla BSP reveal
│ ├── hud/ Game HUD + debug overlay + automap layer
│ ├── menu/ DOM menu screens and modals (WAD list, episodes, multiplayer, options, text entry, pause and its multiplayer sub-menu, death, save slots, pairing, lobby), and their keyboard / gamepad navigation
│ ├── net/ Multiplayer of the game: pairing payload, lobby, main and sub sessions, device availability, the replicated turn state and commands with their binary codecs, the host's turn cycle and the replica applier
│ ├── weapon/ Weapon machinery: psprite machine, hitscan, projectiles, effects, decals, and the drawable weapon and projectile views with their renderers
│ └── wad/ WAD reading + IndexedDB storage, game profiles (profile/), on-the-fly converter (convert/)
└── engine/ Spipu3D — the game-agnostic 3D engine
├── libBootstrap.json Engine bootstrap definition (version + file lists)
├── engine3d.js Viewport, lights, render loop, frustum culling
├── collision.js FPS physics: spatially indexed triangles, box blockers, mover pressure
├── spatialGrid.js Uniform XZ grid over a static triangle set
├── entity/ Object3d, Billboard, Instance (keyframes/triggers/cycles), User, UserCommand, World, external forces
├── input/ Unified inputs: keyboard, mouse, gamepad, virtual touch gamepad, and the command sampler
├── interaction/ Interaction bases (switch modes once/timed/toggle)
├── loader/ URL or in-memory loaders (textures, objects, instances, interactions, world)
├── sound/ Audio primitives: shared AudioContext with music/effects buses, in-memory PCM samples, tone synthesis, music player over a swappable synth contract
├── hud/ HUD bases (debug overlay, screen flash)
└── renderer/ webgl / full / flat / fast renderers
The engine loaders accept either URLs (classic flow, used by the demo pages) or in-memory data (used by the WAD converter):
loader.reset();
loader.beginBatch(); // suspends the global finalize check
const texId = loader.textures().loadFromData(null, imageData); // ImageData
const objId = loader.objects().loadFromData('map', {textures, points, faces});
loader.instances().loadFromData(null, {...instanceData, object: objId});
loader.interactions().loadFromData(new DoomSwitchInteraction(...));
loader.world().loadFromData(definition); // user, background, lights
loader.setCallback(init);
loader.endBatch(); // finalizes everything once, fires init
Every page is loaded by the generic appBootstrap (global instance). Each library declares its files in a libBootstrap.json definition ({version, files: {assets, css, js}}); pages stack the definitions they need and register their entry point:
<div id="screen"></div>
<script src="/js/webapp/appBootstrap.js"></script>
<script>
function loadApp()
{
loader.world().load('./assets/world/definition.json');
loader.setCallback(init);
}
appBootstrap.disablePwaMode(); // demos only — doom keeps PWA mode
appBootstrap.addBootstrapDefinition('/js/engine/libBootstrap.json');
appBootstrap.addBootstrapDefinition('./assets/world.json');
appBootstrap.setReadyCallback(loadApp);
</script>
Versions are aggregated (v2.001|v1.018): a change in any stacked definition triggers a full update — in PWA mode the Service Worker clears its cache and re-downloads everything; in classic mode the page reloads with ?v= cache-busted URLs (appBootstrap.buildUrl).
function init() {
const world = loader.world().get();
screen = new ScreenManager('screen', { fullscreen: true }); // or { width, height }
// or { fullscreen: true, virtualWidth: 1920, virtualHeight: 1080 }
inputs = new Inputs().bindScreen(screen); // one single instance per page, rebound on each level
sampler = new InputCommandSampler(inputs); // the devices, turned into one command per turn
engine = new Engine3d(screen, new Object3dRendererList().getRenderer('webgl')); // binds itself to the screen
const hud = new HudDebug(engine)
.bindUser(world.getUser()).bindInputs(inputs)
.addDescription('(c)2026 Spipu')
;
screen.bindHud(hud);
engine.initFromWorld(world);
requestAnimationFrame(animate);
}
function animate(timestamp) {
engine.calculateDeltaTime(timestamp);
const dt = engine.getDeltaTime();
world.update(dt, new Map([[world.getUser(), sampler.collect(dt).sample()]])); // one command per user
engine.displayWorld(world, world.getUser()); // the camera looks through one user
screen.update(); // updates HUD overlay
requestAnimationFrame(animate);
}
After any file change, increment the version field of the libBootstrap.json of the modified library (engine, doom, or the demo's definition JSON). This drives both the PWA cache refresh and the classic-mode cache busting.
The upcoming work is tracked in ./NEXT-STEPS.md.
This program is distributed under the MIT License — see the ./LICENSE.md file, except:
website/assets/uzdoom/ directory (impact-decal graphics and finale texts taken from UZDoom), distributed under the GPL v3, with its own LICENSE.md and attribution README.website/js/lib/libadlmidi/ directory (the vendored libADLMIDI-JS music synthesizer), distributed under the LGPL v3, with its own LICENSE.md and attribution README.website/js/lib/zxing-wasm/ directory (the vendored QR code library), distributed under the MIT, Apache-2.0 and BSD-3-Clause licences of its three components, all permissive, with its own LICENSE.md.JavaScript
97.0%
HTML
2.1%
Doom in the browser, in pure JavaScript — no framework, no build step, no server side.
Play it: doom.spipu.net
Spipu-Doom (index.html) ships as a PWA and converts any Doom-format WAD on the fly, entirely in the browser: WAD files are stored in IndexedDB, parsed in JS (geometry, textures, doors, lifts, switches, monsters, weapons, sounds and music) and turned directly into in-memory engine objects — no server-side conversion, no generated files. Everything game-specific lives in game profiles (Doom, Freedoom, Heretic…) auto-detected from the WAD content, so other Doom-engine games plug in without touching the converter.
It runs on Spipu3D (js/engine/), the 3D engine written for it: renderer, FPS physics, entities, inputs and audio, with no knowledge of Doom whatsoever. The demos in _examples/ drive that same engine on other scenes.
Nothing to download and nothing to configure — the game runs in the page.
freedoom1.wad or freedoom2.wad with Local file. Add by URL takes any Doom-format WAD reachable over HTTP with CORS enabled. Doom, Doom II and Heretic WADs work the same way, if you own them.Once it is installed and a WAD is stored, it needs no network at all: the app files are Service-Worker cached and the WAD lives in IndexedDB.
It also updates itself: every launch checks for a new version and, when there is one, re-downloads the app whole before starting — nothing to reinstall, no cache to clear by hand. Stored WADs and saved games are untouched by an update.
A modern browser (Chrome, Firefox, Edge) and any static HTTP server (Apache, Nginx, python3 -m http.server) — no build step, nothing server side:
cd website
python3 -m http.server 8080
Then open http://localhost:8080 and follow steps 2 and 3 above.
ESC freezes the game under a translucent overlay — resume, load, save, options, leave the level.js/doom/wad/profile/): everything game-specific — things, specials, weapons, monsters, progression, skies, sounds, HUD — is profile data, auto-detected from the WAD content. Doom, Doom II, TNT: Evilution, The Plutonia Experiment, Freedoom and Heretic are recognised and playable, each with its own level names; unknown WADs fall back to the doom-format baseline.js/doom/wad/convert/): geometry, textures, animations and movers are instantiated directly into in-memory engine objects — no generated file, no server. Middle textures, missing textures and the known defects of the original maps are handled the way the hardware ports do.UMAPINFO overrides honoured) through a tally modal — time, enemies, items, secrets — followed by the game's own chapter texts (from the WAD when it tells its own story, else the translated catalog).Intl.Keyboard defaults below are physical key positions (WASD = ZQSD on an AZERTY layout) and every one of them can be remapped in the Options modal (from a WAD's menu), one key per action — except ESC, the fixed pause key. The lights, game and world demos share this input stack, at the default keys: they answer to the gamepad and to the touch pad the same way, each keeping only the controls it has a use for.
| Keyboard / mouse | Gamepad | Action |
|---|---|---|
| WASD | Left stick | Move / strafe (analog on the stick) |
| Mouse (click canvas first) | Right stick | Look around |
| Left Shift | Button 1 | Jump |
| Left Ctrl | Button 0 | Crouch — careful: holding it with the key that types q is Ctrl+Q, which quits Firefox (a browser-privileged shortcut); remap if it bites you |
| E | Button 3 | Interact (open door, trigger lift or switch) |
| Left click / Q | Button 2 / right trigger | Fire the active weapon |
| ESC | Button 9 | Pause menu over the frozen game (not remappable) |
| F / G | Buttons 4 / 5 | Previous / next weapon (wrapping) |
| H | — | Toggle the game HUD ↔ debug overlay (keyboard only) |
| Tab | D-pad up | Show / hide the automap over the game |
| Left Alt | — | Walk slowly (sticks do it through partial deflection) |
| IJKL | — | Look around — keyboard fallback when the mouse / Pointer Lock is unavailable |
| O | — | Debug cheat (not remappable): grant the full kit |
The gamepad is only visible to the page after a button has been pressed on it (browser privacy rule); it then takes priority over keyboard+mouse. Touch-only devices select the virtual gamepad (see Inputs above). On iOS the touch mapping and the menus stay aligned with the display across device rotation.
js/engine/ is a standalone 3D engine with no external dependency: it renders textured, lit 3D objects entirely in the browser through the HTML5 <canvas> API, and carries a full FPS physics engine (collision detection, gravity, jumping, crouching, animated objects) for one or several users sharing a world, each moved by its own command and blocking the others. It never depends on js/doom/: it exposes parameterisable primitives (depth shading, per-instance light and render offset, external forces, screen sprites…) that the game layer feeds with its own constants.
Four rendering modes are available, selectable in the game from the Display options page and via the Renderer selector on _examples/objects.html:
| Mode | Description |
|---|---|
webgl | WebGL — GPU shaders, z-buffer, texture mapping (default, falls back to full if unavailable) |
full | Per-pixel z-buffer with Gouraud shading and texture mapping |
flat | Painter's algorithm with flat shading — one colour per face, the average colour of its texture dimmed by the sector light — on a neutral grey backdrop without sky |
fast | Wireframe — no lighting, canvas 2D paths only, on the same grey backdrop |
In the two textureless modes the game paints enemies red, every pickup blue — keys included, and the exploding barrel counts as a body — every moving part (doors, lifts, floors, stairs) yellow and the switches magenta, so a scene stays readable without its sprites. Both draw the whole frame in one depth-sorted pass, so a body behind a wall stays behind it instead of being painted over the map.
Whatever the mode, instances are frustum-culled in camera space before any per-vertex work; the static level map is one single object, always drawn whole.
| Page | Description |
|---|---|
_examples/index.html | Home page — links to all demos |
_examples/objects.html | Object viewer — pick an object, resolution and renderer |
_examples/example.html | Static render of the Lotus F1 |
_examples/lights.html | Coloured light sources demo — move them around |
_examples/game.html | Interactive van — drive it |
_examples/world.html | First-person navigation inside a 3D labyrinth |
_examples/pairing-test.html | Network test bench — devices pair by QR code (or two tabs in loopback), link over WebRTC, measure their ping and exchange chunked bursts |
_examples/pairing-game.html | Network game — the main drives the van through the synchronous turn cycle (a state to every sub, a command back from each, then the next turn), every paired sub watches it live with its ping and the turn rate |
Lights, game and world run in a 16:9 letterboxed fullscreen and take keyboard, gamepad or touch pad, exactly like the game.
Demo objects (cube, sphere, lotus, van…) and the labyrinth world live in _examples/assets/.
The static collision geometry is indexed once per level in a uniform XZ spatial grid, so every floor/wall/ray query only tests the triangles of the cells it touches. Dynamic movers stay on a linear scan.
website/
├── index.html Spipu-Doom shell (PWA)
├── appServiceWorker.js Service Worker — cache-first, offline (must stay at webroot: SW scope)
├── ping.json Install/update/start tracking json — hit only
├── css/ Shell + menu styles
├── assets/uzdoom/ UZDoom impact-decal graphics + finale texts (GPL v3 — own LICENSE.md + README.md)
├── assets/spipu/ Our own graphics: the generic splash masks, colourised at level load
├── _examples/ Spipu3D demos + their assets and bootstrap definitions
└── js/
├── webapp/ Generic webapp layer — bootstrap/versioning, IndexedDB wrapper, translation catalog, content hash, wake lock
│ ├── net/ Peer-to-peer network layer — WebRTC and loopback links, compact signals, pairing codes and flows, messages, ping, star sessions
│ └── qr/ QR code writing, camera scanning, camera probe and the pairing view (code shown, code read)
├── lib/libadlmidi/ Vendored libADLMIDI-JS OPL3 synthesizer (LGPL v3 — own LICENSE.md + modification README.md)
├── lib/zxing-wasm/ Vendored zxing-wasm QR code reader and writer (MIT, Apache-2.0, BSD-3 — own LICENSE.md + README.md)
├── doom/ The Spipu-Doom game
│ ├── libBootstrap.json Doom bootstrap definition (version + file lists)
│ ├── doomGame.js Level lifecycle, game loop, menus of the running game, tally
│ ├── doomUser.js Player equipment state
│ ├── doomPlayer.js One player across its levels: its body, its weapon, the equipment it enters and carries
│ ├── doomPlayerBody.js A player's visible body in cooperative, as the others see it
│ ├── doomInertInstance.js An instance born in play that nothing touches (body replica, shot, effect, decal)
│ ├── doomSessionNotice.js The message a session shows over the game (waiting, host paused, respawn prompt, player left)
│ ├── doomPlayerRoster.js The players of a game, the local one among them
│ ├── doomLevelLoader.js The level build every device runs: the converted world and the visual banks it shows
│ ├── doomSimulation.js The level's world and the tic that moves it from one command per player: monsters, projectiles, saves
│ ├── doomMainRole.js The role of the device that simulates: the simulation and what only that device does
│ ├── doomSubRole.js The role of a device that follows another's game: applies each turn state and answers it
│ ├── doomLevelStats.js The level's statistics: secrets, kills and items found against their totals, per player too, the frags, level time
│ ├── doomTurnEvents.js Every one-shot event of a turn (sounds, effects, decals, teleports), emitted by the simulation
│ ├── doomTurnEventPlayer.js How a device plays those events
│ ├── doomItemRules.js What pickups, the starting loadout and the cheat give to a player
│ ├── doomPickupSpawner.js Brings a map pickup back into the level, the same on every device
│ ├── doomItemRespawnQueue.js The deathmatch items waiting to come back
│ ├── doomGameRules.js The mode-dependent questions (things built, spawn spots and keys, death menu or respawn, saves, cheat, screen sharing, cooperative, items staying, friendly fire, frags, players on the map)
│ ├── doomSinglePlayerRules.js Their single-player answers
│ ├── doomCoopRules.js Their cooperative answers
│ ├── doomDeathmatchRules.js Their deathmatch answers
│ ├── doomPresentation.js What a device shows of the game through one player: screen, HUD, weapon overlay, view effects, sound
│ ├── doomSettings.js Persistent settings (IndexedDB)
│ ├── doomTranslations.js Every user-facing text (en + fr + it + es)
│ ├── doomFinaleTexts.js Finale-text catalogs of the games (loaded from assets/)
│ ├── doomImageAssets.js Source pixels of every PNG drawn from outside the WAD (decals, splash masks)
│ ├── main.js Entry point
│ ├── save/ Save slots + level snapshot (deterministic rebuild + state patch)
│ ├── sound/ Game audio: WAD sound loading, logical-name catalog (profile SNDINFO tables), music orchestration
│ ├── object/ Immutable definitions (weapons, ammo, items, decorations, thing and item catalogs)
│ ├── monster/ Monster system: defs, 35 Hz driver, locomotion, senses, attacks, damage, boss deaths, the Icon of Sin, and the drawable body views with their renderer
│ ├── automap/ Level map: line model, state, and the vanilla BSP reveal
│ ├── hud/ Game HUD + debug overlay + automap layer
│ ├── menu/ DOM menu screens and modals (WAD list, episodes, multiplayer, options, text entry, pause and its multiplayer sub-menu, death, save slots, pairing, lobby), and their keyboard / gamepad navigation
│ ├── net/ Multiplayer of the game: pairing payload, lobby, main and sub sessions, device availability, the replicated turn state and commands with their binary codecs, the host's turn cycle and the replica applier
│ ├── weapon/ Weapon machinery: psprite machine, hitscan, projectiles, effects, decals, and the drawable weapon and projectile views with their renderers
│ └── wad/ WAD reading + IndexedDB storage, game profiles (profile/), on-the-fly converter (convert/)
└── engine/ Spipu3D — the game-agnostic 3D engine
├── libBootstrap.json Engine bootstrap definition (version + file lists)
├── engine3d.js Viewport, lights, render loop, frustum culling
├── collision.js FPS physics: spatially indexed triangles, box blockers, mover pressure
├── spatialGrid.js Uniform XZ grid over a static triangle set
├── entity/ Object3d, Billboard, Instance (keyframes/triggers/cycles), User, UserCommand, World, external forces
├── input/ Unified inputs: keyboard, mouse, gamepad, virtual touch gamepad, and the command sampler
├── interaction/ Interaction bases (switch modes once/timed/toggle)
├── loader/ URL or in-memory loaders (textures, objects, instances, interactions, world)
├── sound/ Audio primitives: shared AudioContext with music/effects buses, in-memory PCM samples, tone synthesis, music player over a swappable synth contract
├── hud/ HUD bases (debug overlay, screen flash)
└── renderer/ webgl / full / flat / fast renderers
The engine loaders accept either URLs (classic flow, used by the demo pages) or in-memory data (used by the WAD converter):
loader.reset();
loader.beginBatch(); // suspends the global finalize check
const texId = loader.textures().loadFromData(null, imageData); // ImageData
const objId = loader.objects().loadFromData('map', {textures, points, faces});
loader.instances().loadFromData(null, {...instanceData, object: objId});
loader.interactions().loadFromData(new DoomSwitchInteraction(...));
loader.world().loadFromData(definition); // user, background, lights
loader.setCallback(init);
loader.endBatch(); // finalizes everything once, fires init
Every page is loaded by the generic appBootstrap (global instance). Each library declares its files in a libBootstrap.json definition ({version, files: {assets, css, js}}); pages stack the definitions they need and register their entry point:
<div id="screen"></div>
<script src="/js/webapp/appBootstrap.js"></script>
<script>
function loadApp()
{
loader.world().load('./assets/world/definition.json');
loader.setCallback(init);
}
appBootstrap.disablePwaMode(); // demos only — doom keeps PWA mode
appBootstrap.addBootstrapDefinition('/js/engine/libBootstrap.json');
appBootstrap.addBootstrapDefinition('./assets/world.json');
appBootstrap.setReadyCallback(loadApp);
</script>
Versions are aggregated (v2.001|v1.018): a change in any stacked definition triggers a full update — in PWA mode the Service Worker clears its cache and re-downloads everything; in classic mode the page reloads with ?v= cache-busted URLs (appBootstrap.buildUrl).
function init() {
const world = loader.world().get();
screen = new ScreenManager('screen', { fullscreen: true }); // or { width, height }
// or { fullscreen: true, virtualWidth: 1920, virtualHeight: 1080 }
inputs = new Inputs().bindScreen(screen); // one single instance per page, rebound on each level
sampler = new InputCommandSampler(inputs); // the devices, turned into one command per turn
engine = new Engine3d(screen, new Object3dRendererList().getRenderer('webgl')); // binds itself to the screen
const hud = new HudDebug(engine)
.bindUser(world.getUser()).bindInputs(inputs)
.addDescription('(c)2026 Spipu')
;
screen.bindHud(hud);
engine.initFromWorld(world);
requestAnimationFrame(animate);
}
function animate(timestamp) {
engine.calculateDeltaTime(timestamp);
const dt = engine.getDeltaTime();
world.update(dt, new Map([[world.getUser(), sampler.collect(dt).sample()]])); // one command per user
engine.displayWorld(world, world.getUser()); // the camera looks through one user
screen.update(); // updates HUD overlay
requestAnimationFrame(animate);
}
After any file change, increment the version field of the libBootstrap.json of the modified library (engine, doom, or the demo's definition JSON). This drives both the PWA cache refresh and the classic-mode cache busting.
The upcoming work is tracked in ./NEXT-STEPS.md.
This program is distributed under the MIT License — see the ./LICENSE.md file, except:
website/assets/uzdoom/ directory (impact-decal graphics and finale texts taken from UZDoom), distributed under the GPL v3, with its own LICENSE.md and attribution README.website/js/lib/libadlmidi/ directory (the vendored libADLMIDI-JS music synthesizer), distributed under the LGPL v3, with its own LICENSE.md and attribution README.website/js/lib/zxing-wasm/ directory (the vendored QR code library), distributed under the MIT, Apache-2.0 and BSD-3-Clause licences of its three components, all permissive, with its own LICENSE.md.JavaScript
97.0%
HTML
2.1%