Vanilla JS · zero build · zero dependencies

The Aethelgard Engine — a browser RPG engine that ships as plain files.

Aethelgard Engine is the engine behind Aethelgard: Chronicles of Fate, a deep single-player fantasy RPG that runs entirely in the browser. There is no bundler, no framework and no package to install — just HTML, CSS and ES5-ish JavaScript, loaded in script order, fully offline.

Free & fully offline — nothing to install. The Android .apk is sideload-only (allow installs from unknown sources).

License Stars Last commit Code size
no build step 0 dependencies MIT code PWA / offline 50 JS files

What's inside

Engine decisions, not accidents

Every design choice here is aimed at the same goal: a game engine small enough to read end to end, and open enough that a mod can reshape it without touching the core.

Zero build, zero dependencies

ES5-ish IIFEs loaded in script order. Nothing is imported, nothing is bundled, nothing is transpiled. Open the folder over any static host and it runs.

no bundler · no framework

Data and logic kept apart

Everything under js/data/ is pure data with no side effects. That is exactly what makes mods simple: a mod supplies the same shapes and they merge.

js/data/ · Object.assign(Game, …)

One function draws all the art

The whole game paints through Game.sprite(key, fallback). An art overhaul is a graphics pack that registers images for sprite keys — anything it doesn't cover keeps its emoji.

Game.sprite(key, fallback)

A procedural soundtrack

No audio files ship at all. js/music.js improvises every mood live from a scale through the Web Audio API, sitting under the sound effects.

Web Audio · no audio files

A drop-in mod system

Drop a .js file in mods/ and the engine finds it at boot. Mods declare their impact, are compatibility-checked, and can be switched off with safe mode.

mods/*.js · additive | story | overhaul

A deterministic HTML renderer

No virtual DOM. Each view returns an HTML string that render() assigns to #view-container — so overriding a view is one function. Cheap number updates are split from panel rebuilds.

render() → #view-container

How it works

One global, loaded in order

Every file is an IIFE that hangs its public functions off a single global, window.Game (aliased G). Load order is the order of the <script> tags in index.html. That is the whole dependency system — and it is why a mod can add a chapter, a class or a language without the core changing.

treerepo layout
index.html markup + script order
style.css all styling, CSS custom properties
manifest.json PWA manifest
sw.js offline cache
js/data/ pure data — no logic, no side effects
js/*.js logic — one concern per file
js/views/ one module per tab
js/boot.js startup sequence (runs last)
mods/ drop-in content, auto-discovered
tools/ audit, verifier, simulations

// the module contract — every logic file looks like this

/* js/graphics.js — sprite packs and image rendering */
"use strict";
(function () {
  const G = window.Game;

  /* the whole game draws its art through this one call */
  G.sprite = function (key, fallback) {
    const s = G.SPRITES[key];
    return s && s.src ? img(s) : fallback;  // emoji if no pack
  };

  G.registerGraphicsPack = function (pack) {
    G.GRAPHICS_PACKS[pack.id] = pack;      // a mod calls this
  };
})();

A graphics pack is just a mod that registers sprites for the engine's sprite keys — enemy.<id>, class.<id>, item.<id>, loc.<id> and so on.

Quickstart

  1. Clone and serve

    No install step. Any static host works — a tiny local server is enough to get the service worker and PWA install.

    # extract/clone the whole folder first
    git clone https://github.com/drsamonline/aethelgard-rpg-engine.git
    cd Aethelgard-Engine
    python3 -m http.server 8080
    # → http://localhost:8080
  2. Run the checks

    Plain Node and Python — there is nothing to install. CI runs the syntax check, JSON validation and the three suites on every push.

    node --check js/core.js
    node test6.js && node test7.js && node test8.js
    node tools/verify.js        # end-to-end
    node tools/sims.js 1 12     # 12 simulations
    node js/validate.js         # content validator
    python3 tools/static-audit.py .
  3. Extend it

    Add content to js/data/, or drop a mod into mods/ and change nothing in the core.

    window.AETHELGARD_MODS =
      window.AETHELGARD_MODS || [];
    
    window.AETHELGARD_MODS.push({
      id: "my-mod", name: "My Mod",
      version: "1.0.0", author: "You",
      impact: "additive"   // additive | story | overhaul
    });
