BeamNG.Earth
BeamNG.drive용 1:1 비율 실시간 지구, 실제 고도·위성 이미지·부동 원점·보조 서버에서 LOD 스트리밍
mwg는 로컬 파일만으로 구동되는 타일 게임 프레임워크로 던전·퀘스트·스탯 등을 PixiJS로 2D 렌더링 없이 구현합니다
mwg is a tile-game framework built to run from a local file, with no server: dungeon generation, RPG events, quests, stats and turn order that need no renderer at all, drawn through PixiJS in 2D or Ba
게임TypeScript · HTMLGitHub @datamoc온라인운영자 미확인
써 보셨어요? 한 번만 눌러 주세요. 로그인은 필요 없어요.
A TypeScript game framework that runs from a local file : no server, no install. The game can also be run from a server or like an app.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
A framework for building tile games that run from a local file: no server, no install,
no runtime to download. The game logic depends on no renderer at all, so a game draws through
PixiJS in 2D (mwg/two-d) or Babylon.js in 3D (mwg/3d), and the dungeon generation, map
events, quests, stats and turn order underneath are the same either way.
Licensed under the MPL-2.0: games built with mwg carry no licence obligation
of their own; improvements to mwg's own files are shared back.README 전문 보기 ↗A framework for building tile games that run from a local file: no server, no install,
no runtime to download. The game logic depends on no renderer at all, so a game draws through
PixiJS in 2D (mwg/two-d) or Babylon.js in 3D (mwg/3d), and the dungeon generation, map
events, quests, stats and turn order underneath are the same either way.
Licensed under the MPL-2.0: games built with mwg carry no licence obligation
of their own; improvements to mwg's own files are shared back.
The canonical npm package is @datamoc/mwg; use that name for installation, imports,
tools and bundled data such as data/microgm-opt.sf2. @datamoc/mwg remains a compatible
re-export package for existing projects. The recipes here consistently use the canonical name.
Live examples and API docs: every example below, playable in the browser with no download, plus the generated API reference.
AI project prompt: a copy-ready prompt for planning or porting a demanding game with mwg, with links to the mwg reference and the mwg Pixel Dungeon study.
Status: pre-alpha. Every module in the shared floor below, plus optional 3D, mobile (Capacitor) and desktop (WebView2) packaging, is built and tested - see ROADMAP.md for open work, and CLOSED.md for the full, numbered shipped history. The packaging scaffolding is part of this repository rather than the published package, so a game that installs
@datamoc/mwgsupplies its own native shell: tools.md carries the recipe.0.4.0 was the reshaping release: the renderer moved out of
mwg/core,render/ui/stagebecamemwg/two-d, and a set of names that meant several things each were made to mean one. It was deliberately the last such release planned before 1.0. Still0.y.zuntil then, but the intent from here is additions rather than renames.Versioning:
0.y.zuntil 1.0, so the public API can still change between minor versions. Each future major version is meant to carry a name, one per letter of the alphabet: 1.0 takes a name beginning with A, 2.0 one beginning with B, and so on. None of the names are chosen yet, and nothing depends on them until they are.API stability: everything documented in REFERENCE.md and API_REPORT.md is meant to be stable from 1.0 on. Anything that is not carries an
@experimentaltag in its own doc comment, and is called out as experimental in the release notes that introduce it, so it can still change or disappear without a major version. A rename or a removal is deprecated first: the old name keeps working, its doc comment carries@deprecatednaming the replacement, and the release notes list it. 1.0 is the last release where either can move silently.
Tile-based games with a camera over a grid. Nine existing games mark out the corners of what that has to cover:
| reference | what it demands that the others do not |
|---|---|
| Shattered Pixel Dungeon | generated dungeons, field of view, energy-cost turn scheduling, permadeath, dense inventory UI |
| ADOM | many persistent maps joined by a world map, hunger and long-running effects, skills and attributes, quests inside a roguelike |
| RPG Maker projects | hand-authored maps, events on tiles, dialogue and cutscenes, a party, menu combat, save slots |
| Pokémon, in its traditional form | encounter tables by terrain, a roster of creatures with derived stats, an effectiveness matrix, speed-ordered battles, capture and storage |
| Ren'Py, and the conversation scenes every RPG borrows from it | a backdrop with characters standing in front of it, expressions, whoever speaks picked out from the rest, branching written as data |
| The Battle for Wesnoth | hexagonal tactical maps, movement cost and defense by terrain, zone of control, turn-based unit combat with recruiting and upkeep |
| Flare's Empyrean Campaign | isometric tile maps, loaded straight from Tiled's own isometric export |
| Dungeon Crawl Stone Soup (Tiles) | a branching dungeon of many interconnected levels, and autoexplore pathing to the nearest unseen tile |
| Twine | a story as named passages, not one linear list - a choice jumps to another passage by name, which can loop back or braid together, entirely on state and no visual stage at all |
Underneath they share almost everything: a grid, a camera following someone across it, sprites that need tinting, turn-taking, characters with stats and equipment, an inventory, and a great many windows. The differences are narrower than the genre labels suggest: one generates its maps and another authors them; one schedules monsters and another schedules events; one rolls encounters from the terrain and another places them by hand; one puts its conversations in a corner of the map and another gives them the whole screen; one moves in four directions on a square grid and another in six on a hex grid.
mwg provides that shared floor plus each specialism, on top of
PixiJS for rendering and rot.js
for the classic roguelike algorithms. Optional 3D scenes use Babylon.js. See
Capability spec.
These are references for what a finished game of each kind has to do, and nothing more. No code, asset, data table or file format is taken from any of them: see Licence and provenance.
Worth being precise, because RPG Maker is two things and mwg is only aiming at one of
them for now.
It is a runtime: a tile engine, an event interpreter, a battle system, a message box. That is what the capability spec below commits to.
It is also an editor, and that is what most people actually mean by RPG Maker: drawing
maps and wiring events without writing code. mwg does not have one. What it does instead
is keep maps, events and dialogue as plain data rather than code, so an editor can be
built against that data later without reworking the runtime, and so that in the meantime
the data can be written by hand, generated by a script, or exported from
Tiled, which is free and already good at this.
That data is loaded exactly like any other asset, which means the file:// story covers it
for free: a finished game is still a folder you double-click.
With a toolchain, if you already use a bundler:
npm install @datamoc/mwg pixi.js
import { Game, Scene } from '@datamoc/mwg/core';
import { Sprite } from '@datamoc/mwg/two-d/render';
pixi.js is the 2D renderer, and it is an optional peer dependency rather than an installed
one: a game that only uses mwg/core, mwg/roguelike, mwg/actors, mwg/i18n or the other
logic modules never downloads it. Name it on the install line when a game draws, exactly as
@babylonjs/core is named for 3D.
No npm registry access (an offline machine, a pinned artifact for reproducible builds)?
npm pack @datamoc/mwg downloads the exact published tarball; install it by path
instead of by name:
npm install ./datamoc-mwg-<version>.tgz pixi.js
Without either. Drop one file next to your page and open it: this path exists because
the framework's own target is a game you can run by double-clicking it. Download
mwg.global.js from a release (no npm,
no node_modules, nothing to install):
<script src="mwg.global.js"></script>
<script>
const game = new mwg.Game(document.getElementById('game'));
</script>
Node and npm are developer tools here, in the same sense that a compiler is. Nothing from them is present when the game runs: the output is HTML, JavaScript and assets.
file://Opening a page from disk takes three capabilities away, and the framework is shaped around them rather than pretending otherwise:
| what is blocked | consequence | how mwg handles it |
|---|---|---|
| ES modules (CORS) | <script type="module"> will not run | the standalone build is a classic IIFE |
fetch() / XHR | no asset can be loaded on demand, no directory can be listed | tools/compile-resources turns assets into scripts loaded before the game |
| cross-origin images | an <img> from a sibling file taints a WebGL texture in Chrome | assets travel as data: URIs inside those scripts |
The practical effect: resources are compiled at build time, so at runtime every lookup is synchronous. Game code never awaits an asset. The developer-side scripts behind this and the other workflows (translation editing, benchmarks, packaging) are listed in tools.md.
A game built with its own bundler (Vite, say, rather than this repo's own vite.lib.config.ts
IIFE) hits the same ES-module block on its own entry tag - a bundler's default output is
<script type="module" crossorigin src="...">, which file:// refuses the same way. Rather
than every such game reinventing that one rewrite (item 306),
tools/classic-html.mjs's toClassicScript(html) does it: given the built page's HTML, it
returns the same HTML with that tag rewritten to <script defer src="...">, or null when it
finds no such tag - the guard against running it against an unbuilt dev template by mistake.
The bundle itself still has to already be a classic script (an IIFE or UMD build, not further
ES module output); this only rewrites the tag pointing at it.
With Vite, none of that is by hand: @datamoc/mwg/tools/vite's mwgPage() plugin sets
the classic-script configuration and, when vite build finishes, compiles assets/, rewrites
the tag and precompresses the output (item 381). The same finishing step is emitPage from
@datamoc/mwg/tools/emit-page, or the mwg-emit command, for another bundler.
mwgPage({ singleFile: true }) (or { singleFile: { compress: true } }) additionally writes one
self-contained standalone.html. Plain .ts tests under node --test need a Node with type
stripping (22.18 or later, or any build with --experimental-strip-types); otherwise run them
through tsx.
The finished page also carries a Content-Security-Policy, with nothing to configure (item 374):
scripts only from its own folder and its own hashed inline scripts, no network request except to
origins the game declares (mwgPage({ csp: { connect: ['https://news.example'] } })), and no
image, font or media URL but data:/blob:. A compromised dependency can run, but cannot send
a save or anything else off the machine. 'unsafe-eval' stays allowed because Pixi 8 refuses
to start without it unless a game imports pixi.js/unsafe-eval (then pass csp: { eval: false }).
One consequence of file:// a game has to design around: Chromium gives every local page one
shared localStorage. Any other HTML file opened on the same machine can read a game's saves
and settings, and rewrite them. mwg treats that storage as outside input (SaveSystem.load
rejects a tampered slot rather than loading it, item 372), but no checksum can make it
private or trustworthy, since the page that rewrites a save can recompute any checksum too. So
never put a secret in a save or in settings (an account token, a password, a licence key), and
treat a save as the player's own editable file, which it is.
await load(['vehicles/car.svg'], { resolution: 2 }) rasterizes SVG assets at twice
their logical resolution, keeping them sharp when a camera zooms in. Import load from
@datamoc/mwg/assets. loadSvgParts(path, { resolution: 2 }) loads top-level SVG
groups as textures in their authored stacking order.
Input events live under the Input namespace: import { Input } from
@datamoc/mwg/core and subscribe with Input.onAction.add(...). onAction is not a
direct core export. Input.attachPointer supplies pointer and drag events for mouse and touch.
Use AudioBus for one playback API across ordinary audio and MIDI assets. Its default
loader reads embedded assets, detects MIDI by its header and renders it on a worker.
playBgm/playBgs loop, while playMe/playSe play once; all take the same
{ name, volume, pitch, pan } fields (volume 0-100, pitch 100 unchanged, pan -100 to 100).
Pass load: createAudioLoader(context, { midi: { soundFontBytes, missingProgram: 'synth' } })
to use a SoundFont. Sound remains useful for pooled HTML audio effects and MusicPlayer
for a seekable jukebox.
Sound pools short effects. sound.play(0.7, 1.1) sets gain and pitch for that play;
synthesizeSound({ waveform: 'sawtooth', frequency: 110, duration: 0.2 }) makes a pooled
effect without an asset file. Sound.plays counts started playback attempts for browser tests.
For MIDI music, copy the supplied data/microgm-opt.sf2 from the installed package into
your assets folder alongside the .mid files. mwgPage({ assets: 'assets' }) automatically
reduces the embedded SoundFont to the programs those MIDI files use. Load both through
loadBinary, then render once on a worker and play the resulting buffer:
For a smaller starting bank, data/microgm-essential.sf2 supplies piano, guitar, violin,
trumpet, flute and standard drums in 314 KB. Use it instead of the full bank below, with
missingProgram: 'synth' for the remaining instruments. Both banks retain their WTFPL licence.
import { loadBinary, getBinary } from '@datamoc/mwg/assets';
import { MusicPlayer, renderMidiToBufferAsync } from '@datamoc/mwg/audio';
const player = new MusicPlayer(new AudioContext(), { volume: 0.7 });
await loadBinary(['music/theme.mid', 'soundfonts/microgm-opt.sf2']);
player.load(await renderMidiToBufferAsync(getBinary('music/theme.mid'), {
soundFontBytes: getBinary('soundfonts/microgm-opt.sf2'),
missingProgram: 'synth',
}));
player.play({ repeat: true });
Start playback from a player gesture. Call player.update() each frame to dispatch
onEnded for a nonrepeating track; that signal lets the game choose its next track.
pause(), resume(), position() and seek(seconds) share the same playhead.
toAudioBuffer(context, rendered) also converts a render for direct Channel use.
missingProgram: 'synth' voices instruments absent from the font with the built-in synth.
To catch page growth, set mwgPage({ maxPageBytes: 1_500_000 }). Its build report lists
embedded paths not found literally in the generated game script as potentially unused;
paths assembled at runtime need manual review. assetFiles selects exact asset paths to embed.
A retro-instrument package (an MT-32, OPL or chip synth of its own) plugs into mwg through
contracts mwg owns, so the core never depends on the package. It registers a MidiSynth
under a name with registerMidiSynth('retro:mt32', factory); content then asks for that
name and resolveMidiSynth walks the request, the SoundFont and the built-in synth in
order, firing onMidiSynthFallback whenever a different engine ends up voicing the file.
Endless or machine-code formats (NSF, SID, SPC) play through the streaming Playable
contract (StreamPlayable, StreamChannel) under named decoder guards
(decoderLimits, DecodeBudgetError): input ceilings, an instruction budget per second
of sound, and bounded inflation, so untrusted files fail by name instead of hanging.
Rendered tracks are budgeted (TrackCache, createBudgetedLoader): a five-minute stereo
render is about 100 MB, so the bus caps decoded megabytes rather than keeping every track
forever. Synthesised effects are data, not files: SfxBank builds them from definitions
with a fixed seed, so audio never disturbs a lockstep simulation (its triggers are
deterministic; the audio itself is not in the lockstep state, and A/V sync follows the
game frame counter, not the wall clock). The mwg-audio tool inspects, converts, renders,
budgets and compares these formats from the command line, and spectralDistance gives a
first automated check between two renders. What no automation settles is whether a render
sounds right: the A/B listening protocol in docs/listening-protocol.md is the place to
record those verdicts.
The definition of done for 1.0, drawn from what each of the nine references actually demands. The shared floor is what all of them need; the sections after it are the parts only some do.
| area | capability | provided by |
|---|---|---|
| core | frame loop (variable dt, clamped; FixedStepper for fixed simulation steps), scenes, timing | mwg/core |
| signals and listeners | mwg/core | |
| deterministic seeded RNG, reproducible across machines | mwg/core | |
| named, versioned save slots with a preview | mwg/core | |
declarative reactions to any state crossing a condition, in place of an if cascade | mwg/core | |
| render | sprite batching at thousands of tiles | PixiJS |
| per-sprite colour transform: multiply and add | mwg/two-d/render | |
| animated sprites driven by named frame sets | mwg/two-d/render | |
| dense tile maps, multi-layer, with per-cell visibility | mwg/two-d/render | |
| hexagonal tile maps, alongside square, as a second grid shape | mwg/two-d/render | |
| camera with follow, pan, shake and zoom | mwg/two-d/render | |
| particle effects | mwg/two-d/render | |
| screen transitions: fade, flash, tint | mwg/two-d/render | |
| ui | windows, lists, tooltips, markdown labels | mwg/two-d/ui |
| text and fonts, including non-latin fallback | PixiJS | |
| modal single-line text entry, with validation and an on-screen keyboard for touch | mwg/two-d/ui | |
| pointer, keyboard and gamepad input with rebinding | mwg/core | |
| text | message tables per language, compiled at build time | mwg/i18n |
| plurals, gendered forms, interpolation, and Python f-string-style format specs |
Shared by everything past a single screen, and the thing ADOM needs most.
| capability | provided by |
|---|---|
| many maps in one world, joined by transitions | mwg/world |
| maps that persist when left and returned to | mwg/world |
| an overworld map with locations on it | mwg/world |
| a turn clock driving hunger, poison and other long effects | mwg/world |
| capability | provided by |
|---|---|
| field of view and lighting | rot.js + mwg/roguelike |
| pathfinding over a passability map | rot.js + mwg/roguelike |
| turn scheduling by energy cost | mwg/roguelike |
| level generation from composable room builders | mwg/roguelike |
| autoexplore: pathing to the nearest unexplored cell | mwg/roguelike |
The Wesnoth-shaped half: a grid whose shape is a parameter, not a parallel implementation.
| capability | provided by |
|---|---|
| hexagonal tile maps, addressed by axial or offset coordinates | mwg/two-d/render |
| movement cost and defense bonus by terrain, hex or square alike | mwg/roguelike |
| field of view and pathfinding over a hex grid, not just square | mwg/roguelike |
| isometric (and staggered) projection, alongside orthogonal and hex | mwg/two-d/render |
| zone of control: a hex a unit can only move into, not through, unless already engaged | mwg/board |
| an army economy: recruiting against a currency, recalling from a pool, per-turn upkeep | mwg/board |
A game picks a grid shape once; the camera, field of view, pathfinding and turn scheduler
work the same way over either, rather than a hex game needing its own copy of each. Between
them, orthogonal, isometric and hex cover every map orientation
Tiled itself supports: a Tiled map loads and renders through
the same TileMap, whichever orientation it was drawn in, rather than mwg reading only the
one shape it happens to render natively. Flare's Empyrean Campaign is the isometric-shaped
reference, the same way Wesnoth is the hex-shaped one.
| capability | provided by |
|---|---|
| maps loaded as data, in any of Tiled's map orientations | mwg/rpg |
| events placed on tiles, with pages selected by conditions | mwg/rpg |
| event triggers: action, touch, autorun, parallel | mwg/rpg |
| a command interpreter for cutscenes: move routes, waits, branches, calls | mwg/rpg |
| global switches and variables, saved with the game | mwg/rpg |
| message box with portraits, choices and typewriter reveal | mwg/rpg + mwg/two-d/ui |
| grid movement tweened between tiles, with a walk cycle | mwg/rpg |
| a dialogue stage: backdrop, characters, expressions, speaker focus | mwg/two-d/stage |
| scenes written as a list of commands, awaited as one call | mwg/two-d/stage |
| named passages a choice can jump between, for a story that is a graph, not a line | mwg/two-d/stage |
| named save slots with a preview | mwg/core |
All four references have these, and they are what a game changes most, so they are data rather than classes.
| capability | provided by |
|---|---|
| a stat block: base attributes, derived values, and the rules that derive them | mwg/actors |
| levels, experience and growth curves, all replaceable | mwg/actors |
| skills and proficiencies, checked against a difficulty | mwg/actors |
| equipment slots defined per game, not fixed by the framework | mwg/actors |
| modifiers from equipment, buffs and states, resolved in a stated order | mwg/actors |
| an inventory with stacking, weight and containers | mwg/actors |
| item identification, and cursed or blessed states | mwg/actors |
| a layered character sprite, so worn equipment is visible | mwg/two-d/render |
That last row is why tools/make-example-assets.mjs draws its character in parts. A figure
assembled from skin, eyes, hair and garments is one palette away from being a second
character, and one layer away from showing the armour it just put on. A single flat
sprite per character can do neither.
The Pokémon-shaped half, and the part mwg deliberately keeps generic.
| capability | provided by |
|---|---|
| species defined as data, with per-individual stats derived from them | mwg/battle |
| a party with a roster and off-party storage | mwg/battle |
| an effectiveness matrix between arbitrary types | mwg/battle |
| turn order by speed, with move priority overriding it | mwg/battle |
| moves as data: cost, target shape, effects | mwg/battle |
| encounter tables per map region, weighted, with a rate per step | mwg/world |
| capture, experience, levelling and evolution as replaceable rules | mwg/battle |
mwg supplies the shape: a stat block, a type matrix, a turn queue. It supplies no
formulas, no type chart and no species: those are a game's own design, and copying any
particular game's would be both wrong and useless to anyone else.
All of these specialisms share the tile map, the camera, the windows, the turn clock and the save system, which is why they sit in one framework rather than four.
The single most load-bearing rendering feature in either genre is the colour transform
texel × M + A. Darkness outside the field of view, tinting a poisoned enemy, flashing
white on a hit, a room lit by a fireplace, a screen-wide tint for a night scene: all of it
is that one operation.
Pixi's built-in tint is multiply-only, which can only ever darken. The additive term is
what mwg/two-d/render adds, in the batch shader rather than as a per-object filter, so it costs
nothing per sprite. See examples/colour-transform.
An RPG's content is enormous and repetitive, and almost none of it deserves to be written by hand in TypeScript. Keeping maps, events, dialogue and item tables as data means they can be authored in a spreadsheet, generated by a script, exported from Tiled, or eventually edited in a proper editor, without any of those options requiring changes to the runtime.
It also keeps the content out of the bundle: tools/compile-resources treats a map file
exactly like a sprite sheet, so adding a hundred maps does not touch the build at all.
Content is data (above), but a game's behavior belongs in classes. A monster kept as
loose scene fields works for one tutorial room and collapses for a dungeon: give it a
class holding its state and the methods that change it, built on the framework's own
bricks (core.Scene scenes, actors.StatBlock stats, battle.Creature opponents,
roguelike.Level maps, rpg.GridMover movement, core.EntityRegistry identity).
Subclass the brick that already owns the concern instead of reimplementing it beside an
interface; no new base class is needed for ordinary actors.
The boundary is where serialization starts. simulation commands survive
structuredClone, core.SaveSystem stores JSON-serialisable state, and MWL rows are
records by design, so classes stop where those begin: flatten at the boundary the way
rpg.GameState does, and rebuild objects on load rather than storing them.
Supporting more than one language is mostly bookkeeping. Supporting more than one writing direction is not, and the three cases are genuinely different work:
mwg/two-d/ui
lays widgets out against a direction rather than against "left" so this can be flipped,
and a game only has to mark which of its own icons must not be mirrored.Message tables are compiled at build time, the same way every other resource is, so adding a language costs nothing at runtime and a missing string falls back to the base language rather than showing a raw key to the player.
@babylonjs/core/@babylonjs/loaders are peer dependencies, not installed automatically -
a game that never imports mwg/3d never downloads Babylon.js. Add them yourself
(npm install @babylonjs/core @babylonjs/loaders) once a game imports 3D support:
import { Engine3D, createTileGrid3D } from '@datamoc/mwg/3d';
import { loadModel3D } from '@datamoc/mwg/3d/models';
Engine3D owns a WebGL scene, orbit camera, light, resize handling, and render loop.
createTileGrid3D batches square or flat-top hex tiles and elevation columns with thin
instances; createHeightmapTerrain3D builds continuous terrain from decoded RGBA heightmap
pixels. Character3D moves imported meshes or camera-facing sprites continuously, and can
play imported animation clips. parseVox and createVoxModel3D load one MagicaVoxel model;
loadModel3D handles glTF/GLB.
Run npm run example:3d for the live example or npm run benchmark:3d for its FPS gate.
@datamoc/mwg/simulation has no rendering or browser imports. It provides two
small runners while games keep their state, commands, rules, and event types:
advanceToInput(rules, budget) runs automatic scheduled actions until an actor needs
input, the game finishes, the queue is empty, or the action budget is reached. Rules
supply scheduler, finished, needsInput, and act. act returns the cost to
charge to the current queue entry after the action, or null when it handled its own
scheduling. A limit result does not grant input; the caller chooses how to resume.runScenario({ state, commands, random, step, status? }) applies a finite command
sequence, preserving event order and stopping when a rule returns finished. Each
rule returns { state, events, status: 'ready' | 'finished' }. Results also report
processedCommands; a command need not consume a game turn.import { runScenario } from '@datamoc/mwg/simulation';
const result = runScenario({
state: { position: 0 },
commands: [1, 2, -1],
random: null, // This example has no randomness; supply a generator when needed.
step: (state, distance, _random) => ({
state: { position: state.position + distance },
events: [{ moved: distance }],
status: 'ready',
}),
});
// result.state.position === 2; result.processedCommands === 3
The root/standalone build also exposes these as Simulation. The dungeon example uses
the scheduled runner and shares its own damage rule between the scene and headless
scenario tests. Neither runner implements a game's combat, inventory, or movement rules.
For reproducibility, supply the same initial state, commands, and random state, and keep
rules independent of wall-clock time and ambient randomness. Existing Generator
instances expose getState()/setState(); store that state alongside a game's snapshot.
The runners do not clone state, serialize arbitrary graphs, or roll back a throwing rule.
Frame-based core.Recorder/Player remains available for UI replay; command scenarios do
not need a frame loop. Callers must bound automatic work inside each rule, for example
with advanceToInput.
ai.NeuralPolicy runs small dense models without a runtime dependency. Policies
share weights across agents, mask illegal actions and integrate with JavaScriptAI.
simulation.TrainingEnvironment exposes seeded reset/step episodes over the same
rules a game uses during play. Training runs at simulation speed in Node, with no
canvas, DOM, audio or frame loop. runRolloutsAsync batches whole episodes across
workers; model files bundle into the playable file:// output.
Try the generated courier task, whose optimizer belongs to the example:
npm run train:neural # train without any UI, export model and checkpoint
npm run train:neural -- --resume --iterations 40
npm run benchmark:neural # serial/parallel throughput and inference overhead
npm run example:neural:build # open examples/neural/dist/index.html directly
npm run neural:browser:check # workers, input latency and reset cancellation
Training checkpoints include environment state and randomness, model/schema versions and trainer state. Use small predictions directly when they fit the frame budget; worker startup and transfer costs pay off on larger simulation batches.
examples/pacman is a playable Pac-Man-style game with an original maze and drawn
shapes. Switch the player between keyboard control, random turns, a path-and-risk
heuristic and a trained neural policy. Ghosts independently select random turns,
direct chase rules (JavaScriptAI), path heuristics (HeuristicAI) or NeuralPolicy.
Selections take effect during the current game, preserving score and lives.
Arrows or WASD steer; P and G cycle the player and ghost controllers; Space pauses; R starts a new game. Large dots make ghosts edible. The neural model uses UI-free imitation training from the heuristic, sharing the playable simulation rules.
npm run example:pacman # development server
npm run example:pacman:build # open examples/pacman/dist/index.html directly
npm run train:pacman # retrain and export weights without rendering
npm run pacman:browser:check # selectors, WebGL, frame time and responsive layouts
See ROADMAP.md for open work and CLOSED.md for the full, numbered build order and implementation notes. The optional 3D group is now implemented without changing the existing 2D entry point.
MWL is the framework's declarative, WML-inspired authoring layer. Content files
are JSON5: a top-level array of { tag, ...attributes, children } nodes, with
real numbers and booleans for scalar values and _("...") marking translatable
strings. It describes game data and commands while mwg remains responsible for
execution, rendering, input, audio, and saves. MWL is a new language, not a WML
compatibility layer.
Optional scene and UI composition is available through @datamoc/mwg/mwl/presentation
(build-time compiler) and @datamoc/mwg/two-d/presentation (Pixi runtime). Compile
reusable widgets, responsive layouts, keyed lists, semantic palettes and context overrides
against a TypeScript binding manifest. The game supplies actions, physics and procedural
adapters. See the presentation contract and
examples/mwl-presentation; npm run visual:smoke:presentation verifies the compiled
file:// page, keyboard/pointer input, modal windows, cleanup and update cost.
The Wesnoth port can nevertheless use its native .cfg files as build inputs:
the port-owned WML front end parses them, the adapter converts the supported
nodes to MWL, and the normal MWL compiler emits the runtime module.
The framework exposes the parser, validator, compiler, runtime, i18n extraction,
and the hook contract through @datamoc/mwg/mwl. Compile source at build
time with:
npm run mwl -- validate game.mwl
npm run mwl -- diagnostics game.mwl
npm run mwl -- ci game.mwl
npm run mwl -- format game.mwl -o generated/game.mwl
npm run mwl -- migrate game.mwl --to 1.0 -o generated/game-1.0.mwl
npm run mwl -- build game.mwl -o generated/
npm run mwl -- compile game.mwl -o generated/game-data.ts
npm run mwl -- extract-i18n game.mwl -o generated/i18n.json
npm run mwl -- hooks game.mwl --manifest hooks.json -o generated/hooks.mjs
The input may also be a content directory. mwl build content/ -o generated/
reads every .mwl file recursively in stable path order and emits one merged,
validated catalog. Diagnostics retain each source file and line, so a game can
organize campaigns, assets, units, items, scenarios, dialogue, and AI data as separate files.
mwl build is the normal game command: it produces game-data.ts, i18n.json,
and assets.json in one build-time step. The game imports the generated module;
it never calls Mwl.compile() at runtime. MWG also provides generic helpers for
turning item effects into actors modifiers and for versioned, migratable MWL
saves. Asset attributes such as image, image_icon, profile, icon, file,
sound, and *_sound are collected into the manifest, including comma-separated
sound lists, Wesnoth sound ranges such as human-hit-[1~5].ogg, and image
modifiers. Nested transform arguments are not mistaken for separate assets.
Item slots, formulas, hooks, and business rules remain defined by the game.
[item] itself carries image/icon (item 307), so an inventory item can author its own
sprite or icon the same way [monster]/[unit_type]/[object]/[story] already could; the
manifest scanner recognised these names on any node before the schema let [item] use them,
so this only closes that one gate rather than adding new asset-collection behaviour.
A game needing more than mwl build's three standard files - its own extra generated module,
or a cross-table check this CLI has no flag for - should not look for a flag to add: every
function that command calls (compileSources, emitArtifacts, validateCatalog,
contentReport) is public from @datamoc/mwg/mwl, so that game's own small Node build
script, written against the same library API tools/mwl.mjs itself uses, is the extension
point (item 305).
Campaign metadata can be authored with a [campaign] tag inside [game]. It requires an
id and accepts name, title, description, and start_scene; scenario and extension
children remain game-owned and are preserved by the compiler. The compiled metadata is
available as contentCatalog(game).campaigns.
For a large self-contained HTML page, npm run extract:html -- page.html -o extracted/
writes a rewritten index.html, an assets/ directory, and a deterministic
manifest.json. Inline scripts, styles, and supported data: URLs are extracted in
source order. The original page is never overwritten, and unsupported data forms are
reported as warnings. The output directory is safe to regenerate: its extracted
assets/ directory is replaced, while unrelated files are kept.
CSS url(data:...) values and HTML srcset candidates are handled too, including
their density descriptors.
Translation messages are the values marked _("...") in the source; the
extracted catalog matches @datamoc/mwg/i18n. Hooks are referenced as
type:name, declared in a manifest, and bundled with esbuild, which a game
project installs itself (npm install --save-dev esbuild). The framework does
not depend on esbuild, so a game without hooks pays nothing for the feature.
MwlRuntime runs compiled content: it loads the map, sides, units, and leaders
(leaders spawn on their keep with stats from [unit_type]), runs on= events
with filters and conditions, tracks movement points, advances the [schedule]
on [end_turn], evaluates [objectives], hosts command and predicate hooks,
and saves and restores the world. Pathfinding and combat formulas stay in a
game's engine adapter, because they need that game's real data. A command hook
receives the [hook] attributes as its context; a condition=hook objective
receives its other attributes, so content can parameterize an engine predicate
(value is the generic integer parameter).
Events trigger on start, turn, and moveto. A moveto event fires when a
unit arrives on a hex, filtered by x, y, unit and side, once by default
(and a spent one-shot stays spent across save/restore). A game whose own engine
resolves movement writes the new position into the world and calls
runtime.fireMoveto(unitId), so the trigger works either way. A [message]
becomes an MwlMessage with its text, and its speaker, portrait and side
when the content gives them, which is enough for a real dialogue box instead of a
log line.
Dialogue choices are exposed through MwlMessage.choices and answered with
runtime.answerDialogue(dialogueId, choiceIndex). Pending choices are part of the
saved world. Commands after a dialogue in the same event run immediately, before
the player answers, so deferred follow-up commands belong in the event named by
the selected choice. Events can also be invoked directly with runtime.fireEvent(id);
moveto coordinates accept comma lists and inclusive ranges such as 1,3-5.
The generated data is plain TypeScript and works with the same local-file build pipeline as the rest of the framework. Wesnoth-specific MWL source belongs in the Wesnoth port, not in this framework package.
Games built with mwg are welcome to carry the badge on their login screen:
Copy webpage/assets/powered-by-mwg.svg into your own project (a shipped game
cannot fetch it from here; local-file pages only load their own assets) and link
it to the framework site. In plain HTML that is:
<a href="https://datamoc.github.io/mwg/">
<img src="./powered-by-mwg.svg" alt="powered by mwg" width="196" height="38">
</a>
A canvas-drawn login screen loads the same file through the asset pipeline and
opens the URL on click with window.open('https://datamoc.github.io/mwg/', '_blank').
mwg is MPL-2.0. Its dependencies are permissive and compatible: PixiJS and Babylon.js are
MIT, and rot.js is BSD-3-Clause.
tools/vendored/rgssad-wasm is a build of the rgssad-wasm crate from
https://github.com/nathaniel-daniel/rgssad-rs, used under its MIT licence
option (© 2022 Nathaniel Daniel); see tools/vendored/rgssad-wasm/README.md.
It is a developer-side archive tool only, not part of the framework or any game.
Shattered Pixel Dungeon (GPL-3, © Oleg Dolya and Evan Debenham), The Battle for Wesnoth
(GPL-2.0-or-later, © the Battle for Wesnoth contributors), Flare's Empyrean Campaign
(GPL-3, © the Flare project), and Dungeon Crawl Stone Soup (GPL-2.0, © the Dungeon Crawl
Stone Soup development team) are referenced only as design studies: a list of what a
finished roguelike, a finished hex-based tactical game, a finished isometric one, and a
finished branching dungeon each have to do. None of their code, assets, maps, unit
definitions or text is included here, and mwg is not a derivative work of any of them.
ADOM, RPG Maker, Pokémon, Ren'Py and Twine are likewise references for a shape of game, not
sources of anything. mwg includes none of their runtimes, assets, text or data tables: no
type chart, no species list, no stat formula, no map, no line of dialogue. Those are each
game's own design work. A loader or writer for one of their file formats is a different
matter and is not off limits on its own: a format is a container, understood the same way
any published format is, by reading real files in it to learn its shape; what stays out is
the media that container holds, someone else's actual assets, text or data tables. Where a
format already exists and needs no reverse-engineering, mwg uses Tiled's, which is open
and documented.
Game mechanics are not themselves protected, which is why a framework may offer "a turn queue ordered by speed" or "an effectiveness matrix". Particular numbers, names and artwork are another matter, and none appear here.
The assets under examples/assets are generated by tools/make-example-assets.mjs and
carry the project's own licence. Nothing else in this repository was downloaded from
anywhere, with the sole exception of tools/vendored/rgssad-wasm above.
mwg/i18n |
| Fluent-style FTL catalogs with locale-aware fallback and variants | mwg/i18n |
| left-to-right, right-to-left and vertical writing | mwg/i18n |
| interface mirrored for right-to-left languages | mwg/two-d/ui |
| a missing translation falls back rather than showing a key | mwg/i18n |
per-message sound cues as an audio channel, plus inline {sound:path} markers for timed dialogue cues | mwg/i18n + mwg/two-d/ui |
| audio | sound effects with pooling, music with crossfade | mwg/audio |