Dev Diary

Dev Diary #1:
Building the Avalon Game Engine from Scratch

When we decided to build Avalon, one of the first and most fundamental choices we faced was this: use an existing game framework, or build the engine ourselves in raw JavaScript? The popular options — Phaser, Babylon.js, even lightweight libraries like LittleJS — were all genuinely good tools. We chose none of them. Here is why, and what we built instead.

The reason was not pride or a fetish for reinvention. It was the nature of the game itself. Avalon is not a platformer, not a shooter, not a top-down dungeon crawler in the conventional sense. It is a browser RPG built almost entirely around text, state, and narrative logic. The overhead of a rendering-focused framework would have been considerable, and the specific needs of the engine — managing deeply nested game states, persisting character data across sessions, handling complex NPC dialogue trees — were things a bespoke system could address more cleanly than a general-purpose library.

So we started from nothing. Over the course of about three weeks, the Avalon engine took shape. This is the story of how it works.


The StateMachine

The core of the engine is a finite state machine. At any given moment, the game is in exactly one of a defined set of states: TITLE, CHARACTER_SELECT, EXPLORATION, DIALOGUE, COMBAT, INVENTORY, CUTSCENE, or GAME_OVER. The StateMachine is a single object with three responsibilities: it knows the current state, it knows the valid transitions from that state, and it fires lifecycle hooks when transitioning.

Each state has an onEnter function and an onExit function. When the player initiates combat by encountering an enemy in the exploration layer, the machine calls EXPLORATION.onExit(), which cleans up the exploration UI and pauses the world tick, and then calls COMBAT.onEnter(), which initialises the combat instance, populates the enemy data, and renders the combat interface.

The elegance of this pattern is that states are self-contained. The DIALOGUE state does not need to know anything about what was happening in EXPLORATION. It receives the dialogue tree it has been handed and runs it. When it resolves, it returns control to whatever state requested it. This composability made it straightforward to stack states — for example, opening the inventory from within an NPC conversation creates a DIALOGUE → INVENTORY → DIALOGUE sequence that resolves cleanly without any global state leakage.


The Renderer

The Renderer is, deliberately, not doing very much. Avalon's interface is HTML and CSS, not a canvas. The "rendering" the Renderer handles is DOM manipulation: updating text nodes, toggling visibility classes, injecting HTML fragments into templated regions of the page.

This was a deliberate choice with real advantages. HTML and CSS are extraordinarily good at responsive layout, accessibility, and text rendering. By keeping the display layer as standard web technology, we got mobile-friendly layouts, screen-reader compatibility, and theme support almost for free. The CSS custom properties that drive Avalon's visual theming — the deep purples, the gold accents, the typography — are trivially overridable when the player unlocks a new UI skin in the shop.

The Renderer maintains a set of "regions" — named DOM containers — and exposes a simple API: render(region, templateId, data). It finds the template, processes the data against it using a minimal string interpolation function, and injects the result into the named container. There is no virtual DOM, no diffing algorithm. Updates are targeted and infrequent enough that full-region replacement is never a performance problem.


The Game Loop

Many browser games run on requestAnimationFrame, firing 60 times per second. Avalon does not. Avalon's world updates are event-driven, not time-driven. Nothing happens until the player acts.

However, certain systems do require time: the world tick that causes NPCs to move between locations, the cooldown timers on certain abilities, the real-time countdown on timed encounters. For these, we use a single global tick running at one-second intervals via setInterval. Every registered tick listener receives the current timestamp and can decide whether it needs to act.

The game loop itself — the thing that responds to player input and drives state changes — is not a loop at all in the traditional sense. It is an event dispatcher. Player actions are emitted as typed events with payloads. The current state's action handler receives the event, validates it, and either executes it or returns an error message. This means invalid actions (trying to flee when flee is on cooldown, opening inventory during a cutscene) are handled uniformly by the state layer without any ad-hoc conditional logic scattered through the input handlers.


Save System and Data Persistence

Avalon's save system was one of the trickier engineering problems, not because the serialisation is complex — it isn't, it's just JSON.stringify — but because deciding what to save required careful thought about what constitutes game state.

We ended up with three layers of persistence. The player object — class, stats, inventory, quest log, reputation values — is the core save. The world state — which NPCs have been spoken to, which quests are active, which doors are open — is a second layer. The third layer is the narrative flag registry: a flat key-value store of booleans and integers that tracks every consequential player choice. This registry is what allows the game to remember, three quests later, that you chose to spare a particular NPC and adjust its dialogue accordingly.

All three layers are serialised to a single JSON object and written to localStorage. Three save slots are maintained. The autosave runs on a five-minute interval and at every state transition that constitutes a meaningful checkpoint.


What We Learned

Building the engine from scratch took longer than using an existing framework would have. It also produced something that fits the specific shape of the game exactly, without the friction of working around another system's assumptions.

The two things we would do differently: we would write the StateMachine's transition graph as a declarative configuration object from the beginning, rather than encoding transitions as procedural code in each state — the refactor was painful. And we would build the narrative flag registry as a proper typed schema from day one rather than growing it organically, because the flat key-value store works fine at two hundred flags and becomes unwieldy at eight hundred.

Both are problems we are solving for Phase 2. For now, the engine does what it needs to do: it runs Avalon, in a browser, with no dependencies, on any device with a modern JavaScript engine.

Next week: how we designed the combat system, balanced the numbers, and made fleeing actually feel like a decision rather than an admission of defeat.

✦     ✦     ✦

▶ Play What We Built

The Engine is Running. The Portal is Open.

Everything described in this article is live and running in your browser right now. No download, no login — just the game.

Play Avalon →
✦ ✦ ✦

Join the Circle

Receive the Whispers

New dev diaries, story drops, and Avalon lore — delivered to your inbox.

▶ Play Avalon