docs/ARCHITECTURE.mdthe rules
  • One global. Public functions live on window.Game (aliased G). Nothing is imported.
  • Script order is the dependency graph. boot.js runs last.
  • No virtual DOM. A view returns an HTML string; render() assigns it to #view-container.
  • Split the render. updateStatus() is cheap and updates numbers; renderView() rebuilds the panel. The 3-second tick only rebuilds tabs in G.LIVE_TABS.
  • Synchronous storage. G.store wraps localStorage with an in-memory cache, so a save is durable the instant it is written — a save is about 13 KB.
  • Mode-namespaced saves. Slot keys carry the game mode (aethelgard_v7_slot1 vs aethelgard_v7_villain_slot1), so two games never overwrite each other.

The four subsystems

Renderer · Input · Audio · Scene graph

The engine's runtime concerns live in their own files, wired together only through G. Each one is small enough to hold in your head.

Renderer

js/ui.js · js/views/*.js · js/graphics.js
Each view returns an HTML string. render() writes it into #view-container; expensive lists (the wiki) are paged rather than drawn whole, and only live tabs rebuild on the tick.

Input & shell

js/ui.js · js/core.js
The shell owns tabs, the status bar, modals, settings and the mod panel. Keyboard shortcuts work throughout — Esc closes any overlay, 1–9 switch tabs — and overlays trap Tab focus.

Audio

js/music.js · js/core.js
Zero audio files. The soundtrack is improvised live from a scale via the Web Audio API, scaled down under the effects and gated by the Music switch and volume slider. Volume is clamped so no stored value can break a parameter.

Scene graph

js/maps.js · js/world.js
The world map and each region's local map are drawn from data: nodes come from G.WORLD_POS and edges from G.WORLD_LINKS. A mod can add a region; if it has no map position it is skipped rather than crashing the render.

See it running

What the engine renders

The shipped game is the engine's reference build — a world map, turn-based combat with several enemies, and an in-game diagnostics panel you can open from Settings.

Aethelgard title screen with save slots
title / save slots
The Aethelgard world map with clickable region nodes
world map (scene graph)
Turn-based combat against several enemies
combat (renderer)
The in-game diagnostics report panel
diagnostics (devtools)
Play the demo (this site) Live build — Chronicles of Fate

Get started

Three ways in

Play it, run it locally, or read how it is built before you change a line.

01 · Play

02 · Run & test

  • Clone the repo and serve the folder.
  • node tools/verify.js for the end-to-end check.
  • python3 tools/static-audit.py . for the static audit.

03 · Read the source

FAQ

Questions engineers ask first

Is there really no build step?
Correct. The engine is plain ES5-ish JavaScript, HTML and CSS with no dependencies and no bundler. Files are loaded as separate <script> tags and share one global, window.Game. There is deliberately no single-file bundle.
What do I need to run it?
Any modern browser. Opening index.html from an extracted folder works; serving the folder over http:// (any static host, or python3 -m http.server) additionally enables the service worker, so it installs to a home screen and runs fully offline.
How do I add content or change behaviour?
Two routes. Edit the pure-data files under js/data/ (enemies, classes, quests, items, wiki entries), or drop a .js file into mods/ — the engine discovers it at boot. Content mods apply instantly; story and overhaul mods only load on a fresh start, so a running save is never reshaped underneath you.
Can I ship my own art?
Yes. A graphics pack is a mod that registers images for the engine's sprite keys. Everything is drawn through one function, Game.sprite(key, fallback), so an overhaul is drop-in — and anything a pack does not cover keeps its emoji, so partial packs work.
How is it tested?
CI (.github/workflows/ci.yml) runs a syntax check on every JS file, JSON validation, and three Node test suites on every push and pull request, followed by the static audit. Locally there is also an end-to-end verifier, twelve gameplay simulations, and a content validator (also available in-game under Settings → Diagnostics).
What licence is it under?
The engine code is MIT. The bundled art is © reserved — see LICENSE-ASSETS.md. Core art is emoji and inline SVG, so the engine itself carries no external asset dependency.
How does this relate to the other Aethelgard games?
Aethelgard is a small world of two games. This repository is the engine and the reference RPG, Aethelgard: Chronicles of Fate. The visual novel, Aethelgard — The Books, is built on a separate, equally small engine called Saga.