Copperfall (Placeholder)
A custom 2D JavaScript Canvas game engine and adventure prototype.
From-scratch 2D engine featuring a fixed-timestep ECS loop, parallax renderer, Web Audio reactive soundtrack, and a narrative-driven prototype built on top.

Quick Stats
Copperfall Engine
Copperfall is in active development. This write-up covers the engine architecture and the vertical-slice prototype — a second pass will land once the first full chapter is done.
Copperfall started as a weekend experiment: could I build a 2D game engine from scratch in vanilla JavaScript without reaching for a framework? Fourteen months later it has a renderer, a physics layer, a Web Audio reactive soundtrack, a dialogue runner, and the skeleton of a short adventure game.
Why build an engine?
Existing engines are powerful but opaque. Every bug feels like archaeology — stepping through someone else's assumptions to find your own mistake. Writing Copperfall gave me direct ownership of every abstraction, which makes debugging fast and architectural decisions intentional.
The constraint also teaches. When you can't import Physics from 'matter-js', you learn what physics actually costs.
Architecture
The ECS loop

The simulation runs on a fixed timestep to keep physics deterministic regardless of frame rate. A variable-rate render step interpolates between the last two simulation states to avoid judder.
Renderer
The Canvas 2D renderer handles:
- Sprite batching with a dirty-rect system to skip unchanged regions.
- Parallax layers with configurable depth ratios — background scrolls at 0.2× world speed.
- Screen shake via a decaying offset accumulator applied before the camera transform.
- A lightweight post-processing pass using
ctx.filterfor chromatic aberration on hit frames.
Component queries
Components are stored in typed arrays, not objects, so iteration stays cache-friendly. Systems declare their component signature as a bitmask and the world returns only matching entities.
TransformComponent— position, rotation, scale.SpriteComponent— texture, frame index, animation state.ColliderComponent— AABB with optional offset.ScriptComponent— update function attached directly to the entity.
Web Audio layer
The soundtrack reacts to game state using the Web Audio API's GainNode graph. Each music layer — melody, bass, percussion — has its own gain controlled by the current zone and tension level.
The reactive system works by:
- Zone entry — crossfade gains over 2 s to the target zone mix.
- Combat — spike percussion gain to 1.0, mute ambient pads.
- Dialogue — duck all gains by 60 % and bring up the lead melody.
Dialogue runner
The narrative layer reads from a simple YAML-based script format and drives on-screen text via a coroutine-style async iterator. Characters have portraits, voice variants, and timed pauses baked in.
- speaker: Asha
line: "You came back."
pause: 0.6
- speaker: Asha
line: "I wasn't sure you would."
portrait: asha-uncertain
The entry point is dialogue.play(scriptId) — returns a Promise that resolves when the final line is dismissed.
Roadmap
- Finish level-streaming approach for large maps without hitches.
- Add save/load via
IndexedDBfor persistent run state. - Write the second chapter of the prototype game.
- Publish the engine separately as an open-source library.
The constraint of "no frameworks" isn't masochism — it's the fastest path to understanding what frameworks actually solve.
Have thoughts?
Curious what others see or think
Feel free to reach out or leave feedback
Share FeedbackPrefer email? joshuatjhie@pm.me