A one-line-per-export index of @datamoc/mwg's public API - what exists and where,
not how to use it. The published Documentation page is this file rendered as HTML; the
full generated API (every parameter, every doc comment) lives beside it at
webpage/documentation/api/, with an examples index beside it at
webpage/documentation/examples.html listing every @example per module (all from
npm run webpage:docs). For why the framework
is shaped this way and what it's for, see README's capability spec. For architecture
and build commands, see DEVELOPMENT.md. For the order things shipped in and the reasoning
behind it, see CLOSED.md; for what's still open, see ROADMAP.md.
Every module is its own barrel: import { X } from '@datamoc/mwg/core' (or the
matching subpath for any module below), or import { X } from '@datamoc/mwg' for the
root re-export.
Two rendering worlds, and a core that belongs to neither. mwg/two-d is everything drawn
through PixiJS (Game, Scene2D, and the render, ui and stage modules beneath it);
mwg/3d is everything drawn through Babylon. mwg/core holds what both need and neither
renders: the scene lifecycle, SceneStack, input, saves, RNG, signals, achievements.
Which entry points reach a renderer, checked by a test that walks the real import graph
(tests/renderer-isolation.test.ts) rather than by good intentions:
| renderer-free | Pixi | Babylon |
|---|---|---|
core, i18n, actors, world, battle, simulation, roguelike, board, audio, music, rpg, ai, mwl, assets/paths |
two-d (and two-d/render, two-d/ui, two-d/stage), assets |
three-d (the 3d subpath) |
roguelike adds only rot-js, and pixi.js, @babylonjs/core/@babylonjs/loaders and
@capacitor/core are optional peer dependencies rather than installed ones, so a project names
only the renderer it actually draws with. A Babylon game importing core + 3d pulls in no Pixi
at all, and a 2D game importing core + two-d pulls in no Babylon. rpg is renderer-free too:
its event interpreter takes an injected DialoguePresenter rather than building a widget, so
a 3D game can run map events, dialogue and quests without a 2D renderer anywhere in reach.
MWL presentation
The optional compiler at @datamoc/mwg/mwl/presentation is renderer-free. The
runtime at @datamoc/mwg/two-d/presentation imports Pixi and is kept off the root
barrel. The browser consumes compiled data, with no MWL parser or network requests.
compilePresentation(files, manifest)validates and compiles top-leveltheme_definition,presentation_context,component_variant,ui_componentandscene_definitionnodes. It checks named actions/adapters, binding types, assets, reusable-component cycles, duplicate IDs, overrides and token references at build time, with source-file/line diagnostics. The manifest's asset names must be generated files that the game's asset build actually includes.emitPresentationModule(data)emits deterministic TypeScript data,PresentationId(including keyed instance paths) andPresentationSceneIdliteral types. A game's build script writes this module plusdata.messagesanddata.assetsinto its existing MWL artifact pipeline. Seeexamples/mwl-presentation/generate.mjs.MwlPresentation(data, sceneId, options)mounts existingButton,Label,Bar,Window,WindowStackandCameracomponents.get(id),focusId,paletteandactiveContextexpose instances and presentation state.update(dt),resize(width, height),setContext(id)anddestroy()drive its lifecycle;handleAction(action)offers modal windows input first. Register destruction with the owning scene'sonDestroy; it detaches input, theme, controller and adapter subscriptions.
Declare the TypeScript manifest with values: { name: 'string' | 'number' | 'boolean' },
or { type: 'items', fields: { id: 'string', text: 'string', selected: 'boolean' } } for
collections, plus actions, optional adapters and assets name lists. The runtime's
options.state() supplies those values; options.actions implements the named callbacks.
Repeated-item actions receive the current item, even after reorder. Optional subscribe
returns a disposer, and translate resolves authored/gettext text. Asset textures must
be loaded before mounting. PresentationAdapter exposes a view plus optional update,
resize, restyle and destroy: the runtime destroys the view, while the adapter
detaches its own subscriptions. Cleanup continues even if an adapter throws.
Scenes contain scene_camera (zoom, position or fit dimensions), scene_layer (world or
screen space, optional generated image or named procedural adapter) and UI trees.
ui_mount expands a reusable component at build time. UI trees use ui_container,
ui_row (wrapping by default), ui_column, ui_grid (equal grow columns), ui_label,
ui_button, ui_bar, ui_window, ui_repeat and ui_adapter. Every node has an id;
runtime paths start with the scene ID and join descendant IDs with /. Repeats insert
the URI-encoded stable key before their single template child's ID, preserving instances
and focus on updates. A reusable component using item fields may declare items_binding
so even an unused template can be validated against its collection.
Properties include pixel dimensions, anchors, offsets (x, y), spacing, alpha and
semantic color tokens. Bindings use text_binding, title_binding, value_binding,
max_binding, color_binding, visible_binding, selected_binding and
disabled_binding; item fields use item.field. Buttons name an action, windows may
name a close_action, and repeats declare items_binding plus a string/number key.
Window close actions should update the controller's visibility binding. Closed windows
stay closed until their visibility transitions false then true; reopening creates a
fresh window, because existing Window.close() destroys its contents.
Themes require panel, text, accent, success, warning, danger and selection
24-bit color tokens; games may add scenery tokens and choose font/padding metrics.
Text/panel contrast must reach 4.5, and danger/selection against panel must reach 3.
Contexts choose a theme and may override tokens or authored-node properties using
ui_override.target, such as component:controls/toolbar or scene:bridge/decor.
Image overrides on scene layers select generated scenery. Precedence is base properties,
context overrides, component variant, then ui_state (idle, hover, pressed,
disabled, selected, focused). A game's biome schema selects a context; MWG defines
no biome rules. Context switches preserve mounted adapters and controller state.
One active presentation owns the existing global UI theme. Destruction restores its prior
theme if nobody replaced it meanwhile. Existing widgets also expose local palette updates:
Button.setColors (ButtonColors), Window.setColors (WindowColors) and
Bar.setBackground, for variants that must follow token changes without remounting.
Conventions
Three rules the whole API follows, so a name means one thing everywhere:
advance(turns)moves a clock forward in whole turns or rounds;update(dt)moves one forward in real seconds.TurnClock,Charges,Field,BarrierandAbilityCycletake turns; everything inrender,ui,audioandworld.EnvironmentClocktakesdt. Anything that is not time passing gets its own verb rather than borrowing one of these (QuestLog.advanceStage,BossPhases.check).toJSON()/static fromJSON(defs, data)is how a class saves and reloads. Definitions a game owns (item tables, quest definitions, growth curves, terrain kinds) are supplied fresh asdefsrather than written into the save, so rebalancing reaches old saves. Namespace-level state usesexportX/importXfree functions instead (Inputbindings);Random.GeneratorkeepsgetState/setState, which resumes a live stream in place rather than producing a payload.- "Nothing to pick" is always
null, never-1orundefined:Random.weighted,Random.element,Random.weightedKey,rollEncounter,rollLoot,rollAffix,pickBuilder.
core
The scene lifecycle, signals, RNG, input, and every save/telemetry/replay primitive - the one
module every game imports, whichever renderer it uses, and the only one that depends on no
other mwg module and no renderer at all.
Scene- one screen of the game, as lifecycle only:create,update(dt),resize, theonSuspend/onResumepair a pushed scene needs, and adestroythat fires once. It owns no display node;two-d.Scene2Dadds the Pixi container, and a Babylon game supplies its own.SceneStack-push/pop/replace: suspends the current scene, layers another over or instead of it, and resumes with a result once it pops (minigames, pause menus). Generic over the scene type and typed against the lifecycle above, so it drives a 3D game just as well.SceneComponentHost/SceneComponent- composes a scene out of named sections (map logic, encounter logic, UI wiring), each its own module implementing whicheverScenelifecycle hooks it needs, instead of one scene class accreting every responsibility. A scene owns one host and forwardsupdate/resize/onSuspend/onResume/destroyto it - composition, not a base class, so it works the same whether the scene extendsSceneortwo-d.Scene2D.Registry- a named lookup registered once and read back by name:register/get/has/list, throwing on a duplicate or missing name by that name. The dispatch primitive underneath a catalog of factories or handlers (SceneComponentHostuses one internally) - a game still imports and registers each entry itself, no auto-discovery.createHandles/Handles- numeric leases for values a plain-data command cannot carry (callbacks, RNG streams, query closures):putstores a value and returns its id,getresolves it,drop/clearend leases, andwithscopes a lease so the drop runs even when the body throws. Ids start at 1 and are never reused.Logger- categories, four severity levels, a filter, and a sink tests can capture, instead of bareconsole.log/console.error. Three shapes cover what "an event" means here, kept deliberately distinct rather than folded into one bus: a GameEvent is a rule's ordered output - what happened, described after the fact - realised as the genericEventtype parametersimulation'sSimulationRule/SimulationRuntimeRulealready return, never something a handler reacts to mid-calculation. A Hook (HookRegistry, below) is a point of modification a rule consults while it runs -modifyDamage, "can this creature act this turn" - synchronous, and read back by the same rule that asked. A Signal is a plain notification with no bearing on any rule -sceneChanged,saveCompleted. Reaching for a global bus that severalAttackRequested/DamageCalculated/HpChanged-style events all dispatch onto, each triggering the next, is the shape to avoid: it turns a rule's synchronous, readable logic into a chain no single function owns.Signal- a typed event emitter: one event, one payload, LIFO listeners, and a listener can consume it.HookRegistry/Hook- many events keyed by name, dispatched in registration order, with asourcetag so everything one ability or worn item registered comes off in a singleoffSourcecall.battle.BattleHooksandroguelike.CombatHooksare both this with their argument types fixed; they were two copies of one class until they were merged.PresentationQueue- plays a batch of simulation events one at a time, at whatever paceplay(event)(a tween, a sound, a floating number) says to wait before the next - the presentation-side counterpart to aSimulationRuntime.dispatch()/runScenario()result, kept out of the logical commit that already happened. The same queued/timed shape astwo-d.ui. Toast, generalised to any event type and needing no renderer.parseCSV/CsvOptions/CsvColumnType- a header-row CSV into an array of typed row objects, so a content designer edits a spreadsheet instead of a.tsobject literal for a table shaped likeactors.AffixTable.columnsnames which fields coerce to'number'/'boolean'/'list'(semicolon-separated)/'map'(semicolon-separatedkey=valuepairs); an empty cell omits that field entirely, the same as an optional property never set. Takes a raw string with no opinion on how it was loaded, the same boundaryi18n.parseFTLalready draws.clamp-valuerestricted to[min, max]. Shared sotwo-d.render.Camera's bounds clamp andrpg.Collision's tile-edge resolver stop being the same three-line function written twice.parseDialogueLines/DialogueLine- the terse@id text/--alternation dialogue-text parsing engine behind bothtwo-d/stage.parseDialogueTextandrpg.parseDialogueText, so the two-speaker state machine lives once instead of twice: each module's own wrapper only renamesDialogueLine.speakeronto its own command shape (StageCommand.asversusEventCommand.speaker).parseTwee/TweeCommand/TweeChoice/TweeStory- the Twee-notation parsing engine behind bothtwo-d/stage.importTweeandrpg.importTwee, for the same reason: passages,[[links]]andStoryData/StoryTitleparse once here, andTweeCommand/TweeChoiceare structurally compatible with bothStoryScript/StageChoiceandEventStoryScript/EventChoice, so each module's wrapper needs no per-field conversion.Random(namespace) - seeded RNG:int,float,weighted,element,shuffleand the rest; every "pick one" returnsnullwhen there is nothing to pick.Generator- the seeded RNG classRandomwraps directly, for a game that wants its own instance.JavaRandom/JavaRandomDraw/JavaRandomOptions-java.util.Random's 48-bit generator bit for bit (nextInt,nextInt(bound),nextLong,nextDouble,nextFloat,nextBoolean), so a game ported from the JVM replays the same seed;onDrawlogs every underlying draw for a parity diff against a JVM run. Not for general use, that isGenerator.MersenneTwister/MersenneTwisterState- MT19937 withmt_rng'sseed/discard/discardCountbookkeeping, for a port that consumes the reference's raw 32-bit stream; the engine matches the standard test vector, while the int mapping is MWG's own unbiased reduction (C++'suniform_int_distributionis implementation-defined).RandomStreams- named, independent MT19937 streams over one base seed, so a loot draw and an AI draw never shift each other, withgetState/setStatefor a save andreseed.Input(namespace, soInput.onAction, not a bareonActionimport) - named-action input:bind/isDown/justPressed/justReleasedover keyboard,bindButton/bindAxis/pollGamepadsover a gamepad,bindTouch/pressTouch/releaseTouch/attachSwipeover touch,attachPointerfor down/move/drag/up signals with target-relative and (throughtoWorld) world positions,actionsForKeyfor rebind-conflict detection,rumblefor gamepad haptics.onTextcarries the character a key press produces (viatextFromKey/dispatchText) andonActionthe named action a key fires, each with its own programmatic counterpart (dispatchText,dispatchAction) so an on-screen keyboard feeds the same signals a physical key travels;onCompositionfollows an IME throughstart/update/end, so a free text field can be built on top.PlayerInput- a per-player scopedInput, for local multiplayer/split-screen.StoredValue/defaultStorage- one JSON value under one storage key (read(fallback)/write/remove) over the same storageSaveSystemuses: a game's structured progress or best scores without a save slot.FixedStepper- turns the variableScene.update(dt)into fixed simulation steps (advance(dt, tick), amaxSubstepscap,alphafor interpolated drawing): the deterministic, refresh-rate-independent loop a physics solver or lockstep game needs.SaveSystem- named, versioned save slots overlocalStorage(in-memory fallback underfile://); amigrationschain, andimportExternalas a plug-in point for a foreign save format's ownnormalizefunction.load,listandimportSlotread every slot throughparseInboundplus a{ meta: { version, savedAt }, state }shape check: a corrupt or tampered slot loads asnulland is left out oflist.StateRegistry/StateExtension/StateRestoreDiagnostic- named game-owned state extensions with deep-cloned snapshots and transaction rollback, with independent versions, ordered migrations and explicit handling of extensions absent from a restored save, so custom save data has one atomic boundary.ActionJournal- serializable ordered action and outcome batches with checkpoints, incremental reads, truncation and validated restore for replay, undo and synchronization. Actions and events are copied onappend, so both must be plain data.cloneData/uncloneablePath-structuredClonewith an error naming the first value that cannot be copied (action.target.reactions.rules[0].when), and that path on its own. The journal andsimulation.SimulationRuntimeuse it, so a command carrying a live actor fails with where and why instead of a bareDataCloneError.scramble/unscramble- light save-data obfuscation, not encryption.SaveSyncClient- pushes/pulls save data to a server the game supplies.LockstepClient- deterministic multiplayer over an injectable WebSocket, tick-driven. Server messages are size-capped (maxMessageBytes, 1 MB) and type-checked; a malformed one is dropped and reported ononProtocolErrorinstead of throwing out ofonmessage.stateChecksum/SyncGuard- out-of-sync detection for lockstep: a key-order-stable 32-bit checksum of JSON state, and a guard comparing the checksums two peers compute for each tick, recording the first tick they disagree on.RunHistory- a local record of completed runs (score, cause of death, whatever a game's ownsummaryholds), distinct fromSaveSystem's continuable slots.PlayerStats- a lifetime running total folded in one run's summary at a time, distinct fromRunHistory's per-run records.encodeShareLink/decodeShareLink/fitShareLink/dropByPriority/bisectShareLink- share links: a short URL whose fragment (#mwg:, never sent to a server) carries an envelope of content type,id@versionand a JSON payload, deflated, base64url and ending in a CRC-32 prefix that catches a link a messaging app cut short. The envelope is public and the payload is the game's own, validated by the game on read; reading is bounded (link length, inflated size) and every failure is areasonrather than an exception.maxCharsis 2000 by default, up to 4093.fitShareLinktakes the first of several candidate payloads, fullest first (dropByPrioritybuilds that ladder from a record), that fits;bisectShareLinkkeeps the most items of a list that fit. Applying a decoded link after the player confirms is the game's.TelemetryClient- sends usage/error events to a server the game supplies.NewsClient/NewsSeenTracker- a game's own patch-notes/announcement feed, with a seen-tracker so a player is only notified of what's actually new.checkSize/checkNoControlCharacters/sanitizeInboundText/validateSchema/parseInbound- security sanitization of inbound data: an imported save, a network payload, player-typed text.parseInboundis the whole pass in one call (size, control characters,JSON.parserefusing__proto__/constructor/prototypeat any depth), and everycorereader of JSON it did not just produce uses it: save slots,Collection, stored values, settings,NewsClientandLockstepClient. Underfile://Chromium shares onelocalStoragebetween every local page, so a game's own storage is inbound data too.Collection- named, queryable record collections overlocalStorage(all/get/put/remove/where/clear) - a quest log or bestiary, not a save blob.Recorder/Player/serializeReplay/deserializeReplay- recordsInput.onActionagainstGame's own frame counter and replays it deterministically, for testing. Both resume mid-run:RecordertakesfromFrameso a restored run continues its recording where the checkpoint left it, andPlayertakesfromFrameso playback skips what the restored state already contains.parseReplayEventsvalidates an already-parsed event array, the half of the checkdeserializeReplayapplies after its ownJSON.parse.exportReplayFile/importReplayFile/LastRun/runCheckpoint/resumeRunPlayer- a run made durable (roadmap 396). The file is a versioned, self-describing envelope (format: 'mwg-replay',version,framework,game, optionalseedandrecordedAt,events), and import refuses another game or another framework build by name, treating the file as inbound data the way a save slot is treated.LastRunkeeps the newest recording, one deep and bounded, through the same storage a save slot uses;runCheckpoint(recorder, state)pairs the partial recording with the game state a save already captured, andresumeRunPlayerturns the restored checkpoint back into a mid-runPlayer. Thefile://plumbing rides along:downloadReplayFileis a Blob download (nofetch()),pickReplayFileis the file picker resolving null on cancel, andreadReplayFilereads anyBlob, headless Node included.Achievements- counters crossing a target unlock a named milestone;drainNew()queues unlocks for a UI to announce. An achievement can name severalcriteriainstead of onecounter/target(item 273's sub-achievements, "recruit every unit type"), unlocking only once every criterion is met;subProgressreads them individually.Settings/GameSettings/SettingsOptions/CustomSettingValue- persisted player settings over the same storageSaveSystemuses (in-memory fallback underfile://): music and sound-effect levels, master mute, zoom preference, key bindings (applied toInputon load), and a game-definedcustombag for the rest (hints,violence, ...).defaultSettingsnames the fresh-game values,effectiveMusicVolume/effectiveSfxVolumeread 0 while muted, corrupt storage reads as defaults.Session- counts launches over the same storageSaveSystemuses, for a native wrapper's own rating-prompt timing; never prompts itself.FeedbackClient/HttpTransport/HttpTransportOptions- an injectable HTTPS JSON transport for player-submitted feedback; the game owns the endpoint, consent flow, and server-side storage. EveryHttpTransportOptionsclient (FeedbackClient,NewsClient,SaveSyncClient,TelemetryClient) andLockstepClientrefuses anhttp:/ws:url to anything butlocalhost/127.0.0.1/[::1]unlessallowInsecure: true, and a response body is read up tomaxResponseBytes(10 MB), refused as soon as it passes the limit.Spawner- adt-driven, timed, escalating wave spawner (a horde mode, a survival minigame), distinct fromroguelike.Scheduler's turn-order primitive.hexNeighbors/hexDistance/hexLine/hexRange/hexToPixel/pixelToHexwithHexShape/HexOrientation/HexOffset- hex grid geometry: cube-based neighbours, lines and ranges, and the pixel projection in both orientations and both offset parities, defaulting to the flat-top odd-q layout this module always had.pixelToHexanswers with the nearest cell centre rather than a closed-form inverse per combination, so it cannot disagree withhexToPixelat a hex's edge.weightedFlood/WeightedCell/WeightedFloodOptions- renderer- and rules-neutral Dijkstra flood over any cell topology, with injected costs, blockers, budget and stopping rules.cellInside/cellIndex/cellX/cellY/cellKey/cellFromKey- bounded-grid indexing: the row-major arithmetic and bounds check thatroguelike.Leveland theboardmodules used to spell out for themselves, now stated once (the Wesnoth port asked for exactly this, to stop duplicating it in its own systems).cellInsideis the shared bounds check andcellIndexis deliberately unchecked arithmetic, so a loop that has already proved its coordinates pays nothing while a module that wants a throw or a-1keeps that policy in its own method;cellKey/cellFromKeyare the'x,y'spelling forMap/Setmembership.NEIGHBOURS4/NEIGHBOURS8/CIRCLE8/euclidean/chebyshev- one frozen offset table per square neighbourhood, shared instead of copied: axial north/east/south/west, the eight surrounding cells in row-major order, and the same ring clockwise from north (the sequenceroguelike'sneighbourOffsets(8)always returned, now returned as-is from here).euclideanis the straight-line ruler,chebyshevthe king-move one (a diagonal costs one step), andmanhattanthe orthogonal-step count;distancemeasures with the ruler aDistanceMetricnames, so a data-authored rule picks its geometry by name.roguelike'schebyshevDistancedelegates tochebyshev.Blob- a spreading volume field over a grid: a per-cell number thatspreaddiffuses a share of into its open 4-neighbours and decays the rest (decay: 1conserves and only moves volume around).seedadds to a cell,clearzeroes one cell and leaves its neighbours alone, andcellsAbovelists what a game applies its effect on; what a volume means (fire, gas, ooze, water) stays the game's own reading.reducedMotion/setReducedMotion/prefersReducedMotion/watchReducedMotion- the one answer to "should this animate": the OSprefers-reduced-motionpreference, overridable per game, with a change subscription for something long-lived that has to stop mid-flight.motionDuration(duration, intent)is the policy built on it, andMotionIntentis the caller's declaration: adecorativemotion (the default) collapses, ameaningfulone is shortened rather than deleted, because a change that motion conveys stays readable.Tweener,Camera(shake and follow),ScreenEffects,ParticleEmitterandFloatingTextconsult it.AnimatedSpritedeliberately does not: a frame cycle is usually game state.Tweener/Easing/TweenOptions- a generictween(duration, apply, ease?)plus a small linear/quad/cubic easing-curve set. The third argument also takes{ ease, intent, alternate }:alternateis the non-vestibular variant to run instead under reduced motion (a fade where a slide was), and it is ignored while the preference is off.UndoHistory- gameplay-level undo/redo over pushed, reference-held states.LoadQueue- a reusable loading-screen lifecycle: named tasks, aggregate progress, cancellation.EntityRegistry/EntityId- assigns and looks up stable string ids for live objects, so events, saves, AI targets and buffs can name a creature or item without holding it; not an ECS, and distinct fromCollection's persisted, queryable records.idOfis what a game passes asroguelike.Scheduler.toJSON's orsimulation.SimulationRuntime'sactorId.ReactionTable/ReactionRule- declarativewhen/actionrules checked against any state shape (aStatBlock's values, a plain object's own fields), firing edge-triggered so a still-true condition never re-fires;onceretires a rule after its first firing. The generic alternative to a branch cascade ofif HP < nchecks in game code, and works the same for a character or an inanimate object such as an item's durability.
two-d
Everything drawn through PixiJS, and the counterpart to three-d. The barrel re-exports
render, ui and stage below, so mwg/two-d is a one-stop 2D import; the granular
subpaths (mwg/two-d/render, mwg/two-d/ui, mwg/two-d/stage) are what a game watching its
bundle should reach for.
Game- owns the PixiApplication, the frame loop, and the current scene; reachable as the singletonGame.current.step(dt)drives one frame by hand, which defeats Chrome's background-tab throttling ofrequestAnimationFrame. WithautoPause(the default) it suspends the loop and silences itsaudiorig (AudioSuspendRig, anOrchestratorfits) on page hide and resumes both on show, viasuspend/resume: the clock parks at 0, the top scene hearsonSuspend/onResume, and the player's mute setting is never touched.qualityScalingsteps the backing-store ratio down after sustained over-budget frames (and back up only after a much longer streak inside budget) via aQualityScalerover the static fit, off unless given. It draws at the display'sdevicePixelRatio, reduced to the largest whole number whose backing store fits the device'sMAX_TEXTURE_SIZE, with a warning naming the limit when that happens: WebGL clamps a larger request silently, so a device with a small limit renders softer rather than not at all.Scene2D-core.Scene's lifecycle plus thestagecontainer everything this screen draws hangs off, destroyed with the scene. This is what a 2D game extends.
two-d/render
Pixi-backed 2D rendering: sprites, tile maps, camera, animation. All Pixi
batcher/high-shader internals are confined to ColorTransformBatcher.ts.
ColorTransformBatcher/packColorAdd/packTintAdd/NO_COLOR_ADD- a per-spritetexel × M + Acolour transform (multiply and add) in the batch shader.TintedSprite/registerColorTransform- a sprite drawn through the colour-transform batcher; register the extension once viaGameOptions.extensions.parseImagePath/imageModifier/colorShiftMatrix/applyImageModifiers/croppedTexture- parses common image suffix modifiers such as~FL,~GS,~SCALEand~CROP, then applies the supported Pixi presentation changes without mutating shared source textures.channelScaleMatrix(~R/~G/~B) builds theColorMatrixFiltermatrixapplyImageModifiersalso applies for~O(opacity) and~CHAN(channelSwapMatrix- a channel-source swap/constant, not Wesnoth's own per-channel formula language).applyTextureModifiers/ImageTextureProbecover the modifiers that need real pixel access, a sibling texture, or have to rotate the actual art rather than a sprite transform:~RC/~PAL(exact palette swap - seerecolorTexturebelow - accepting hex or, viaprobe.resolveColor, a named colour;parseColorPairs/parsePaletteListsare the argument parsers, the latter reconstructing~PAL's two comma-separated colour lists sinceparseImagePath's own comma split does not respect that list boundary),~BLIT/~MASK(composite/mask against aprobe.resolveTexture- resolved sibling texture at an offset - it receives the argument exactly as written, nested modifiers included, not a bare path;maskPixelsis~MASK's renderer-free alpha-multiply core),~BLEND(blendPixels, an exact per-pixel lerp towards a colour, baked once - not the runtimeColorMatrixFilterapproximationblendMatrixstill offers on its own) and~ROTATE(rotatePixels, which rotates the source pixels and expands the surface, unlike a sprite's ownrotation;~ROTATE(degrees)samples nearest-neighbour, exact at quarter turns, and~ROTATE(degrees,linear)blends the four pixels around each sample point instead - what a non-square angle onto a hex grid needs.parseRotateModeis the second argument's parser, and both modes sample premultiplied so a transparent neighbour contributes coverage and never colour. It is bake-time work: measured,linearcosts 2.4 to 4.2 timesnearestper image - 0.45 ms against 0.12 ms for a 64x64 tile at 60 degrees, 3.6 ms against 1.5 ms at 256x256 - for a visibly smooth edge, once, while the texture is built).spriteColorMatrix(sprite, matrix)(item 309) attaches any of the matrix builders above (orblendMatrix, or a game's own) to a sprite as aColorMatrixFilter, the one remaining case that needed a directpixi.jsimport just to construct that one class.applyAllImageModifiers(sprite, parsed, probe?, scale?)(item 310) is the two-step recipeapplyImageModifiers's own doc comment names, run for a caller in one call: it bakesapplyTextureModifiers's exact pixel-level result intosprite.texturefirst, then applies the sprite-property/filter modifiers on top, so a~BLEND/~ROTATEpath never silently getsapplyImageModifiers's no-op for them.remapPixels/paletteRangeMapping/recolorTexture/withTextureCanvas/PaletteMapping/PaletteRange/PaletteRemapMode- palette-remap recolouring (team colour by range, not multiply/add):remapPixelsis the renderer-free core,'exact'by default (a pixel is repainted only on an exact match to afromcolour, correct for a short specific list like~RC/~PALand for apaletteRangeMappingover a reference palette, where the pixels outside it are the ones the art deliberately left alone) or'nearest'(every opaque pixel repainted by closest match, which a palette covering the whole image wants - the wrong mode for a sparse list, where it silently recolours the entire image).paletteRangeMappingbuilds a[color_range]-shaped mapping: the reference palette's first colour is its anchor and becomesmid, and every other entry is shaded by its own brightness, darker towardsminand brighter towardsmax, with the floored average and truncated blend a Wesnoth[color_range]team colour is defined in, so a range's shades come out byte for byte as the engine's own. The reference after its anchor needs no order.recolorTextureis the canvas-backed wrapper, built on the sharedwithTextureCanvashelperapplyTextureModifiersabove also uses.AnimatedSprite/Animation- frame-sequence sprite animation. A frame may carry its own duration (Wesnoth'simage=a.png:120,b.png:80), and an animation may start partway into itself or after a delay (startTime, which is whatstart_time=-450on an attack means: begin four and a half frames in, so the hit lines up with the damage frame). A frame may also carry an offset, reported throughAnimatedSprite.frameOffsetfor the caller to add where it positions the sprite, because that position is the caller's (GridMover, a walk tween) and a sprite that overwrote it every frame would undo it every frame.SpriteGroup/isOnScreen(item 369) - animate only what is on screen:update(camera, dt)advances the members insidecamera.view(plusmargin) and leaves the rest on their current frame, so a crowd ofAnimatedSprites (orParticleEmitters, which satisfy the sameSpriteGroupMembershape) costs no CPU off screen. Never freezes anything visible: under a rotated camera the tested box is the over-inclusive oneTileMap.culluses.SpriteSheet- a grid-sliced sprite sheet, or, built with no frame size, one whose frames are its ownrect(index, x, y, width, height)declarations: a hand-packed icon atlas, a strip of bar segments, an item whose art is smaller than its cell. A rect is cached under its index like any grid frame, soget/region/name/picktreat the two the same and no site hand-builds aTexture.Camera/createCamera/snapZoom- world-to-screen camera;snapZoomkeeps tile edges pixel-aligned at a fractional zoom. Fixed-angle view rotation (item 285):gridpicks four quarter turns (square, the default) or six 60-degree steps (hex),setRotationStep/rotatemove through them,toScreen/toWorldinvert the turn so a click still lands on the cell aimed at,viewreports the box around the turned viewport for culling, anduprightRotationis the angle that keeps a label drawn into the world upright. Free rotation (item 286):rotateToturns to any angle immediately andanimateRotationToeases towards one, shorter way around the turn, sharing the same angle and the sametoScreen/toWorld/viewmath the stepped API uses.shakeScreen(intensity, duration)(item 303) isshaketaking screen pixels instead of world units, dividing by the currentzoomonce so a caller thinking in screen pixels (a convention several engines use for this call) does not have to at every call site.Viewport/splitScreenHalves- a camera scoped to one screen region, for split-screen.createColorBlindnessFilter/COLOR_BLINDNESS_MATRICES- accessibility colour filters.Minimap/newlyRevealed/minimapCellCenter/MinimapMarker- bakes an explored-cell set into a persistentRenderTexture, never redrawing an already-baked cell, and supports several game-owned unit or quest markers.TileMap/EMPTY/tileFrame/tileFrameSheet/tileFrameIndex- tile map rendering: square/hex/isometric/staggered projections, multi-sheet tiles, elevation columns.LayeredSprite- layered character sprites (body/hair/equipment as separate layers).SkeletalSprite/ArticulatedChain/ChainRig(items 399/400) - bones driving drawn parts: the pose math lives in the renderer-freeskeletonmodule (bone tree, keyframed clips, crossfade, priority layers),SkeletalSpritebinds a part to a bone by name andupdate(dt)places it at the bone's world transform, andArticulatedChainis the follower rule a keyframe cannot express - a serpent's spine whose segments ease toward their parent's previous position within per-segment rotation limits.-Projectile- tweens a sprite in a straight line for a thrown/shot visual flourish; an optionalanimation(item 311) advances flight frames with the tween's own elapsed time, read back throughframe/frameOffsetso the caller can apply the frame's offset where it already positions the sprite.Flightsowns the live ones:add(sprite | texture, from, to, { speed, tint?, spin? | spinDegrees?, fadeIn?, onArrive? })returns a handle,update(dt)drives positions and dispatches each arrival exactly once,cancel()/clear()forget without dispatching; caller sprites stay caller-owned while texture-built sprites are parented here and destroyed with their flight.spinDegreesisspinin degrees per second (converted once, so an authored angular speed needs no radian arithmetic at the call site) andfadeIn: 'progress'(ortrue) eases the sprite's alpha 0 to 1 along the tween's own progress instead of seconds from launch.LightningArc- the position data for a jittered line between two points (a bolt, a tether):pointstapers to zero offset at both endpoints,retargetmoves either endpoint for a tether following two moving units, and an optionalflickerIntervalre-rolls the jitter on a timer. Geometry only, drawn by the caller throughShape2D'sGraphics.Beamis the drawn one-shot counterpart (a fading line, or a texture strip - repeating by default, orBeamTextureOptions'stretch: truefor art that spans the whole beam once, with ananchorplacing it across the thickness - additive by default,retargetmoving either end,thinnarrowing the body as it fades,updatereporting expiry once);Beamsowns the live ones withadd/update/clearand destroys finished beams.SpriteAttachment- ties a second sprite's position to a first one's:follow(x, y)applies an offset, and an optionaldurationmakesupdate(dt)reportdoneonce it elapses, so a permanent shadow and a temporary status icon are the same class with different options. Renderer-neutral likeProjectile/LightningArc; z-order and parenting stay the caller's.LiquidLayer/LiquidLayerOptions- an animated liquid surface under a map: one scrolling tiling quad per liquid cell with its own visibility tint (setCellColor), andripplerings that grow and fade; pure decoration, every child opts out of pointer events.FogLayer/paintFogPixels/FogCell/FogColor/FogLayerOptions- a smooth fog-of-war overlay: a small linear-filtered canvas withresolutionpixels per tile, so fog edges blur across cells and one cell can be shaded in parts;paintFogPixelsis the same shading as a pure function over an RGBA buffer.TileMap.stampRectcopies a block of frames into a layer at a tile position and throws when it does not fit;AutotileSet.mode: 'mixed'reads the MV A4 sheet's alternating rows with the floor table (wall tops) and the wall table (wall faces).Halo/HALO_ANIMATION- the glow around a unit, an aura, a shrine's light: anAnimatedSpritedrawn additively (Wesnoth's[halo] blend_mode=add) whosefollow(x, y)applies the halo's own offset once instead of in every game that draws one. Z-order stays the caller's, because a halo that belongs behind its unit is added before it and one that belongs in front is added after, and a framework that guessed would be wrong half the time.ParticleEmitter/Particle- a pooled, seeded particle emitter (sparks, dust, rain):burst/start/stopover a pool allocated once atmax. Runs the whole simulation with notexturegiven, which is how it is tested without a renderer.ParticleSpawnAreagives births a localrectorellipseextent instead of the emitter's single origin.scaleandalphatake aParticleCurve(tover the particle's life) as well as the birth-to-death pair,tinttakes a[from, to]range each particle draws its own colour from channel by channel, andflickerwobbles the scale every frame with a seeded draw (dropped under reduced motion). All three draws happen only when used, so an existing emitter's seeded spray is unchanged.attach(target)follows a display object or position thunk on every tick (withoffsetX/offsetY, anenabledcallback gating tick emission, avisiblecallback hiding the whole spray (fog-of-war gates) while the simulation steps on behind the invisibility, and auto-detach for destroyed targets);detach()keeps the last position.ScreenEffects/ScreenEffectStep- a full-screen colour wash:fadeOut/fadeIn/flash/setTint, driven byupdate(dt)returning true on the frame an effect completes.sequence(steps)(item 302) chains fade/hold/flash steps end to end as one call, for the hold-then-fade-back transition the four individual methods cannot express alone;updatekeeps returning false at every step boundary, true only once the whole sequence finishes.SceneTransition/TransitionOptions/TransitionRunner/TransitionDisposition+transitionDisposition,tileShrink,shrink,pop,grow,slide,irisanddissolve- screen transitions:Game.switchScene(Next, { transition, duration, easing })photographs the outgoing scene and animates it away (or grows the new one over it),Scene2D.transitionTo(swap, options)does the same for a scene changing its own content,Game.transitioningtells a scene to ignore input, a second transition requested while one plays is ignored unless its options sayqueue: trueorinterrupt: true(transitionDispositionis that rule as a plain function), and reduced motion skips it.tileDelays,tileProgress,popScale,openAnimationFrameandresolveEasingare the timing as plain functions.ActorAnimator- an idle/move/action animation state machine with one interruption rule.StatusVisuals- composes every active status effect's colour additively onto a sprite'scolorAdd(item 301), instead of one status winning by declaration order; each channel clips at 1 rather than wrapping. Never touches the multiplytint, so a sprite's own identity tint (a team colour) survives underneath.flash(color, strength, duration)layers a one-shot, linearly-decaying pulse (a hit, a heal) on top of whatever is active.SpriteMotion- squash, hop, bob, wobble, spin, shear and flip over any display object's transform through aTweener(item 404). Effects compose from the rest pose captured at construction, never drift, return aMotionHandle(done,cancel()), and followsetReducedMotion;destroyrestores the rest pose, flips included.ClothSprite- aphysicsClothmesh drawn as a textured sprite on the 414 mesh path (item 423): owns the simulation,update(dt)steps it and re-uploads the vertex buffer, the top edge starts pinned, UVs are never rewritten.setWind/pinPoint/unpinPointtune it, and it honourssetReducedMotion.Spring- a damped 2D chase spring for secondary motion (item 424): the anchor teleports, the bob lags, overshoots and settles through pure arithmetic, O(1) per update, deterministic for a fixedupdate(dt)sequence.place(target)parks a follower on the bob; honourssetReducedMotion.SpringSpritedraws the anchor-to-bob line, redrawn by an explicitsync()after stepping.HazeLayer/hazeField/paintHazePixels- an atmospheric haze sheet, never fog of war: translucent drifting weather with soft edges, density and per-plane wind, on a small canvas scaled up with linear filtering.paintHazePixelsis the same shading as a pure function over an RGBA buffer, which is how it is tested without a renderer.NeonGlow/glowFalloff/paintGlowPixels- a soft additive halo baked from a shape or mask, with colour, radius, intensity and flicker; a global bloom stays optional.loadTiledMap/TiledMapData/TiledTilesetData/TilesetSheet/LoadedTiledMap- loads Tiled JSON maps into aTileMap: orthogonal, isometric and staggered orientations, multiple tilesets (embedded or external.tsx). Lives here rather than inrpgbecause what it builds is aTileMapout ofSpriteSheets.blobIndex/autotileFrames/BLOB_SHAPES- auto-tiling: the 47-shape "blob tile" reduction of 8-neighbour terrain matches.rpgmAutotileFrame/RpgmAutotileAtlas- renderer-neutral RPG Maker MV autotile atlas geometry: decode one of the four atlas slots, select its four 24px source quadrants for a shape, and describe their 48px output position.RpgmAutotileAtlascaches repeated frame descriptions.RPGM_FLOOR_AUTOTILE_TABLE/RPGM_WALL_AUTOTILE_TABLEare the standard MV shape tables,RPGM_AUTOTILE_SLOT_BASES/RPGM_AUTOTILE_SLOT_COUNTSthe family address facts, andrpgmAutotileSlotclassifies a raw map id into its family (item 370).TileMapaddAutotileLayeroverAutotileSet/AutotileCell- a native autotile layer: raw MV ids or XP values route to the set claiming them, and the map assembles each cell's quadrant halves straight from the source sheet (item 370).setAutotileFrame/getAutotileFrameadvance and read the animation frame;setTile/getTilekeep working with raw ids.XP_AUTOTILE_PATTERNS/XP_NEIGHBORS_TO_PATTERNare the XP template and neighbourhood tables,xpAutotileRef/xpAutotilePatterndecode and compute XP cells, andautotileCellParts/assertAutotileLayoutresolve and validate one cell's pieces renderer-free.AutotileSet.tableEdgewithrpgmTableEdgeCellsoverRpgmTableEdgeMap- the front-edge strip the engine draws under an A2 table:rpgmTableEdgeCellsfinds the cells that get one (table above, none here, no A3/A4 wall on the ground layer) and the set draws its two half-height pieces.TileMapaddShadowLayeroverShadowLayerOptionsadds the engine's shadow bits (map layer 4) as translucent quarter-tile quads.resolveTerrainGraphics/matchTerrainRule/squareRotate/hexRotateoverTerrainRule/TerrainCondition/TerrainImage/TerrainPlacement/TerrainFlagsAt- a rule-driven[terrain_graphics]-style transition pass for whatAutotile's fixed 47-shape table cannot express: flag conditions at arbitrary offsets, one rule placing more than one image (so a piece bigger than one cell is the rule's own data, not somethingTileMaphas to hold),rotations(squareRotate's 4 exact steps orhexRotate's 6) andprobabilitybreaking a tie among rules matching at equal specificity. Returns plain placement data.TerrainGraphicsLayeroverTerrainGraphicsLayerOptions- the renderer for that data: oneSprite2Dper placement, added inlayerorder (a stable sort, so the rule pass's own row-major walk survives within a layer), each resolved through the asset resolver at the pixel position the caller'sprojectcallback reports.setPlacementsredraws; z-order against the rest of the scene is the caller's, as it is forHalo.inspectGraphicsCapabilities/detectWebGpu/RENDERING_DECISIONS- checks WebGL/WebGPU/WGSL support and records this project's own rendering-backend decisions.Container2D/Texture2D/Rectangle2D/Rect/TextureRegion/rectOf- the renderer boundary names for common 2D values. The first three work in both type and value positions, so a game can construct a container or use texture and rectangle constants without namingpixi.js. For the rare need the facade does not cover,two-d/pixi-interopre-exports the underlying Pixi classes explicitly.Node2D/Shape2D/Text2D/Sprite2D/TiledSprite/Gradient- bare, MWG-named re-exports of Pixi'sContainer/Graphics/Text/Sprite/TilingSprite/FillGradient: a plain grouping layer, vector drawing, one-off text, a plain untinted sprite, a repeating/scrolling texture, and a gradient fill/stroke, none of which needed new behaviour, only a name a game can import without namingpixi.jsitself. Preferui.Label/BitmapLabeloverText2Dfor anything styled throughui.theme(), andrender.TintedSpriteoverSprite2Dthe moment a colour transform is needed.createLayers(parent, names)(item 312) - builds one labeledNode2Dper name, attached toparentin the order given and returned keyed by name, so a game's conventionally-ordered layer stack (terrain, units, effects, UI, ...) is one call rather than a dozen hand-wired containers.
two-d/ui
Windows, lists, message boxes, HUD widgets - all themed from one live-swappable Theme.
theme/setTheme/defaultTheme/highContrastTheme/themeChanged- the active theme;setThemefiresthemeChangedso already-built widgets restyle in place.Label- a themed text wrapper over PixiText;stroke/resolution/roundPixelsoptions, useful for text over artwork.parseMarkup/stripMarkup/markupToHtml/escapeHtmloverMarkupSpan/MarkupOptions- inline markup (<b>,<i>,<span color='…' size='…'>,<br/>,<img>path</img>,$name, and the five entities) as a renderer-neutral contract: aMarkupSpanis aMarkdownSpanplus the colour, size and image a renderer may use, kept as written because resolving them is the renderer's business. Unknown tags, malformed tags and unset variables stay literal rather than disappearing, andmarkupToHtmlis the escaped HTML fragment for a renderer that speaks HTML text - it now also renders colour and size, as an inline style.MarkupOptions.tags(item 313) names extra tags to treat as styling containers; a run inside one carries its name inMarkupSpan.tag, so a renderer can resolve its style (Pixi'stagStyles) while the parser stays renderer-neutral, andmarkupToHtmlre-emits that tag as a real element forHTMLText.markupAccessibilityTextis the proper accessibility projection (an image becomes a caller-described string, not its raw path, whichstripMarkupkeeps for round-tripping instead).layoutMarkupLineswraps already-parsed spans word by word against a caller-suppliedMarkupMeasure, each word measured under its own span's style - wrapping computed after styling, so a bold or larger run wraps where its own wider glyphs actually land - andpositionMarkupLinesplaces those already-laid-out lines intoPositionedMarkupSpanruns (onex/yper span) under aMarkupLayout'sdirection/align, so anrtlline flows right to left and alignment defaults to the direction's own edge. Both are backend-neutral: the same spans,measureandmaxWidthdecide the same line breaks and the same runs whether a caller then draws them throughHTMLTextor a canvasText2Dpass.MarkupText/MarkupTextOptions- the canvas backend for that contract, the counterpart toRichLabel's HTML text: oneText2Dper styled run and oneSprite2Dper<img>span, resolved through the asset resolver, so an image is drawn rather than a caller positioning it itself at alayoutMarkupLinesoffset. It readsdirectionfrom the theme and takesmaxWidth/lineHeight/align/resolveImage/variables/resolutionoptions, plustagStyles(item 313): a PixiTextStyleOptionsrecord keyed by custom tag, applied to each run and used when measuring it, so<quest>...</quest>is styled and wraps under its own font.RichLabel/parseMarkdown/stripMarkdown/sliceSpans- basic inline markdown (**bold**,*italic*, combined***both***, backslash escapes) through PixiHTMLText, which is what makes mixed styles inside one string possible at all.parseMarkdownis pure span-splitting (unmatched markers stay literal);stripMarkdownrecovers plain text;sliceSpanstakes the first N visible characters with styles kept, for progressive reveal without leaking half-shown markers. Costs more than aLabel, so this is for descriptions and help bodies, not per-frame numbers.tagStyles(item 313) takes PixiHTMLTextStyleOptionskeyed by custom tag; a registered tag survives escaping as a real element forHTMLTextto style, while an unregistered one is still escaped as literal text.startReveal/advanceReveal/completeReveal/revealComplete- the shared progressive-display primitive: one character count behindMessageBoxpages (which already revealed this way),Labellines, andRichLabelmarkdown.LabelandRichLabelexpose it asshowProgressive/updateReveal/completeReveal, driven by the game's own frame loop;MessageBoxkeeps its confirm-completes-then-advances rule on top unchanged.BitmapLabel/bitmapLabelStyle- bitmap-font-backed text, for a HUD value redrawn every frame whereLabel's per-string texture re-render would be wasteful.NinePatch- a resizable nine-slice panel.Window- a themed panel container.open({ animation, from })playspop,grow,slideorfade(WindowAnimation) andclose({ animation })plays it backwards before closing, both driven byupdate(dt)and skipped under reduced motion.close()frees the window and its contents and is idempotent;closedis the guard for the caller's reference afterwards (input andplaceon a closed window are no-ops), andWindowStack.pushrefuses one with a named error. A window's own area is not clickable, so a click travels to whatever is under it;blocker: trueadds the full-viewport layer that swallows clicks and dismisses the window for a click outside it (handleOutsideClickis that decision).WindowStack- keyboard focus goes to the top window only; dims what's underneath;blockeron a window is what stops clicks reaching the map behind it.handleAction(action)is public so a scene with its own key handling can ask the windows first:Input.onActionoffers an action to the most recently registered listener first, so a scene that registers after building its stack is asked before the windows are and can swallow a key they need.MessageBox.blocker- whether a click under the box reaches the map behind it; on by default, and since the box is not closable that can only ever swallow clicks rather than dismiss it.blocker: falseis for a box the player is meant to click past.ListView- a scrollable, keyboard- and pointer-navigable row list (click, wheel-scroll);multiplechecks rows instead of picking one, read back throughcheckedIndexes.IconGrid- a multi-column icon-grid inventory view; tap-then-tap "drag and drop", frame-driven long-press for a quickslot.TabbedList/ListTab/TabbedListOptions- a renderer-free model for an inventory, journal, shop or codex: caller-supplied tabs and rows (no assumed taxonomy), an optional label, filter and disabled predicate, selection that skips disabled rows, a page derived from the selection rather than tracked beside it, and anopenDetail/closeDetailstate.Slider/SliderOptions+sliderFraction/sliderValueAt- a draggable knob on a themed track; the two pure functions are the value/track arithmetic (clamping, the step grid, the degenerate range), so a game routing its own input snaps the same way.Checkbox/CheckboxOptions- a ticked-or-not box; a caption is aLabelthe game places beside it, andonChangefires only when the state moves.RadioGroup/RadioOption/RadioGroupOptions- exactly one choice among several: arrows move the selection and select at once, a tap selects directly, disabled options are skipped.Spinner/SpinnerOptions+spinValue- an up/down numeric stepper;spinValueis the rule (snap to the step, then clamp at the ends or wrap past them).DropdownView/DropdownViewOptions- the widget over aDropdownmodel: a button showing the choice that opens a column ofButtons on an opaque backdrop, the highlighted one marked withButton.setSelected.maxVisibleRowsmakes a long list scroll (wheel, touch, keys, scrollbar) and it opens with the chosen row in view;autoWidthfits the longest label,maxWidthcaps that widening, andresize(width)re-lays the button and list in place;handleKey(arrows, Page, Home/End, Enter, Escape) andhandleOutsideClickare what the scene forwards from its own input;fitBelowlimits the rows to what the viewport leaves under the button.Dropdown/DropdownOption/DropdownOptions- a renderer-free option button: a selected entry, an open list, a highlight that starts on the selection and skips disabled options.setHighlight(i)opens the list on that row, sosetHighlight(i); confirm()is a whole programmatic pick.TextModel/TextModelOptions- the editing state behind a text field, fed bycore.Input'sonText: caret, anchor, selection-replacing edits, a length cap and a mask. Renderer-free.multilinekeeps real newlines with row navigation (lineCount,caretLine,lineRange,moveCaretLineholding a sticky goal column); single-line fields strip them on every edit.TextPrompt/TextPromptOptions- a modal single-line text field over aTextModel(a name entry, a save-slot label): typing throughonText, caret and edits throughleft/right/backspace/deleteactions,confirmgated byvalidate(the error shows and announces assertively, the prompt stays open),cancelgiving up. IME compositions preview until they commit, and the message plus validation errors announce throughscreenReader. The interface example's Name button is the reviewed hosting pattern: push it on aWindowStackand it owns focus, closing and the stack's un-popping by itself. The touch half (roadmap 402):keyboard: UPPERCASE_KEYBOARD(or anyKeyboardLayout) hosts one beneath the field and resizes the window to fit it -OnScreenKeyboard, next.OnScreenKeyboard/KeyboardLayout/KeyboardKey+UPPERCASE_KEYBOARD/pressKey/layoutKeys- pointer-only typing: a layout is rows of keys as plain data (ragged rows fine), each key a caption, thetextit types (defaulting to its label), or the namedactionit fires instead, plus aspanin row units for a space bar. The class renders it through the theme's ownButtonand reportscontentHeightso a host can size itself;pressKeyis the pure dispatch rule (dispatchTextfor a typing key,dispatchActionfor an action key, the two paths a physical key travels, so no listener can tell a tap from a keystroke) andlayoutKeysthe rows-to-buttons arithmetic behind it.UPPERCASE_KEYBOARDis the one shipped default: the alphabet over a digit row, thenBksp, caret moves, space,OK,Esc.DataTable/TableColumn/DataTableOptions- a renderer-free columned table: sort by a column (toggling direction), a highlight that skips disabled rows, and a page derived from the highlight.TreeView/TreeNode/TreeRow/TreeViewOptions- a renderer-free collapsible tree, flattened to the rows on screen; collapsing the branch the highlight is inside lands it on the branch.ScrollBox/ScrollBoxOptions/ScrollKey+scrollOffset- a clipped viewport with a themed scrollbar;scrollOffsetis the clamping rule the wheel,scrollByandscrollIntoViewall share.touch: truelets a finger drag the content (a drag cancels the tap on the row under it) and coast on release,handleKeyscrolls by line, page and to either end, the thumb drags and the track jumps, andautoHeightsizes the viewport to the content up toheight. The box owns itshitArea(its viewport, kept current byresizeandsetContentHeight), so the wheel reaches it without a row under the pointer.update(dt)drives the coasting.ScrollDrag- touch scrolling as plain numbers (start/move/end/step): how far the content follows a finger and how it coasts after release, with no pointer events or clock of its own.ScrollText/ScrollTextOptions- a wrappedLabelin a self-sizingScrollBox: as tall as the text up tomaxHeight, scrolling beyond it, re-measured bysetTextand re-wrapped in place byresize(width).MarqueeLabel/MarqueeLabelOptions+marqueeOffset- one line that stays on one line: a long text slides back and forth in a clipped window (and holds still, cut short, under reduced motion).ellipsizeandtruncateLines- cut a text to a width or a number of lines, ending in an ellipsis, with the caller supplying the measuring function.flowLayout(items, width, { gap, align })- left-to-right rows that wrap, returning each item's position and the total height: a toolbar that takes two rows on a phone.Grid/GridSpec/GridTrack+resolveAnchor/anchorAlignwithAnchor/AnchorSpec/LayoutRect- data-driven shell layout (item 263): named anchors placed inside bounds (margin, offset, fill), and columns/rows that aresizeorgrow, so one sidebar plus one content column needs neither side to know the window's width. Pure geometry, renderer-free.SkinswithSkin/SkinStates/SkinData/WidgetState- per-widget, per-state looks looked up by name, the counterpart the one globalThemehas no room for. A lookup falls back widget state -> widget idle -> wildcard state -> wildcard idle ->{}, andSkins.fromreads the plain data a config file would parse into.MessageBox- dialogue text box: paged reveal, choices, ADV/NVL display modes,autoAdvance, and timed{sound:path}markers delivered throughonSound.convertTextrewrites every page's text, speaker and choice label as the box is built, so a game resolves display-time codes once in the renderer.messageBoxPresenter- wiresrpg.EventRunner's dialogue to aMessageBoxon aWindowStack, in one argument:present: messageBoxPresenter(this.windows). Takes the sameconvertTextoption.VerticalLabel/layoutVertical- vertical writing layout, with CJK glyph rotation.RebindScreen- a keybind-rebinding flow overInput, with an optional conflict hook.SettingsScreen/SettingsCustomRow/SettingsScreenOptions(item 364) - a ready-made settings screen overcore.Settings: music/sfx sliders, mute checkbox, zoom slider, one row percustomdescriptor (boolean/number/choice), an embedded controls page that writes rebinds back intoSettingson exit, and a reset row.cancelon the main page returns false for the caller to close.Button/ButtonSkin/ButtonState- idle/hover/pressed/disabled clickable region, icon and/or text, optional per-button nine-patch skin with per-state tints.Bar- a filled-proportion track (health/mana/XP); flat colour or texture, optionalroundUpToPixelso a nonzero value never rounds down to invisible.Meter/MeterOptions(item 368) - a discrete meter: N icons with a value filled, the heart row or star rating toBar's continuous fill. Fractions clip mid-icon through a mask, so halves need no extra art; flat theme pips by default, a game-supplied filled/empty texture pair (which must come together) otherwise.FloatingText/floatingTextAlpha/floatingTextRise/floatingTextAgeAtLeast- a rising, fading damage/pickup number and its pure motion, opacity and age curves;FloatingTextStackwithfloatingTextStackLift/floatingTextStackMoves/floatingTextStackLifePenaltyandFLOATING_TEXT_STACK_GAPstacks simultaneous pop-ups at one world point without overlap, lifting the lines already there the way Java'sFloatingText.push()does.MessageLog/linesToDrop/takeLastEntries/MessageLogEntry/MessageLevel/MessageLogOptions- a player-facing message pane: one coloured block per message (info, positive, negative, warning, highlight) that drops the oldest blocks once the wrapped lines pass a cap;linesToDropis that budget as a pure function, andlastEntries(n)reads the newest retained entries back as{ text, level }records (oldest first) without scraping rendered children.fitWindowZoom/sharpenText- step a window stack's integer zoom down until its content fits the viewport, and re-rasterise everyTextunder a zoomed container at device ratio times its on-screen scale so it is not magnified 1x text.Toast- a queued, timed pop-up notification (fade in, hold, fade out).Tooltip- a hover explanation over a themedWindow: a frame-driven hover delay, and edge-aware placement that flips rather than letting the panel run off screen.TooltipHost/TooltipHostOptions/TooltipTarget- drives oneTooltipfrom rectangles: a press that rests forlongPressseconds shows the text on a touch screen, and a disabledButton(which gets no pointer events) is still explained from its bounds and itstooltip.attach(pointer)wires anInput.attachPointertracker.HelpScreen- a topic-list-plus-body help/controls screen.StatsScreen- a player-stats display screen.LoadingScreen- a progress-bar loading screen wired tocore.LoadQueue.ScreenReader/screenReader- the screen-reader bridge:Windowannounces its title,MessageBoxeach page and its choices, andTextPromptits message and validation errors through a visually hiddenaria-liveregion, and a game's own widgets use the samescreenReader.announce(text, { assertive }). No-ops where there is no DOM, so ordinary scene code needs no guard around it.contrastRatio/meetsContrast/relativeLuminance/ContrastLevel- WCAG contrast for athemepalette: the standard luminance and ratio formulas with AA/AAA thresholds, so a game checks its own colours rather than guessing.
two-d/stage
Dialogue scenes: backdrop, characters, a script runner - the visual-novel half.
DialogueStage/CharacterDefinition/ShowOptions- backdrop plus character conversation scenes; the speaking character lit, others dimmed.StageScript/StageCommand/StageChoice/StoryScript- a command interpreter;runStoryfollows a named-passage graph ({goto}, choice jumps);history/showLastfor read-only dialogue rollback;skipSeenfor auto-advancing already-read lines;ScriptOptions.mode: 'nvl'for the accumulating-block presentation.importTwee/TwineStory- imports Twee-notation Twine stories intoStageScript's command format, a thin wrapper aroundcore.parseTweenaming theStoryScriptreturn type.rpg.importTweeis the same wrapper around the same engine, targetingEventStoryScriptinstead, so a text-only RPG needs no Pixi dependency to import a branching Twee story, and a visual novel that never touches map events needs norpgdependency either.parseDialogueText/extractDialogueCatalog- a terse line-based dialogue format for straight (non-branching) scenes:@id textnames the speaker, an unprefixed line narrates, and- textalternates between the first two speakers established by@idlines (capped at two - a third distinct speaker is refused). A thin wrapper aroundcore.parseDialogueLines, mapping itsspeakerfield ontoStageCommand'sas.extractDialogueCatalogbuilds an identityi18ncatalog from the resulting commands (or aStoryScript), the same shape MWL's ownextractCatalogbuilds from_("...")-marked strings.rpg.parseDialogueTextis the same wrapper around the same engine, targetingEventCommandinstead, so a text-only RPG needs no Pixi dependency to use it, and a visual novel that never touches map events needs norpgdependency either.StoryScreen/StoryScreenOptionswithStorySequence/StoryBeat- the between-scenario interludeDialogueStageis not: a full-screen backdrop, title and text advanced by a click. The sequencing (advance/back/goTo/skip/restart, and the current beat'smusicreported as a signal so the game plays it) lives in the renderer-freeStorySequence, and aStoryBeathas the same{ text, title, image, music }shape asMwlWorld.story.
assets
The file:// story: a compiled build resolves paths through a data: URI map; dev mode
serves them normally. Load once per scene; everything after is synchronous. The compiler side
of this (tools/compile-resources.mjs, tools/emit-page.mjs, tools/single-file.mjs) offers
independent build options on top of that one story - multi-file vs. single-file output,
compression (gzip or brotli, single-file only), WebP image conversion and SVG optimisation
(svgo: true, ids and viewBox kept, only when smaller) - documented in
DEVELOPMENT.md's "Build options" section and tools.md's tool reference; none of them
change what a game calls to load an asset.
Split by renderer specificity: assets/paths.ts resolves paths and needs no renderer,
assets/binary.ts caches raw bytes just as renderer-free, assets/loader.ts fetches and
decodes textures through Pixi. The assets barrel exposes all three; a game rendering
through something else imports @datamoc/mwg/assets/paths or
@datamoc/mwg/assets/binary and pays nothing for Pixi, which is how 3d and audio
reach the compiled asset map without it.
setBase/setAssetMap/isCompiled/paths/has/resolve- dev-vs-compiled path resolution, renderer-free (also its own entry point,@datamoc/mwg/assets/paths).setAssetMaphands these a game's own map directly (item 300), for a game bundled by its own tool rather thantools/compile-resources, and takes priority overwindow.__MWG_ASSETS__while set;undefinedreverts to it. A value is a URL string, typed bytes (aBlob, or{ bytes, type }), or a function returning typed bytes.load()reads typed and lazy values once and mints ablob:URL for them, carrying their MIME type to the texture loader.resolvethrows for them untilload()has run, andreleaserevokes the minted URL. A missing key throws at load time.load/texture/get/isLoaded/release- load assets by path, read them back synchronously, and free GPU memory once a zone is no longer needed.load(paths, { resolution })rasterizes a vector source (SVG) at a multiple of its intrinsic size, so a game that zooms into an icon asks for a2or3instead of shipping a soft bitmap;onProgressis the sameLoadQueueseam whether passed alone or in the options.optionalloads those paths separately so one missing file never aborts the required batch, callingonMissingfor each;texture/gettake afallbackreturned instead of a throw for a path that never loaded, so a caller stops needing its own try/catch around a missing asset.sanitizeSvg/svgScaleVariants/svgDataUri/splitSvgGroups(item 398) - SVG as inbound data:sanitizeSvgrefuses what it cannot prove inert by name (script,foreignObject, event handlers,javascript:and activedata:URLs - a gate, never a silent rewrite),svgScaleVariantsrasterizes one proven-inert source once per named scale with its resolution hint, andsplitSvgGroupsloads a group-authored SVG as one standalone part per top-level<g id=...group, the authoring form for an SVG puppet.loadalso supplies Pixi'sAssets.addaformathint - the original path's own extension- whenever the resolved
srcis a compiled build'sdata:URI (item 308): adata:URI carries no extension of its own for Pixi's resolver to pick a parser from, which otherwise left a game no way to load a compiled asset without building its own{src, parser}descriptor by hand.
- whenever the resolved
loadBinary/getBinary/isBinaryLoaded/releaseBinary- the renderer-free counterpart toload/texture/.../release, caching rawArrayBuffers instead of Pixi textures (also its own entry point,@datamoc/mwg/assets/binary);3d/models'Vox.parseVoxis the direct consumer, taking bytes this now fetches and caches instead of a game doing it by hand.AssetStream/AssetBundle- preloads likely-next bundles with LRU eviction under a byte budget, for progressive loading between scenes.fetchWithByteProgress/ByteProgress/OnByteProgress- real cumulative byte-progress reporting, for a server or desktop host (notfile://, which blocksfetchentirely).
audio
Sound- a pooled, round-robin one-shot sound effect player.play(gain, pitch)takes an optional pitch asHTMLAudioElement.playbackRate(1 unchanged); there is no matchingpan- a plain<audio>element has no pan of its own, the same reasonPositional.audioPanonly reports a number rather than owning a panner node.suspendcuts in-flight sounds and silencesplayuntilresume.onCaption/CaptionEvent- accessibility captions fired alongside a sound cue.Music- crossfading background music.suspendpauses the current and fading tracks and freezesupdateuntilresumerestarts the current one where it paused.duckglides to an attenuated level untilunduck, for dialogue and menus.createAudio/Playable- an injectable audio backend (tests supply a fake in place ofnew Audio()).Orchestrator/OrchestratorState- maps a named game state ("combat", "boss") to aMusictrack and crossfade, and fires one-shot cues by event name; re-entering the same state never restarts its track.suspend/resumesilence and restore the music and every cue.AudioListener/SoundSource/AudioFalloff- positional audio: a listener the game moves, aSoundplaced at a point, andaudioGain/audioPanturning distance and heading into a volume and a stereo position.SoundSource.playForapplies the distance gain throughSound.play; panning is reported for a backend that has a panner.Musicstays global.XpbdWorld(@datamoc/mwg/physics/xpbd) - a deterministic structure solver (extended position-based dynamics):addNode/addLink(stiffness,tensionOnlycables,strength/tensionStrength),applyForce,step()on fixed substeps, the load of every link read back (force, smoothedratio,peakRatio) andonBreakwhen a link passes its strength;ground(a height or a function of x) holds nodes on a surface withgroundFriction. The engine of Pontifex, generalised: bridges, trusses, cranes, anything where the question is which beam fails.XpbdWorld<T>/XpbdLink<T>/XpbdLinkSpec<T>carry game-owned linkdatawithout affecting the solver.breakLink(index)dispatches rupture once.linksInBoxincludes segment crossings;linksNearorders nearby unbroken segments by distance.snapshot()returns positions, velocities, load state and queued forces as detached arrays;hash()compares their exact numeric bits for runs with the same configuration. Payloads and ground callbacks are excluded. The bridge example applies a moving vehicle's weight withapplyForce.PhysicsWorld(@datamoc/mwg/physics) - rigid bodies and constraints in plain TypeScript, no engine dependency:addBox/addCircle/addConstraint/remove,update(dt)fromScene.update,onContact,alphafor interpolation. A constraint withmaxStretchbreaks when pulled that far past its rest length (onBreak,stretchOf). Bodies never spin;angleis a display offset.synthesizeTone/synthesizeSound/playTone/Waveform- a runtime square/triangle/sine/noise/sawtooth waveform synth for a procedural tone or SFX, no sample library;synthesizeSoundreturns a pooledSoundover the tone.parseMidi/scheduleMidi/midiLoopStart/noteToFrequency/MidiPlayer- a small.midfile player built onsynthesizeTonerather than a licensed instrument library: program, bank, controller and pitch-bend events plus the controller-111 loop start, with each scheduled note carrying its channel voice.MidiPlayer.create(file, { humanize })humanizes first and then schedules, for the same player with a performance behind it.renderMidiToBuffer- renders a parsed.midfile to stereo PCM frames, offline and deterministic: browsers cannot decode MIDI, so the file is synthesized once and plays back like any decoded track, with its controller-111 loop region. An optional game-suppliedSoundFontvoices real samples; without one the built-in waveform synth plays.renderMidiToBufferAsync- the same render as raw MIDI bytes, run on a worker thread so the seconds a real track costs (2 to 11.8) do not freeze the loading screen. Parse, schedule and voice lookup stay on the caller's thread; the stereo buffers come back transferred rather than copied, and the result is bit-identical to the synchronous render because both call the same function.soundFontBytestakes raw SoundFont 2 data.parseSoundFont- reads SoundFont 2 bytes into layered preset voices (key and velocity ranges, tuning, loops, envelopes, pan, attenuation); the font file itself stays the game's to ship and to license. A SoundFont 3 (compressed samples) is refused by name rather than read as noise:loadSoundFontreads either kind.loadSoundFont- reads a SoundFont 2 or 3 into layered preset voices: a plain font goes straight toparseSoundFont, a SoundFont 3 is decoded first, throughwebAudioSampleDecoder()in a browser unless{ decode }names another decoder. It is what a game calls on the bytes it ships, whichever kind they are.decodeSoundFont- a SoundFont 3 as plain SoundFont 2 bytes: every sample a preset reaches goes through the decoder once (an unreachable one never does), and the loop points a SoundFont 3 states relative to its sample become the absolute ones SoundFont 2 uses. A SoundFont 2 comes back unchanged. The result feeds anything that takes SoundFont 2 bytes,renderMidiToBufferAsyncincluded.encodeSoundFont- the other direction: a SoundFont 2 compressed into a SoundFont 3. Every sample a preset reaches goes through the encoder once, loop points become relative to their sample, the sample type gains the compressed flag and the INFO states version 3. The encoder is yours to give (mwg-soundfonts converthands it ffmpeg for Vorbis and Opus);flacSampleEncoderwrites lossless FLAC with nothing installed.detectSampleCodec/SampleCodec/SAMPLE_CODECS- which codec a compressed sample uses, read from its first bytes: Vorbis and Opus both sit in Ogg pages (told apart by their first packet), FLAC is a native stream. The format itself only flags a sample as compressed, so a reader has to look; an unknown codec is named in the error instead of failing opaquely in a decoder.encodeFlac/decodeFlac/flacSampleEncoder/flacSampleDecoder- a small lossless FLAC codec for one mono 16-bit sample, tested against ffmpeg in both directions. The decoder reads any FLAC stream up to 24 bits (constant, verbatim, fixed and LPC subframes, every stereo mode, left channel kept) and bounds the length and rate a stream claims before allocating.webAudioSampleDecoder/SoundFontSampleDecoder/SoundFontSampleEncoder- the decoder contract (compressed bytes, the sample's own rate and its detected codec in, mono 16-bit frames out, synchronous or not; the encoder is the same contract reversed) and the browser implementation of it overdecodeAudioData: each sample decodes in a context created at its own rate, so the browser never resamples it. A Node tool passes a decoder of its own.collectSoundFontUsage- lists the (bank, program) pairs scheduled notes actually request across MIDI files, drums as bank 128, for the compile step to keep.missingSoundFontPrograms- the pairs of a usage list a font does not carry (the bank 0 fallback counts as carried, a drum kit needs that exact kit), on a SoundFont 2 or 3. The build runs it for you: each embedded font's result lists itsmissingprograms andmwg-emitwarns.mergeSoundFonts- joins several SoundFont 2 files (or several SoundFont 3 files, the Ogg bytes copied untouched) into one, presets in the order given and the first font winning a shared (bank, program), which is how single-instrument files (the per-program collectionmwg-soundfonts fetchreads) become the one file a game ships.subsetSoundFont- writes a smaller valid SoundFont 2 (or 3) file carrying only the used programs, zones and samples copied verbatim and indexes remapped, so a game ships megabytes instead of tens of megabytes with identical voices. The build does this on its own:compileResources(and soemitPageandmwgPage) embeds.mid,.sf2and.sf3assets, and subsets every embedded SoundFont to the programs the embedded MIDI files use.soundFont: falseembeds the font whole,soundFontUsagenames the programs explicitly, and with no shipped MIDI the font embeds whole because the usage is unknowable. A plain.sf2is then compressed into a SoundFont 3 in Opus (the asset key does not change, andloadSoundFontreads either kind):soundFontCodecpicks'vorbis'or'flac'instead, orfalsekeeps the SoundFont 2. Opus and Vorbis are encoded byffmpegon the developer machine (cached between builds); without it the default keeps the SoundFont 2 and the build says why, while a codec named explicitly fails the build.soundFontBitrate(kbit/s, 6 to 510) sets how much an Opus font keeps andsoundFontQuality(-1 smallest to 10 best) does the same for Vorbis; each is ignored by the other codecs, and a measured guide is in tools.md. A.sf3is embedded as it is. The reference bank indata/microgm-opt.sf2(2.6 MB, WTFPL, shipped in the npm package for a game to copy into its assets) drops to tens of kilobytes per game this way: measured, piano and drums come to 264 KB, eight essential programs (piano, guitar, bass, strings, brass, flute, synth lead, drums) to 421 KB, sixteen to 585 KB. A program the font lacks plays the font's own piano (preset 0:0) and, only when it has no piano either, the built-in synthesizer;parseSoundFont(data, { missingProgram: 'synth' })(also onloadSoundFontandrenderMidiToBufferAsync) hands those instruments to the synthesizer instead of a piano line, and a bank missing the program or a drum kit still falls back to bank 0 or the standard kit, never to a melodic preset. It is the default bank. A game that wants another picks it fromdata/soundfont-catalog.jsonand fetches just its instruments withmwg-soundfonts(see tools.md). A SoundFont 3 stays compressed all the way to the page (FluidR3's piano, guitar and drums: 2.7 MB, against 20 MB decoded to a SoundFont 2), andloadSoundFontdecodes it at load, so prefer it over a SoundFont 2 for anything but the smallest banks.createAudioLoader/AudioLoaderOptions- a cached loader for embedded audio and MIDI, shared by allAudioBuslifetimes. MIDI is detected by header and rendered on a worker; other files usedecodeAudioData. Options select SoundFont/render settings or a byte source.AudioBususes this loader by default, soplayBgmandplaySeaccept asset paths with the same volume, pitch and pan fields. BGM/BGS loop; ME/SE play once.MusicPlayer/MusicPlayerOptions/MusicPlayOptions- one loadedAudioBufferorRenderedMidi, with play, pause, resume, seek, position, volume and anonEndedsignal. Callupdate()each frame to dispatch track completion and choose the next track.toAudioBuffer(context, rendered)copies rendered stereo samples into anAudioBuffer.Channel/AudioBus- buffered WebAudio music: aChannelplays one looping track with volume, pitch, pan, a loop region and a seekable playhead, and anAudioBuswires two of them into independent BGM/BGS loops, ME jingles that suspend and resume the BGM, overlapping SE one-shots, save and replay with playheads, and plain-data snapshots.AudioBus.setMixer(kind, { volume, muted })scales a whole kind on top of each track's own volume, without changing what snapshots report.MidiSynth/MidiSynthFactory/MidiSynthRequest+registerMidiSynth,unregisterMidiSynth,midiSynthNames,resolveMidiSynth,renderMidiThroughSynth,renderMidiStems,onMidiSynthFallbackandINTERNAL_MIDI_ENGINES- the named-engine contract for MIDI: a package registers a synth under a name (retro:mt32), content asks for it by name, and resolution walks the request, then the internal names (gm-soundfont,builtin), firingonMidiSynthFallbackwhenever another engine voices the file instead.timedMidiMessagesis the raw timed message stream a synth is driven by.StreamPlayable/StreamChannel- the streaming half of thePlayablecontract: seek andcurrentTimeon a synthesised stream,playbackRateby resampling, and a playhead counted in frames, so endless formats (NSF, SID) play without a whole-track render.TrackCache/createBudgetedLoader/BusBudgetPolicy+bufferBytes- bus render budgets: an LRU cache of decoded tracks under a megabyte and entry cap, withmaxSeconds, mono or half-rate rendering for ambience throughfitToBudgetandresampleStereo.DEFAULT_DECODER_LIMITS/decoderLimits/DecodeBudgetError/InstructionBudget+assertInputSize,guardBlockSourceandinflateBounded- the decoder guards for untrusted retro formats: input and inflate ceilings, an instruction budget per second of sound, and one named error per breach instead of a hang.SfxBank/SfxSound/SfxArbiter+sfxSeed- data sound effects: synthesised chip effects with a deterministic seed, arbitrated against thesebus by priority, firing captions like a file effect.AudioSettings/AudioSettingsData/AudioRegion/OutputStage+regionPlaybackRate,softenCutoffanddynamicsLimit- the player-facing audio settings in one plain-data object: volume per bus (applied to anAudioBusbyapplyToBus), the preferred MIDI engine name, PAL or NTSC region (a tune authored for one runs slower or faster on the other,regionPlaybackRate), a faithful or clean output stage, caption display, a dynamics control (a soft limiter,dynamicsLimit) and bright-sound softening (a falling low-pass cutoff,softenCutoff). A CPU and underrun indicator readsAudioCoreHost's stats andonAudioUnderrun.audioSettingsRowsgives the rows (soften, dynamics, captions, output stage, region) to pass asSettingsScreen'scustom, under theAUDIO_SETTINGS_KEYScustom keys, andAudioSettings.restoreFromCustomreads the answers back.AudioExport/AUDIO_FORMATS/ExportTooLargeError+estimateExportBytes,exportTagsanddownloadSaver- rendering to a file: the service decides which of WAV, FLAC and Opus this device offers (WAV is written here, FLAC and Opus come from injectedExportEncoders such as the retro-audio package'sexportAudio, Opus hidden whereopusAvailable()is false), refuses an export over its size limit before spending the time, merges default tags, honours anAbortSignal, and hands the finishedBlobto a saver (downloadSaver, a link with thedownloadattribute, unless the platform supplies its own).inWorkerruns self-contained encoders on athreads.spawnworker (main thread where none exists).mwg-audio convertwrites.flacand.opusthrough the same service with--encoder(a module, orffmpeg).webCodecsOpusEncoder/isOpusEncodingAvailable/oggOpus- Opus without a dependency: the encoder anAudioExporttakes foropusin a browser (48 kHz, 20 ms blocks into a WebCodecsAudioEncoder, the encoder's own pre-skip, tags), and the Ogg Opus writer under it (OpusHead,OpusTags, CRC-checked pages, a last granule trimmed to the exact length), checked against ffmpeg's decoder. Where there is noAudioEncoder(Node, old WebViews) the game injects another.OutputRecorder+AudioBus.record()- what the bus plays, every kind as the player hears it, recorded into plain stereo frames through a silent script processor (the capture node that exists everywhere,file://pages included) and bounded bymaxSeconds;stop()returns the take forAudioExport.benchEngine/EngineBenchmark- times an engine on up to 16 held voices and reports its share of real time;mwg-audio benchis the command line. The number belongs to the engine and machine it was measured on; over about half of real time,playMidiHostedis the way out.auditEngines/AuditFinding/AuditReport- renders calibration notes through named engines and flags a fallback, a failure, silence, clipping, a DC offset and a pitch that is no harmonic of the note;mwg-audio auditis the command line over it. Screening, not a verdict.playMidiHosted/sendMidiToHost/HostedMidiandreportUnderruns- a MIDI file played through anAudioCoreHostinto aStreamChannel(the file's messages queued on the host in one post), and a forwarder that turnsonAudioUnderruninto summed, rate-limitedaudio_underruntelemetry events through any sink withsend,TelemetryClientincluded.waveformEnvelope/channelEnvelopes/WaveformBucketandtrackerPattern/trackerNoteName/TrackerRow- the data cores of the audio views: a waveform as per-bucket min/max pairs per channel (what an oscilloscope draws), and a MIDI file as tracker rows, one column per channel on the file's own tick grid. A game draws them with its own widgets; the reduction and the grid live here and are tested as arithmetic.SyxBank/SyxPatch+splitSysexanddx7PatchNames- a.syxbank a player drops: its whole sysex messages, the patch names of every checksummed DX7 32-voice bulk dump in it (another maker's bank names nothing rather than guessing), and anauditionthat loads a patch into aMidiSynthand renders one note through it for a settings screen.readSyxandSyxBank.skippedsay what a tolerant read left out (SyxSkip: bytes before the first message, an unterminated tail, a DX7-shaped bank with a bad checksum, a message that is no DX7 bank) so a mixed or damaged file is used for what is whole and reported for the rest. Each patch has anidofsource#message.voice@fingerprint, the identity a save holds: a name alone is shared by many patches. A DX7 patch also carriesinfo(Dx7VoiceInfo: the algorithm, feedback, LFO wave and transposition, read bydx7VoiceInfofrom the packed bytes).audit(synth)renders one note of every patch (each selected by a program change after the bank loads) and returns the silent and clipping ones (SyxPatchFinding), so an author knows before publishing.connectWebMidi/requestWebMidiInput/WebMidiConnection- live Web MIDI into aMidiSynth: every input port of aMIDIAccessis wired to the synth (channel messages toevent, sysex tosysex, transport dropped), andrequestWebMidiInputis the browser entry that returns null where Web MIDI does not exist or was refused.AdaptiveMusic- adaptive tracks: mute and unmute stems of a running track with a fade, and switch between two versions at the same frame position;loopRegionFromFilereads the loop region a file declares as the default. A version loaded withbarSecondsacceptsquantize: 'bar'(Quantize) onsetLevelandswitchVersion, so a layer comes in or a scene changes at the next bar line instead of cutting the measure.AudioCoreHost+onAudioUnderrun- the off-main-thread audio host: a synthesis core in a worker behind the samePlayablefacade, with a main-thread fallback and an underrun signal feeding bounded telemetry. The core is given as a self-containedfactory, or asfactorySource(the text of an expression that evaluates to the factory) for a core with imports, bundled into one script by the core's own package.start(events)begins a piece in order (reset, messages, then rendering) andprefetch: falsekeeps a new host from rendering silence first: a modelled instrument that has rendered a second of silence is not a fresh one, and the piece would differ from a direct render. A reset also drops blocks still in flight from before it (each block carries the host's epoch).encodeWav/decodeWav/wavInfoandspectralDistance- WAVE as plain bytes (themwg-audiotool's convert and compare verbs over the same code), and a spectral distance between two renders for a listening check that can be automated.parseVorbisLoopTags/loopRegionFromTags- read theLOOPSTART/LOOPLENGTHcomments an Ogg Vorbis file carries (a bounded walk of the first pages, null on anything malformed) and turn them into the seconds-basedLoopRegionaChannelor the bus loader hands back.humanizeMidi/HumanizerFactory/HumanizeRequest/MidiHumanizerEngine- the entry point for a performance that sounds played rather than played back.humanizeMidi(file, options)resolves the tier from the request (aGrooveTemplatein it means the groove tier, anything else the lite one) and returns a new file;HumanizerFactory.create(tier)is for holding one engine across several files. The pass never mutates its input:intensity: 0reproduces the file exactly, aseedreproduces a run, note durations and every tempo, program, controller and meta event survive, events come back sorted by tick with ties in file order, and nothing lands before tick 0 (a shift that would cross it stops there instead).create('magenta')throws naming what is missing rather than loading:@magenta/musicand a PerformanceRNN checkpoint cannot be fetched from afile://build.LiteHumanizer/GrooveHumanizer/HumanizationOptions/HumanizeStyle/HumanizerTier- the two shipped engines and their options. Tier 1 moves timing and velocity by gaussian noise over a seeded gradient drift, scaled byintensityand bystyle(jazz wide, classical tight), and holds the kick and the lowest notes to a few milliseconds of their slot; tier 2 applies a template's per-slot offsets and velocities instead, converted through the file's own tempo map when the template was recorded at another tempo (which says so throughonWarnrather than staying quiet), plus contextual rules - an accent on a local melody peak, restraint on the beat.initialize()is idempotent on both.subdivisionCount/assertGrooveTemplate/GrooveTemplate/GridSubdivision/GrooveOffset- the units everything else is measured in: how many slots a bar of a signature holds at a grid (subdivisionCount([4, 4], '16n')is 16,[6, 8]at8nis 6, and a grid that does not tile the bar throws), and the validator for a template's invariants - dense offsets in subdivision order, whole ticks, a power-of-two denominator, a velocity factor between 0 and 1 - run when one is built and when one is imported.GrooveExtractor- a reusable template out of three kinds of evidence, each reduced to the same thing: the median of each slot's deviations, so one flubbed note cannot drag it and a slot nobody played stays on the grid. Mode A matches a played pass against a straight one, note-on to nearest note-on of the same channel and pitch within half a slot; mode B projects a single file onto its own implied grid, which is exactly the residual a quantization would discard; mode C takes anAudioBuffer, detecting attacks by spectral flux (hand-rolled FFT, no analysis dependency) and taking the tempo as an argument because timing means nothing without one, with velocity left at the neutral 1 since a recording carries none.GrooveConverter- a template to and from the two formats that can carry one. JSON is exact; the MIDI encoding is one note per slot behind an anchor a subdivision ahead, with velocity at 1/64 per unit ofvelocityFactor, so the writer enforces|offset|below the slot and a factor of 1/64 to 127/64 instead of rounding past them, and the file gets its own reader because the round trip needs the tempo, time-signature and name metas thatparseMidideliberately drops.GrooveFormatalso namesableton-agrandreaper-groove, both of which throw saying they are not shipped rather than returning a file that resembles one.
music
Generated game music, renderer-free and deterministic. import * as Music from '@datamoc/mwg/music'.
composeMusic(scene)- a scene (MusicScene:id,seed,mood,tension,key,bars,voices, optionaltempo) becomes aGeneratedMusic: a playableMidiFile, itsbarSecondsand its measuredpolyphony. The same scene gives the same notes, since the seeds are mixed the way the data sound effects' are (sfxSeed).voicesis kept by writing fewer layers (one voice is the melody alone, two add a bass, three a one-note arpeggio, four drums, more pad notes), so a piece for a 3-voice SID or YM never needs a note cut. The result plays throughrenderMidiToBuffer, a registered MIDI engine, orAdaptiveMusic.composeStates(base, states)- one piece per game state (exploration, combat, victory) with the same key, tempo and length in bars, soAdaptiveMusiccan switch between them on a bar line withbarSecondsandquantize: 'bar'.normalizeScene/MUSIC_MOODS/MusicMood- makes a scene safe from a save, a share link or a settings screen (clamps every number, unknown moods becomecalm), so a scene is plain data a game stores.MUSIC_GENERATOR_VERSIONchanges when the composer's output for a scene does: a save that keeps scenes keeps the version with them.maxPolyphony(file)- the most notes sounding at once in anyMidiFile.
battle
The Pokémon-shaped half of the capability spec: species/stats/party/type-matrix/turn-order. No move, formula, or number belongs here - only the shape.
Creature/Species- wrapsactors.StatBlock/Progression, not a separate stat system.TypeMatrix- attack-type effectiveness lookup.Party- a roster of creatures.battleOrder/Move/BattleAction- one round's priority-then-speed turn order.checkEvolution/EvolutionRule- level/condition-based species evolution.StatStages- a bounded, symmetric stage ladder over a stat (±max), replacing rather than stacking the modifier for a stat's current stage.chooseMove/chooseSwitch- battle AI built onTypeMatrix.BattleHooks/BattleHook-core.HookRegistrywith battle's argument shape: a handler receives the creature plus a shared mutablecontext, for passive effects, abilities and turn-skipping statuses.Field/FieldCondition- field-wide conditions: weather, terrain, screens; named, optionally timed, ticked down byadvance(rounds).AttackPreview/AttackPreviewOptions/AttackFrame/PreviewCombatant- the damage totals and one-frame-per-strike timeline an attack dialog prints and animates, from damage the game's own formula produced (totalDamage, chance-weightedexpectedDamage,hitsfor a rolled outcome,sampleAton the animation clock). It computes no damage itself.AttackDialog/AttackDialogOptions/StrikeNumbers- the dialog's whole renderer-free state: aUnitSelectorto choose the pair, theAttackPreviewbuilt from a game-supplieddamageFor, and theupdate(dt)/finishedclock that drives the animation.UnitSelector/UnitSelectorOptions/SelectableUnit/SelectorStage- the two-step attack selector: enabled units on the selecting side as candidates, then the enemiescanTargetallows as targets;backundoes the attacker choice.Whiteboard/WhiteboardEntry- planned turn orders with one plan per unit,undo/redo(a new plan clears the redo stack) andcommithanding the plans back.BattleStats/BattleStatCategory(item 273) - a per-run breakdown by category (recruits,recalls,advances,kills,deaths,damageDealt,damageTaken) and unit type, the shapecore.PlayerStats's opaqueTcannot give a game for free;recordadds to one unit type's count,total/breakdownread a category back summed or per type, andtoJSONfolds into acore.RunHistoryentry orcore.PlayerStatstotal like any other run summary.
board
Board/card/dice games and generic tactics - traditional public-domain rules, no formula borrowed from any licensed game.
chess (
chess.ts):startingChess/parseFen/positionKey/cloneChess/legalMoves/applyMove/inCheck/gameResult/sq/squareName- full rules: legality, check/mate/ stalemate, castling, en passant, promotion, FEN in both directions (positions keyed without clocks).engine (
Engine.ts):chessGame/chooseMove/search- the chess rules adapter and convenience wrapper over the sharedai.alphaBetaSearch, with material evaluation and depth/node limits.searchAsyncscores each root move on a worker thread and combines the scores first-wins, so a depth-3 search stops dropping frames; inside the node budget it chooses exactly the movesearchwould. The minigame path keeps this small search and never pays for the tournament module below.searchAsynccombines root-move scores first-wins.bitboards (
bitboard.ts): the same rules on twelve 64-bit boards (startingBitboard,bitboardFromChess,bitboardMoves,makeMove/unmakeMove,perft), the mutable board the tournament engine searches.bitboardFromChessbridges from achess.tsposition; the differential tests keep both rule sets in agreement.openings (
openings.ts):probeBooklooks a position up in a compiled opening book (ECO, name where a known line ends, UCI continuations),parseUciMovereads a UCI move back. The book is a game-side artifact built bytools/compile-openingsfrom vendored CC0 tables, so no table ships with the framework.tourney (
tourney.ts):searchTourney/searchTourneyAsyncover one self-containedtourneyThinktask, so the worker side cannot diverge from the main-thread side: negamax alpha-beta with principal variation search, null-move pruning, late-move reductions, iterative deepening with aspiration windows, a bounded Zobrist transposition table, quiescence over captures and promotions, hash/MVV-LVA/ killer/history ordering, and classical material plus in-house piece-square evaluation. Positions are plain bitboard data and structured-clone straight into workers.TourneyResult.rootScoresis populated by an async search with legal root moves and scores from the mover's view, in root order, for near-best selection without a second search. The chess example plays its reply throughsearchTourneyAsync, with a thinking indicator, a per-move time cap, side and depth choice, a promotion picker and an in-check highlight.tablebase (
tablebase.ts):probeTablebase/loadTablebaseEnding/tablebaseIdplus thehuffEncode/huffDecodecodec - WDL outcomes with distance-to-mate for pawnless white-extras-against-lone-king endings, fifty-move-adjudicated against an explicit halfmove clock, generated and verified bytools/build-tablebasesand shipped compiled underdata/tablebases(3-piece; 4-piece stays a tool run away, full 4-piece tables are tens of megabytes each).classics (
Classics.ts):BoardGrid; checkers (startingCheckers/checkersMoves/applyCheckersMove, forced multi-jump captures); go (startingGo/playGo/passGo/goResult/goScore, ko and area scoring); backgammon (startingBackgammon/rollBackgammonDice/backgammonMoves/applyBackgammonMove, bar and bearing off); cards (createDeck/shuffleDeck/deal/trickWinner); solitaire (dealSolitaire/drawSolitaire/moveSolitaireTableau/moveSolitaireToFoundation/solitaireWon, seeded Klondike); dice (rollDice/rollExpression/DiceCup/scoreDice, Yahtzee-shaped keep-and-reroll scoring).tactics (
Tactics.ts):startingTactics/addTacticalUnit/canPlaceTacticalUnit/tacticalMoves/moveTacticalUnit/setTacticalOverwatch/triggerTacticalOverwatch/tacticalAttack/endTacticalTurn- squad tactics: action points, cover, overwatch, and zone of control baked intotacticalMoves/move cost.FactionFog/VisionCell- fog of war as a per-faction union of every controlled unit's own vision, with explored memory retained.shareputs factions on one map, sight and memory alike, which is Wesnoth'sshare_vision; nothing shares until asked.seeshands a side's sight out as a predicate for anything that asks what it can see.army (
Army.ts):startingArmy/recruit/recall/bankUnit/armyIncome/applyUpkeep- recruiting/recalling units against a currency total, per-turn income and upkeep, no specific rate baked in.hex skirmish (
HexSkirmish.ts):startingSkirmish/setSkirmishTerrain/canPlaceSkirmishUnit/addSkirmishUnit/skirmishMoves/moveSkirmishUnit/skirmishAttack/skirmishIncome/endSkirmishTurn- a Wesnoth-style hex army-game rules layer distinct fromtacticsabove: per-terrain movement cost and defence, adjacency-only attacks where a surviving defender always strikes back, capturable villages that grant income and heal whoever owns them, no zone of control.BoardPiece(type, fromClassics.ts) - a generic owned/countable/capturable/promotable token distinct fromroguelike'sCreature-shaped actors; every game above builds on it.
actors
Stat blocks, equipment, inventory, and the numbers layer nearly every other module builds on. This is the shape - a game names its own attributes and formulas.
StatBlock/Modifier/DerivedStat- base attributes plus derived stats, combined through ordered modifiers (add → multiply → set).toJSONsaves base values only: modifiers are reapplied on load by whatever owns them (equipment, status effects, auras), so saving them too would double every bonus.composeModifiers- the same canonical modifier composition asStatBlock, available to adapters that resolve declarative effect lists without creating a stat block.Progression/powerCurve- levelling from experience against a growth curve;toJSONsaves level and experience, the curve being a definition.skillCheck- value-plus-modifiers against a difficulty, one dice roll.SkillPoints- a spendable ledger that raises a stat's base rank, with per-stat caps and a rising cost per rank.toJSONsaves only unspent points; the ranks bought are already in theStatBlock's own base values.EquipmentSlots/EquippableItem/SavedEquipment- equip/unequip applies or removes an item's modifiers immediately; an optionallockedpredicate refuses unequip/swap for cursed/quest gear.toJSON(identify)records slot-to-id, andfromJSONre-equips so every modifier lands back on theStatBlock; a locked item is restored still worn.Inventory/InventoryItem/ItemDefinition/SavedInventory- stacking (by id, and byinstanceIdwhen either item sets one), weight, capacity, containers holding their ownInventory.toJSONsaves per-item instance state (level, wear, affix, identified,instanceId) whilefromJSON(defs, data)takes kind-level fields from the game's item table, so a rebalanced weight reaches an old save.craft/Recipe/Ingredient- resolves a recipe against anInventory: all-or-nothing, with a capacity-rollback path if the result doesn't fit. An ingredient may match an exactid(or a list of them), acategoryon the item's definition ("any herb"), or amatchespredicate; the allocation is made against a working copy so two flexible ingredients cannot count one stack twice.InventoryItem.category/ItemDefinitioncarry the kind-level grouping (supplied by the game on load, likestackableandweight).applyStatusEffect/EffectClock/StatusEffectHandle- aStatBlockmodifier tied to aTurnClock's automatic expiry;cancel()for early removal.identify/enchant/damageItem/repairItem- identification; enchant/upgrade level (enchant'saffixPolicy: 'keep' | 'remove'decides whether an upgrade strips an affix); durability wear and repair, opt-in per item.applyItemStatusEffect- a status effect sourced from an item rather than a spell.Charges/ChargesOptions- a resource regenerating in banked-turn chunks viaadvance(turns);refund()restores charges directly (a kill or crit), outside that path.toJSONsaves the count and the progress banked towards the next, so reloading cannot shorten a recharge.rollLoot/LootEntry/LootTable- rolls whether anything drops, then weight-picks which, the same "roll whether, then which" shapeworld.rollEncountershares.Advancement- level-gated tiers granting ledger points, with mutually-exclusive branch/capstone choices.rollAffix/affixOf/applyAffix/removeAffix/copyAffix/matchesContext- named item affixes: weight-picked, applied/removed onInventoryItem.affix, copied between item instances, and matched against a fired trigger's attackkindviaAffixDef.kinds.scaledModifiers/LevelScale- level-scaled gear:{stat, op, base, perLevel}resolved to a plainModifier.assignAppearances/Appearances- per-run appearance shuffling for unidentified items, seeded and fixed for the run.buy/sell/Price/ShopOptions- a currency-StatBlocktransaction against anInventory, all-or-nothing with rollback.canAfford/spend/refund/convertToCharges/ResourceCost- a spendableStatBlockpool (mana, stamina): check, all-or-nothing spend, mirror-image refund (optionally fractional), or convert a spent amount intoChargesat a rate.Barrier/BarrierLayer- a layered shield/absorption pool; the most recently added layer drains first, with optional independent per-layer decay ticked byadvance(turns).assignTraits/TraitDef/AssignedTrait- per-instance random traits drawn from a shared pool at creation, applied as permanent modifiers.AuraField/AuraDef/AuraParticipant- continuously reapplies a carrier's modifiers to whoever is currently adjacent, diffing rather than reapplying every tick.SupportLedger- a pair-order-independent, thresholded bond/support relationship between two units, growing through proximity or shared battles;toJSON/fromJSONround-trip it.buildEntity/buildEntities/EntityTemplateRow/EntityTemplateCatalog/BuiltEntity- one row of a hero or monster file (typically acore.parseCSVresult) into a fully wired entity: aStatBlockof base stats (every non-reserved column), an optionalProgressionagainst a named growth curve, a starting item carrying a named starting affix, and aReactionTablealready holding a low-HP rule if the row names a threshold. What each named growth curve/affix/item actually is stays the game's ownEntityTemplateCatalog.toEntitySaveState/fromEntitySaveState/EntitySaveState- aBuiltEntity's mutable state (base stat values, level/experience, the carried item) in the same "definitions supplied fresh on load" shapeStatBlock/Progressionalready draw.fromEntitySaveStaterebuilds viabuildEntityfrom the same row and catalog, then overlays the saved state on top.
roguelike
FOV, pathfinding, the energy scheduler, level generation, and combat-adjacent primitives - the dungeon-crawl half of the capability spec.
Level/WALL/FLOOR/rectCenter/rectsOverlap- the tile grid: terrain kinds, passability/transparency, square or hexshape.generateDungeon/findFreeCell/furthestRoom/DUNGEON_KINDS- procedural dungeon generation.generateDungeonGraph- the same pipeline asgenerateDungeon, also returning the room graph (corridor edges, backbone then extra loops) and a rejected-placement retry count;DungeonOptions.hooksfiresonRoomPlaced/onCorridorCarvedas the pipeline runs, the seam a regional generator hangs hand-placed rooms, branches, wells, plants, statues, chasms, doors and traps from.generateCave(item 407) - the cave counterpart: chambers grown as random blobs (jaggedthe chance a growth step wanders), passages as biased random walks (windiness,width,chance), starts on chamber centres with cleared rings,villageDensityreturning picked clear cells as data, and a forced join so a cave is always connected whatever the rolls did. Seeded through theRandomstack like the dungeon.generateOverworld(item 410) - a whole realm as plain tile data: seeded elevation and moisture fields classify water, plains, forest, hills, mountains, desert and swamp behind aseaLevelknob; rivers run downhill to the sea, merging when they meet, drowning a lake at a ridge instead of crossing it; settlements place themselves by suitability (buildable ground, water within reach, spaced apart) and join into a road tree that prefers flat ground, water a ford rather than a wall; points of interest carry aleadsToid forOverworldlocations, and every cell carries a coarse region id for encounter tables. The realm invents itself from the seed: no reference game's map data.shape: 'hex'ongenerateDungeon,generateDungeonGraph,generateCaveandgenerateOverworldlays the same generator out on a hex grid (odd-q offsets, theLevelconvention): the dungeon and caveLevelcarry the hex shape, a dungeon corridor leg runs as a hex line, a cave grows its chambers by hex distance and walks passages toward their target in true on-screen distance, and the overworld samples its noise where each hex is drawn and steps rivers and roads across the six edges. The default stays square, byte for byte.generateCavealso takesrandom(aRandomSource, such as aGenerator) in place ofseed: it draws from, and advances, that stream, leaving the ambientRandomstack untouched.RoomBuilder/hallBuilder/eligibleBuilders/pickBuilder- composable room interiors: the generator places and joins rooms, a builder carves each one, picked by weight among those whose size constraints fit.DungeonOptions.builderswires them in; a room no builder fits falls back to a plain hall rather than staying solid rock.FeatureLayer- a floor feature/event layer: named kinds withinspect/interact/consequence/persistentrules attached to generated cells; definitions supplied fresh on load, only cell-to-kind pairs are save data.rollRoster- a variant-spawn/content-roll API: weighted regular entries, rare additions, per-entry alternative swaps, and a shuffle, in that order; every roll (including an explicitly disabled rare entry's non-roll) is traced for parity tests.candidateCells/cellsNear/selectDistinctCells- deterministic content placement over aLevel:candidateCellsfilters by terrain kind, occupancy and an optional region;cellsNearwalks outward by radius for a cluster anchor;selectDistinctCellsshuffles and slices a de-duplicated pool, bounded to however many candidates exist rather than throwing. Composes for neighbouring-item clusters, scattered decorations, and single room/branch rewards; nothing is placed until the caller commits, and the returned trace is plain JSON-safe data forDungeonArtifacts.contentor a save file.compareDungeonArtifacts/checkDeterminism- a dungeon parity/test harness: diffs a seeded run's room graph, retries, terrain, features and content rolls against a reference or golden fixture, tagging every mismatch'graph'or'paint';checkDeterminismrepeats a generation call and confirms every run matches the first.FieldOfView/HeightSight- shadowcast (square) or straight-line (hex) visibility, withElevation-aware asymmetric cliff sight.Elevation- whole-level height per cell, read byFieldOfView/Pathfinder.Pathfinder/neighbourOffsets- A*(square)/Dijkstra(hex or elevation-aware) pathing,distanceMap/descend/autoExplore, an optional climb limit.Scheduler/Actor/SchedulerSnapshot- energy-cost turn order, the sole authority on logical time; fractional costs,postpone(delaying an actor other than the current one), andtoJSON/restorefor a deterministic snapshot keyed by a caller-supplied actor id.decideMonsterAI/AIState/AIDecision/Disposition- wander/hunt/flee built onFieldOfView/Pathfinder/Scheduler; peaceful/neutral/hostile disposition, provoked override.Secrets- disguised terrain (secret doors, hidden traps) that passes every passable/transparent check as its disguise until revealed.Doors- open/closed/locked door state; locking is independent of open/closed.chebyshevDistance/traceLine/ballistica/hasLineOfSight/canTarget/resolveArea- targeting and collision paths: range/line-of-sight checks, configurable stopping cells, and single/line/burst/cone area resolution.TargetingController- the input-facing half of targeting: a cursor moved by keyboard (move) or by a caller-resolved pointer cell (moveTo), range and line-of-sight validation with an optional gamevalidatehook, apreviewof the shape's cells, and aconfirm/cancelresult. It returns cells only; the game keeps legality details, damage and drawing.coneCells/chainTargets/knockbackPath- a widening cone spray, a greedy nearest-hop chain, and a shove's path until the first impassable cell.coneSector/ConeSectorOptions- a circular sector aimed at an angle: rays cast every 0.5 degrees across an arc ofdegrees, each clamped torangeand stopped by the first wall, so a wall shadows what is behind it while the rest of the cone reaches. UnlikeconeCells, the aim is not snapped to one of the eight directions.resolveAreaOnLevel/hexConeCells- topology-aware area resolution and widening hex cones, complementing the square-grid targeting helpers.rangeMultiplier/areaFalloffMultiplier/RangeBand- a range-band damage multiplier lookup, and a per-target falloff multiplier for an area effect hitting several targets.BossPhases/AbilityCycle- an HP-fraction phase ladder whosecheck(hpFraction)reports newly entered phases, and a named-cooldown ability rotation (ready/use/advance).CombatHooks/CombatEvent/DamageContext-core.HookRegistrywith combat's argument shape (one mutableDamageContext), plusmodifyDamage's pre-damage seam.Stealth/StealthOptions- sticky, one-way detection:checkDetectionreturns true only on the call that first brings an observer within radius.TriggerTracker- a streak counter (combo, kill-streak) that extends within a turn window and restarts once the window lapses.MultiStageAbility/AbilityStage- an ability that unfolds through named, turn-timed stages (windup/active/recovery) once started.Minimap/minimapCellCenter- exploration minimap rendering and overlay coordinates for square and odd-q hex maps.MultiTurnBeam- a deterministic one-front-per-turn beam. The default shape is a straight line;frontsopts into a per-turn front resolver (cone, burst, fork, moving front),blockeris the explicit terrain/none/custom policy withisBlockedas an extra game rule,onCellsees every reached cell, and a save carries the shape name so a reload resumes the same one. Live target lookup, game-owned damage application, cancellation and save/restore as before.BeamBlocker/BeamStep/MultiTurnBeamSave- the blocker policy, one turn's reached cells and damage, and the serialisable front list with its shape identity.
rpg
Tiled map/event data, the map-event interpreter, switches/variables, and grid movement - the classic top-down RPG half.
automap/AutomapRule/AutomapTarget/AUTOMAP_EMPTY- Tiled-style automapping:input/outputpattern rules applied in order, each rule's matches collected before any write. Takes any grid exposinggetTile/setTile(aTileMapsatisfies it structurally), so automapping needs no renderer.GameState- switches and variables.activePage/conditionHolds/EventPage/MapEvent- last-matching-page resolution for a map event, re-checked rather than cached.EventRunner/EventCommand/DialoguePresenter/DialogueRequest/EventChoice- aStageScript-shaped command interpreter for map events. Dialogue goes through an injectedpresentfunction rather than a widget, so the interpreter is renderer-free and a game can drive it from a 3D scene, a DOM overlay, or a test asserting on script order.two-d/ui.messageBoxPresenteris the ready-made 2D implementation.DialogueRequest.portraitis an opaque, renderer-specific value (Texture2DformessageBoxPresenter), kept untyped here sorpgstays renderer-free.EventRunner.runStory/EventStoryScriptfollow a named- passage graph the same wayStageScript.runStory/StoryScriptdo - agotocommand, or a choice's owngoto, jumps to a different passage; a plainrun()refuses a straygotorather than silently ignoring it, the same guardStageScript.runapplies.EventRunnerOptions.onUnknownCommandhears every command the runner has no handler for (and agotoin a straightrun(), which then ends the list instead of throwing), so a game converting foreign event data can make every dropped command loud.importTwee/EventTwineStory- imports Twee-notation Twine stories intoEventStoryScript, a thin wrapper aroundcore.parseTwee(the same enginetwo-d/stage.importTweewraps forStageCommand/StoryScriptinstead) naming theEventStoryScriptreturn type, so a text-only RPG can import a branching Twee story without depending ontwo-dfor anything.GridMover/Direction4- tweened tile-to-tile movement plus a walk-cycle hook.GridMover.jumpByrepositions instantly, with no tween or walk animation, for a move-route "jump" step.MoveRouteRunner/MoveRoute/MoveRouteStep/MoveRouteOptions- drives anyGridMover(the player's or an NPC's) through a scripted route over successiveupdate(dt)calls: fixed/random/toward/away directional steps, a turn-in-place step, a jump step, and a wait step, withrepeatandskippablepolicies. Whether a step is passable is asked through the samecanMovecontractGridMoveritself expects of its own caller.FreeMover- continuous (non-grid) position and facing, for action-game movement.MovableSprite- what both movers actually require:x/y, and optionallyupdate/has/play. A static sprite, a 3D mesh wrapper or a plain test record all satisfy it.aabbOverlap/circleOverlap/circleAabbOverlap/resolveAabbAgainstTiles- 2D collision: shape overlap tests, and an axis-separated sweep against solid tiles.QuestLog/QuestStage/QuestDefinition/QuestMarker- quest stages advanced one at a time byadvanceStage, prerequisites,markerForderiving'offer'/'turnIn'/'none', and a tracked-quest pointer/location.questsFromRows/QuestStageRow- groups a flat table of stage rows (typically acore.parseCSVresult, one row per stage sharing aquestId) intoQuestDefinitions aQuestLogcandefine.decodeMarshal/encodeMarshal/RubySymbol- RubyMarshal4.8 binary serialization (the container format RPG Maker's own.rxdatasaves use).hashDefaultOf/withHashDefault- reads and attaches a Ruby hash's default value, whichMarshalcarries alongside the entries rather than as one of them.parseDialogueText/extractDialogueCatalog- the terse@id text/--alternation dialogue format targetingEventCommand, a thin wrapper aroundcore.parseDialogueLines(the same enginetwo-d/stage.parseDialogueTextwraps forStageCommandinstead), so a text-only RPG (coreplusrpg, no renderer at all) can author dialogue without depending ontwo-dfor anything.extractDialogueCatalogbuilds an identityi18ncatalog from the result, the same shape MWL's ownextractCatalogbuilds from_("...")-marked strings.
simulation
TrainingEnvironment adapts game-owned TrainingRules around the existing
SimulationRule, with no renderer, DOM, audio or animation loop. reset(seed)
returns a TrainingFrame with per-agent observations, rewards, terminated and
truncated. step(actions) takes one action index per agent (or null only for
an all-masked idle agent), invokes the same rule used during play and returns the
next frame. Rewards inspect a cloned pre-transition state, so mutable rules work.
The agent roster stays fixed within an episode. maxSteps is required and capped
at one million; reaching it truncates an unfinished episode, while rule completion
terminates it. Real-time games advance fixed simulation ticks inside their rules;
turn-based games advance turns. Neither waits for wall-clock time. A throwing
rule can mutate its state, as with runScenario; reset or restore before continuing.
snapshot()/restore() use TrainingSnapshot: versioned environment state, schema,
step limit, completion flags, seed and RNG state. TrainingCheckpoint<State, TrainerState>
combines that snapshot with the NeuralModel and game-owned optimizer/exploration
state. Persist every random stream the trainer uses in trainerState. Train with
the same observation schema and action ordering used by the playable game.
runRollouts(environment, model, RolloutOptions) chooses actions from each new
observation until completion. Options provide episode seeds, optional seeded
softmax sampling and trajectoryLimit (first transitions per episode, zero by
default). RolloutEpisode reports steps, cumulative per-agent rewards, completion
flags and a bounded array of TrainingTransitions. No command journal or presentation
events are retained. Batches accept at most 10000 episodes and 10000 stored transitions.
runRolloutsAsync(TrainingFactory, config, model, options) shards episodes across
workers (default four, configurable jobs in 1..64). Each worker keeps its model,
environment and inference buffers for the whole shard. Results stay in seed order;
signal and per-worker timeout terminate work, and any failure terminates sibling
workers. The factory must be self-contained, like threads.spawn, accepting the
supplied TrainingEnvironment constructor and structured-cloneable configuration;
it cannot close over module imports. Both paths run the same classes and inference
code, including in a compiled file:// page. Training algorithms and rewards are
game-owned; tools/train-neural.ts shows a seeded evolution optimizer, resumable
checkpoint and model export, and examples/neural loads its trained weights.
imitationFromReplay(environment, { actions, events, seed }) turns a persisted run
(roadmap 397) into imitation targets: it walks the recorded actions against the
same seeded environment they were played on and returns the (observation, action)
samples a game's own trainer fits, sourced from how its players played rather than a
generated expert. One recorded event is one environment step; the environment must
expose exactly one agent (the recorded player - anything else that moves is part of
the rule, like the maze chase's ghosts); a recorded action outside the list is a
named error, never a silently skipped event.
runSeededEpisode(environment, choose, { seed }) plays one whole run with the AI
choosing, from a named seed: the same seed and the same deterministic choose make
the same decisions, so runs compare across builds, policies and machines. Unlike
runRollouts the chooser is not a NeuralModel - a heuristic, a NeuralPolicy's
argmax, or any named function works - and the result records every chosen action and
the cumulative rewards, with the environment's maxSteps as the truncation bound.
Headless, deterministic turn/scenario running - for testing and benchmarking without a live game loop.
advanceToInput/ScheduledTurns/TurnRules/TurnResult- drives turns forward under a rule set until the next input is due.runScenario/Scenario/ScenarioResult- a bounded, scripted simulation run against a set of expectations.Campaign/CampaignLevel/CampaignLevelResult/CampaignSnapshot- generic level order, result, carry-over, reminder and transition state, with JSON-safe save/restore.runHeadlessScenario/HeadlessScenario/HeadlessScenarioResult- seeded, renderer-free scenario execution returning the final state, ordered events and resumable random state.SimulationRuntime/SimulationContext/SimulationOutcome/SimulationRuntimeRule/SimulationSnapshot- a facade over one state + oneroguelike.Scheduler+ onecore. Generator, for the interactive half of a turn-based simulation:dispatchruns a single command through the game's rule, threadingrandom/scheduleras context and charging any returned cost;snapshot/restorecapture and rebuild the whole triple. Commands, events and (withhistory) state are journalled throughstructuredClone, so they must be plain data: name an actor by id, never pass the live object. Adispatchthat cannot copy them throws with the offending path and commits nothing. Composes withadvanceToInputagainst the same scheduler for the automatic-actor loop, rather than replacing it.EventPresentation/EventPresentationOptions- the documented pattern tying aSimulationRuntimeto acore.PresentationQueue:submitcommits a command and starts its events,lockedis the animation locksubmitrefuses under,followUpschedules a secondary actor's commands for after the current batch,canceldrops the rest of the show without rolling the committed turn back, andsnapshot/restoredeliberately leave the queue out of a save so a load resumes idle.Scheduler/Actor/SchedulerSnapshot- re-exported here fromroguelike(its real home), since everySimulationRuntimeneeds one; a simulation-first game need not import a second module just to construct the class its own runtime is built around.CampaignSave/CampaignSaveState/CampaignSaveParts- onecore.SaveSystemslot for a whole campaign:savecaptures theCampaignsnapshot, the game's own world payload and theSimulationRuntimesnapshot together,loadreads them back migrated, andlist/deleteareSaveSystem's. The world is opaque, so anMwlWorld, aLevelor a game's own scenario object all fit.
skeleton
Skeletal pose math, renderer-free: a bone tree, keyframed clips and layered blending, as
pure data and arithmetic so it loads headless and tests without a renderer. Clips are plain
JSON-shaped data a game authors itself. import * as Skeleton from '@datamoc/mwg/skeleton'.
create(bones)- builds aSkeletonBonesfromBones (name,parent,length,origin,restrotationin degrees, optionallimits), throwing by name on a duplicate bone, a cycle, a missing parent or inverted joint limits.boneOrderlists names parents first.resolve(skeleton, pose?)- walks the tree and returns oneBoneTransformper bone (worldx/y,angle, composed scale, a 2Dmatrix, and thetipX/tipYthe children start from). APoseis one rotation per bone name; aTransformPoseadds per-bone translations and scales.samplePose/sampleTransformPose(clip, time, skeleton?)- aClip(duration,loop, ascendingKeyframes) interpolated at a time: a looping clip wraps, a one-shot holds its last frame. Passing the skeleton validates the result.advancePlayback/createMixer/crossfadeTo/advance/advancePose- aMixerholds a basePlaybackplus weighted, prioritised additiveLayers.advance(mixer, dt)ticks every playhead, folds in a running crossfade and the layers, and returns rotations;advancePosereturns the fullTransformPose.mixPosesblends rotation poses by layer priority and weight.validatePose- refuses non-finite rotations and angles outside a bone'slimits.solveTwoBoneIK(skeleton, first, second, target, bend?, pose?)- solves a connected two-bone chain toward a world target without stretching; throws for an unreachable target or a second bone with a non-zero origin.
physics
Small deterministic simulations, renderer-free, with no third-party engine. Three entry
points: @datamoc/mwg/physics (rigid bodies, cloth, spring) and
@datamoc/mwg/physics/xpbd (a structure solver). Everything is plain arithmetic, so a
game's headless tests run the same world.
PhysicsWorld- rigid circles and axis-aligned boxes (never rotating:angleis a display offset), stepped on aFixedStepperso results do not depend on the display refresh rate. Semi-implicit Euler, impulse contacts with restitution and friction, and distance joints.addBox/addCirclereturn a plainPhysicsBody(position,velocity,angle,label,isStatic);connectjoins two bodies or a body and a world point (ConstraintOptions:stiffness,damping,length, attachment points, andmaxStretchto make the joint breakable);removetakes a body or constraint;stretchOfreads a joint's strain;update(dt)runs the fixed steps it covers and returns their count.onContactandonBreakareSignals.XpbdWorld(physics/xpbd) - point masses (addNode,fixedanchors) joined by springs (addLink:stiffness,rest,tensionOnlycables,strength/tensionStrength), solved with extended position-based dynamics and the force in each link read back, so a link breaks at a real load. Fixed steps and no randomness make a replay identical on every machine.step()advances one frame;applyForcepushes a node for one step;groundis a height or a function of x;breakinggates breaking so a structure can settle first;onBreakfires per broken link andpeakRatiois a strain gauge. It has no rigid bodies or collisions, which is whatPhysicsWorldis for.Cloth- a pinned-point Verlet grid (top row pinned) with structural links only, no allocation per step.indexOf/pointPosition/pointLinks/writePositionsread it back;pinPoint/unpinPoint,setWind/setGravity,resetandupdate(dt)drive it.two-d'sClothSpritedraws one as a mesh on the GPU path.Spring- a damped chase spring for secondary motion (hair, hems, panels):setAnchormoves the target,displaceshoves the bob,offset/length/settled(tolerance)read the lag,place(target)positions a display object,update(dt)steps it. Under reduced motion it followsSpringOptions.intent(decorativesnaps to the anchor).
testing
Test doubles for the seams mwg takes by injection (item 388), so a game's tests and this repository's own share one copy of each. Renderer-free, dependency-free, and off the root barrel: nothing here reaches a shipped game unless a test imports it.
createSceneControls(item 408) - named steps a game opts into (enterScenario,finishTurn,advanceDialoguein the Wesnoth port), driven by name from a test or a headless Chrome: the game decides what a step does, the controls only promise that a registered name runs and an unknown one is refused by name.memoryStorage- aSaveStorageheld in memory, forSaveSystem,Settings,Collection,PlayerStatsand the other stored values; the same classcorefalls back to wherelocalStorageis missing.exposeForTests(name, object)- publishes inspectable game state underwindow.__mwg_test__[name]for browser tests; does nothing outside a browser.fakeAudio/fakeAudioWithEnded/FakePlayable- aPlayableforSound/Music'screatethat counts its plays; the second also has theonendedslot a backend reporting track ends has.FakeAudioContext/FakeBuffer/FakeBufferSource/FakeGain/FakeBlockSource- a fake WebAudio graph for the audio tests: sources, gains and buffers a test can inspect, and a block source for aStreamChannel, with no realAudioContextanywhere.FakeRetroPlayer- aMidiSynthdouble for a retro engine: it records every message it was driven with and renders a flat level while a note is on, so a renderer test reads what was sent and what sounded, withFakeAudioContext.advance()as the clock it moves by hand.FakeSocket- aWebSocketLikeforLockstepClient'screate:sent,receive(message),receiveRaw(text),close().fakeFetch/FakeFetchCall- afetchfor everyHttpTransportclient, answering with aResponseor a value sent as JSON, recording each call.fakeGamepad- aGamepadfrom button values (pressed above 0.5) and axes, forInputandPlayerInput.
threads
Runs a self-contained function on a worker thread, so a blocking computation costs a
background thread instead of a dropped frame. mwg's own runtime rather than a dependency:
a classic blob worker in the browser, because that is the one shape a double-clicked
file:// page accepts (the shipped CSP allows worker-src blob: and new Function, and
a module worker loaded from file:// fails CORS whatever the policy says), and
node:worker_threads in Node. Off the root barrel like testing: a game that never
spawns a worker pays nothing for it, and imports @datamoc/mwg/threads when it
wants one.
openWorker(source, events)- opens a long-lived worker from a self-contained script (blob worker in a browser,node:worker_threadsin Node, null where neither exists) and returns its message link;spawnis the one-task use of it, a service such as the audio core host keeps its worker and talks to it.spawn(fn, args?, options?)- runsfnon a worker and resolves with its value. The task is serialized withFunction.prototype.toString, so it must be self-contained: no closures over module scope, no imports, no bound or native functions - everything it needs arrives asargs, structured-cloned in.options.transfermoves listed buffers instead, detaching them from the caller's thread. The result returns structured-cloned with everyArrayBufferinside it transferred rather than copied, since the worker is finished with them. A task that throws, closes over something the worker cannot see, or kills its worker without answering rejects the promise instead of hanging it;options.timeoutabandons a task that never returns, andoptions.signal(anAbortSignal) cancels one on demand. One worker per task, terminated the moment it settles. In Node an unsettled task keeps the process alive, so settle or cancel every task before exit.
headless
The blessed renderer-free entry: every module that loads in bare Node, in one import, with no DOM or WebGL globals. A rule imported through here is headless by construction; anything outside it belongs in presentation, not in the rule.
core/simulation/mwl- scenes, saves, RNG, headless scenarios and content tables.i18n/actors/world/battle- messages, stats, maps, encounters and type matchups.roguelike/board/audio/rpg- dungeon generation, board rules, sound/music primitives (injectcreate, as under plainaudio) and map events.ai/testing- decision runners and the shared test doubles.assetPaths/assetBinary- the two renderer-free asset halves (path resolution and byte caching) beside the Pixi-backedassetsloader.
Excluded with reasons in src/headless/index.ts: three-d (needs Babylon at import),
the Lua runtimes (mwl/pyroeis, ai/lua), and every two-d/* module.
three-d (optional)
Babylon.js-backed 3D, kept fully separate from the 2D default - nothing in core imports
this module.
Engine3D- owns Babylon's engine, scene, orbit camera, light, resize, and render loop.gridPoint3D/GridCell3D/GridShape3D- square/hex 3D grid cell projection.createTileGrid3D- a thin-instanced 3D tile floor (square or hex) with elevation columns for nonzero cell heights.Character3D- moves an imported mesh or a camera-facing billboard sprite through continuous world coordinates;playAnimationplays a clip an imported model carries.parseVox/createVoxModel3D- MagicaVoxel.voximport, batched by colour.loadModel3D/loadModelContainer3D/isModelContainerLoaded/releaseModelContainer(@datamoc/mwg/3d/models, kept out of the main3dbarrel so a scene that never imports a model pays nothing for the loader) - glTF/GLB import.loadModel3Dimports straight into the scene, for one copy;loadModelContainer3Dfetches and parses a source once, caches theAssetContainer, and a game callsinstantiateModelsToScene()on it for each independent, unaliased copy it wants placed - Babylon's own answer to "load once, place many" without a second network fetch.createHeightmapTerrain3D- a continuous displaced-mesh terrain from a greyscale heightmap image, alongsidecreateTileGrid3D's stepped floors.buildHeightIndex/cellAt/heightAt/resolveCapsuleAgainstGrid- horizontal collision for a moving capsule againstcreateTileGrid3D's stepped grid.
world
Many maps, transitions, persistence, the turn clock, encounter tables - the connective tissue between scenes.
World- many maps, each created once and kept alive (or rebuilt fresh per entry when definedpersistent: false);unloadfrees a map's own state.Overworld/Location- named-location lookup.TurnClock/TimedEffect- hunger/poison-style timed effects, ticked onadvance.rollEncounter/EncounterTable- rolls whether an encounter fires, then weight-picks which.EnvironmentClock/DayPhase/EnvironmentSnapshot- a continuous day/night-phase and weather clock above any one encounter, driven byupdate(dt)in real seconds, with change notifications andtoJSON/fromJSON.SideTurns/SideTurn/TimeOfDay/TimeArea/Alignment/alignmentBonus- the reference strategy game's outer turn model: sides take turns in order, a round is all of them, and the time-of-day schedule steps on per round.lawfulBonusAtapplies a unit's alignment to the current time'slawfulBonus, and aTimeAreaoverrides the schedule for part of the map, which is what makes the bonus per-hex.toJSON/fromJSONresume the exact position.
i18n
Message tables, plurals, interpolation, and direction - pure logic, no Pixi dependency.
Direction/Catalog/MessageValue- the message-table shape: locale, direction, messages, an optional Fluent-parsed or plain-string/plural-form value.setBase/setActive/locale/direction- the active and fallback language, and the directionui'sTheme.directionreads from;resetclears both back to unset.t/tRaw/formatSpec/tokenizeMessage/diffPlaceholders/MessageParams- resolves a key, selecting a plural form viaIntl.PluralRulesand interpolating{token}s, including dotted paths that walk into nested params ({side.stored.hitpoints}against{ side: { stored: { hitpoints: 12 } } }); falls back to the base language, then to the raw key. Placeholders also take Python f-string-style specs ({dmg:03d},{hp:.1%},{name:>12}),!s/!r/!aconversions and=debugging, resolved throughformatSpec(verified case by case against CPython; an ill-fitting spec stays untouched rather than throwing).tokenizeMessageis the single definition of that placeholder grammar, shared by interpolation and bydiffPlaceholders, which compares a reference string against its translation (dropped, added, or reshaped tokens) fortools/i18n-editto surface.tRawskips the display-text decoration (typographic spacing, RTL wrapping) for catalog values that are not shown to the player, such as a sound path.parseSoundMarkers/stripSoundMarkers/InlineSoundCue/ParsedSoundText- extracts{sound:path}markers from translated text, returning marker-free display text and each cue's visible-character offset.MessageBoxconsumes these markers during its reveal;t()andtRaw()preserve them for the presentation layer.typographic- locale-aware curly-apostrophe substitution (French/Italian/Dutch elisions); for French, also a narrow no-break space before; ! ? :and around« », and a plain no-break space between a numbering word and its number ("Chapitre 3"); for German, a no-break space between a number and its unit ("5 kg") and after a small set of abbreviations ("Nr. 3", "Dr. Müller").nonBreakingUnit- joins a value and a game-defined unit ("pièces d'or", "points de vie") with whichever no-break space French/German typography calls for; the unit's own text is always the game's vocabulary, never MWG's.has- whether a key resolves to something real, in either language.parseFTL/FluentOptions- parses Project Fluent.ftlmessage resources into the sameCatalog/t()surface, with variables, exact and plural variants.parsePo/PoOptions- parses a gettext.pofile into the sameCatalog/t()surface:msgid/msgstr,msgctxt,msgid_pluralwithmsgstr[N]mapped onto the locale's CLDR categories, the header entry dropped, untranslated entries left out for base fallback - which includes a#, fuzzyone, the way gettext's own tools treat it - and a non-default gettextdomainprefixing keys.SemanticMessage/MessageChannel/MessageFormatter/createCatalogFormatter- a typed{ type, params }communication intent rendered differently per channel (log/compact/ accessibility/debug/audio) from the same underlying catalog:createCatalogFormatterlooks up${type}.${channel}, falling back to${type}alone, then formats throught(). A simulation emits the message once; the channel is a presentation choice, not a rule. Theaudiochannel holds a sound path (played game-side throughaudio'sSound), resolves throughtRawrather thant(), returns''when no<type>.audioentry exists, and never falls back to a bare type holding a sentence.EntityTextResolver/GrammaticalEntity- type contracts a game uses while building aSemanticMessage's params (resolving an id to display text, or carrying gender/number/ proper-noun metadata for its own catalog's grammar); MWG never calls or interprets either.formatNumber/formatDate/formatList- locale-awareIntl.NumberFormat/DateTimeFormat/ListFormatwrappers reading the active locale fromlocale(), for a message that needs more than{token}substitution - a game calls these while building at()/SemanticMessageparam, the same way it resolves an entity id to display text.diffCatalogKeys/validateCatalog/CatalogKeyDiff/CatalogIssue- catalog consistency checks:diffCatalogKeyscompares two catalogs' key sets (a base language against a translation);validateCatalogflags empty messages and plural forms missing theirotherbranch within one catalog. Deliberately not a parameter schema validator - aSemanticMessage<TType, TParams>'s own generics already give a game compile-time parameter safety, which is the primary mechanism the source document calls for.validateMessageAudio/AudioIssue- structural checks over<type>.audiosound-cue entries: empty paths and non-string (plural/select) values, which would reduce silently at lookup time rather than selecting per play.EditSession/EditRow/RowFilter/AUDIO_SUFFIX/createEditSession/sessionKeys/sessionRow/sessionRows/setTargetText/copyFromBase/deleteTargetKey/setTargetSound/cueKeyFor/sessionCompleteness/swapSession/isAudioKey- the pure session core behindtools/i18n-edit: translatable rows with reference/target text and an effective cue (a channel key shares its semantic family's<type>.audio, one cue per family), completeness ignoring cue keys, and swapping which catalog is the reference.tools/i18n-edit.mjsis the terminal split-screen editor over it, with a--checkmode for CI.messageText/levenshteinDistance/findSimilarMessages- content-management over a catalog's actual message text rather than only its key shape:messageTextreduces anyMessageValueto plain comparable text,levenshteinDistanceis the classic edit-distance primitive, andfindSimilarMessagessurfaces near-duplicate messages worth merging by similarity threshold, most-similar first.catalogUsage/catalogCompleteness/pluralFormCoverage- catalog statistics:catalogUsagecompares a catalog's keys against a caller-supplied referenced-keys list (mwg has no view into a game's own source) for used/unused counts;catalogCompletenessis a 0-1 translated fraction built ondiffCatalogKeys;pluralFormCoveragetallies which CLDR plural categories a catalog's plural-form messages actually define.mergeCatalogKeys- drops a merged key from one catalog, keeping the surviving key's text untouched; rewriting call sites that used the merged key stays the caller's own job, the same boundarycatalogUsage'sreferencedKeysalready draws.
mwl
MWL is the build-time, game-neutral content layer. It uses a WML-inspired syntax for data
such as units, items, maps, events, AI hooks, translations, and asset references. The runtime
consumes generated data and does not parse .mwl source files in the browser.
mwl build- reads one file or a directory of.mwlfiles in stable path order and emitsgame-data.ts,i18n.json, andassets.jsonin one step.mwl build --asset-root public/assets- optionally verifies every extracted asset before emitting the generated files. Missing paths include their MWL source location; without this flag the build stays portable for games that resolve assets elsewhere.contentReport/loadContent/mwl report- counts tags, lists opaque names, reports resources, dependencies and unresolved references, and returns structured load diagnostics for parity checks and CI.validateCatalog/validateCatalogNodes/MwlValidationOptions- semantic checks shared by every game:MWL_DUPLICATE_ID,MWL_UNKNOWN_SLOT,MWL_INCOMPLETE_EFFECT,MWL_INVALID_HOOK/MWL_UNKNOWN_HOOK.MWL_DUPLICATE_ID'stag:idkey is scoped byrowIdScope(item 304):'global'(the default) checks the whole compiled catalog, every source file combined;'file'folds each node's own source file into the key too, for a game whose id convention is meaningful only within one file and reuses ids across files on purpose- a real duplicate within one file still reports.
[campaign]- declares game-owned campaign metadata (id, optionalname,title,description, andstart_scene). Its children are intentionally open so a game can define scenario and progression tags without changing the core MWL schema; metadata is exposed bycontentCatalog(game).campaigns.campaignChain- points a[campaign]atsimulation.Campaign:first_scenarioopens the chain, each[scenario]'snext_scenariois where it goes when it is won, and a scenario that ends the chain ends the campaign. The order is content, the playing stays the game's (runis a callback), and a scenario that decides for itself - a runtime[endlevel]- keeps its decision. The[campaign]schema readsfirst_scenario, and the catalog exposes each campaign'sscenarios.endLevelCarryover/carryoverIntoScenario/MWL_DEFAULT_CARRYOVER_PERCENTAGEwith[endlevel]- a scenario's end as the reference models it: a share of the side's gold (80% by default), a bonus on top, that side's surviving units as the recall list, and the next scenario.result=victoryends it won,result=defeatlost, and the arithmetic is the same for both.One side identity (item 277): the
ida[side]declares (now astring, so a named side likeid=rebelsvalidates) keysworld.sidesandworld.gold, marks a map keep (rebels Kh), and is whatunit.side,MwlMessage.sideand everyside/side_filterfilter hold.MwlSideRefis just that id.nodesToObjects/MwlObject- converts compiled nodes to JSON objects using the tag schema: declared numbers and booleans become typed fields, child tags become ordered arrays, and acardinality.maxof 1 selects a single object. Missing optional fields stay absent. Check the returned diagnostics for conversion errors and field collisions. Attributes not listed inMwlTagSchema.requiredare optional; usenumber, notnumber?.readAttributes/readChildren- shared typed readers for compiled nodes. They coerce scalars and lists and return source-located diagnostics instead of silently guessing.MwlNode.attributeLocations/valueLocations- parsed JSON5 nodes retain the exact key and value start for each attribute, and schema diagnostics use those locations when available.MwlValidationOptions.mapBounds- an adapter can supply map dimensions sovalidateCatalogrejects out-of-bounds integer, list, and range coordinates without embedding a map system in MWG.readTableMap/readTableIndex/tableKey- project typed table rows into aMap, use collision-free native composite keys, and build inverse indexes that retain every matching row.A
key="""..."""value (item 317) is the one attribute form that may span lines, and its text is exactly what sits between the delimiters, so wrapped message, objective and option-label text survives a conversion without the whitespace-collapsing workaround it otherwise needs. It carries the_gettext marker like any other value;text="""done"""is the same form on one line. (A line starting with#is still a comment wherever it appears, so a block cannot carry one at the start of a line.)MWL tables use
[table] columns=name:type|...with typed[row]attributes. The compiler validates column shape and values, andcontentCatalog(game).tablesexposes typed rows. Supported column types matchcore.parseCSV:string,number,boolean,list, andmap, with configurable list and map delimiters.typedRows/MwlTypedRow/MwlColumnValueTyperead rows through the declared columns (every field optional, an absent cell omits the key), andemitTableTypesemits one named row interface plus one typed accessor per table for a game's build script to write and import, so a changed column type or a removed column fails type-checking at every use.emitTableTypes(tables, { literalColumns: ['id', 'sprite'] })also exports sorted, deduplicated readonly value lists and literal unions such asVehiclesSpritefrom the current string-column values. Regenerate them with the catalog to catch stale game ids.MwlValidationOptions.tableReferences/MwlTableReference(item 362) - declarative cross-table checks, so no game hand-rolls one validator per table: a column declaresreferences: { table, column }oroneOf: [...], andvalidateCatalogchecks every cell once for all tables, reportingMWL_TABLE_REFERENCEwith the table, the row, and the offending value. The declarations' contents stay game data; absent or empty cells are skipped and alistcell checks each entry under the table's own delimiter.composeEffects- resolves MWL effects throughactors.composeModifiers, so declarative item effects and actor stats use one composition rule.evaluateCondition- evaluates bounded content conditions with comparisons,and/or/not, arithmetic, literals,wherebindings, and explicitly supplied pure helpers. There are no loops, assignments, dynamic code loading, or implicit game rules.mwl validate- checks syntax and content structure without generating runtime files.mwl diagnostics/mwl ci- emit stable JSON diagnostics, withciadditionally exposing a booleanvalidresult and returning a failing process status when errors exist.mwl format/mwl migrate --to 1.0- expand and emit a deterministic canonical MWL document; migration requires an explicit target and currently normalizes the MWL 1.0 syntax surface.mwl compile/mwl extract-i18n/mwl hooks- lower-level commands for a generated module, translation extraction, or hook manifest when a build pipeline needs separate outputs.parse/compile/validate/extractI18n- the programmatic compiler surface exported from@datamoc/mwg/mwl; games provide their own hook implementations and interpret game-specific effects.The
[side]surface Wesnoth's own data writes, and the one key of it with behaviour here (item 252):team_name,user_team_name,share_vision,village_gold,heal,fog,shroud,hiddenandflagare read ontoMwlWorld.sidesas written,yes/nobecoming booleans.sideVisionGroupsturnsteam_nameandshare_visioninto the groupsFactionFog.sharetakes, so a scenario's teams see together without every game re-deriving who shares with whom.leader=names either a[unit]id already declared (that unit is the side's leader; nothing is spawned) or a unit type (one leader per free start cell, with that type's stats); a value that is neither is refused by name (item 405).A
[side]'s own[victory]/[defeat]children (item 283): a side condition that names noside/side_filterreads as that side's own, its result lands inMwlWorld.sideStatuskeyed by the side id, and the aggregateMwlWorld.statusends the scenario as a scenario-wide condition would (a side victory outranking another side's defeat when both fire at once).[win]/[lose]record the side they name too.Public MWL exports -
MwlRuntime,MwlSyntaxError,collectHookReferences,compileNodes,compileSources,compileAndEmitSources,emitArtifacts,coerceTableValue,decodeSaveEnvelope,parseTableColumns,contentCatalog,createWorld,decodeSave,effectToModifier,emitHooksDeclaration,emitModule,emitTableTypes,encodeSave,evaluateExpression,execute,extractCatalog,hookTypes,inventoryItem,isGettext,isMwlId,itemDefinition,PACKAGE_TAGS,PACKAGE_ASSET_KINDS,PACKAGE_MEDIA_KINDS,PACKAGE_SCRIPT_ACTIONS,KNOWN_PACKAGE_CAPABILITIES,DEFAULT_PACKAGE_LIMITS,PackageError,PackageFormatError,PackageIntegrityError,PackageLimitError,PackagePathError,PackageConflictError,encodePackage,decodePackage,verifyPackage,loadPackage,mergePackage,compilePackages,packageContent,packageRef,parsePackageRef,parsePackageDocument,checkPackagePath,checkPackagePolicy,checkConditionSyntax,compareVersions,satisfiesVersionRange,requestedCapabilities,detectRomImage,registerRomDigest,generateCreditsandsha256Hex(self-contained.mwpcontent packages: one compressed container with per-entry digests and declared limits, a capability policy the game accepts or refuses,id@versionnamespaces in saves, and a ROM-image refusal with generated credits;verifyPackage'ssharedoption demands a licence on every synth-patch bank, and itsinspectPatchhook lets an instrument's own parser report what only it knows, its warnings (an unrendered wave) never refusing the package and its errors refusing it),parseExpression,parseHookReference,parseMapFile,parseTerrain,parseValue,preprocess,schema01,typedRows,validateCatalogNodes,validateHookReferences,loadContent, andvalidateWorld.parseMapFile/MwlMapFile(item 284) - reads a Wesnoth-shaped.map: thekey=valueheader is split off and kept, and the comma-separated rows go through the sameparseTerrainan inline[map] terrain=uses, overlays (Gg^Vh) and<side> <code>starts included. Header keys are not interpreted here, the same way[terrain_graphics]rules stay content's business.MwlHookDeclaration/validateHookAttributes(item 321) - a hook may declare the attributes its own[hook]calls carry (attributes, typed the way a tag schema types its own, oropenAttributes), andvalidateCatalog'shooksoption takes it in place of a baretype:nameid, so a typo in one of a command hook's eighteen modes is a compile-timeMWL_VALUErather than a throw mid-scenario. The declaration is the hook's, not the tag's: only[hook]calls are checked, where every attribute butnamebelongs to the hook. The rule itself isvalidAttributeValue/attributeTypeDescription, exported because the tag schema and a hook declaration check values the same way rather than two ways that drift.MwlDomainHook/MwlDomainHookRegistry- game-declared hook namespaces such asitem-effect:potion-strengthuse a typed context/input/output boundary. MWG validates and emits the reference shape, while the game owns the domain types, rules, and implementation.HookWorld/Emit(item 319) - the hook boundary's two halves.world.variableAt(path)resolves a nested variable through the same walker[variable] name=reads through, andemit.setVariable(path, value)is its write, so a hook asks forstored_naga.hitpointsand means the same variable content does; writingworld.variablesdirectly gets a flat dotted key no reader finds.HookWorld.random/MwlRuntimeOptions.random- a hook draws from the runtime's own seededcore.Generatorinstead ofMath.random(), so agenerator/ai/domain hook stays reproducible; the draw position is carried onMwlWorld.randomand resumes exactly across save/restore.MwlTraceEvent/MwlRuntimeOptions.onTrace- opt-in lifecycle tracing for event claims, completions, variable writes and runtime errors.MwlHookRegistry.predicatelets an adapter register named, typed filter predicates without putting game-specific semantics in MWG.MwlSaveableHookState/MwlHookRegistry.saveable- a game may explicitly expose JSON-safe hook state;MwlRuntime.save()includes it andrestore()hands it back to the same adapter.MwlRuntime.journal/MwlRuntimeAction- public runtime triggers and their trace batches are recorded in order and persisted with the save, providing a replay and diagnostics input without making MWG interpret game-specific commands.validateCatalogalso reportsMWL_ALIAS_CYCLEfor cycles among resolvedaliasofreferences; aliases outside the catalog remain available for game-defined sentinel values.validateSimulationReplay- replays aSimulationRuntimejournal from a checkpoint, compares each deterministic event batch, and optionally verifies the final state with a stable checksum.SimulationRuntimeaccepts an optionalhistory: { actorOf, limit? };canUndo,canRedo,undo()andredo()restore complete state, scheduler, RNG and journal checkpoints.CanonicalState/CanonicalStateSnapshot- a defensive, JSON-safe root state combined withStateRegistryextensions; snapshots, version migrations, transactions and restores cover all pieces atomically.LockstepClientsupports optional command validation, receives a session seed and initial state, and can submit per-tick state checksums;onDesyncreports differing peer checksums.createLockstepServeraccepts matchingseed,initialStateandvalidateInputoptions.MWL_SCHEMA_01/MWL_SCHEMA_10andschema01/schema10identify the supported MWL vocabularies;mwl migrate --to 1.0updates the document version without changing its data. Schema 1.0 additionally enforces declared required attributes and child cardinalities.ScriptHostandcreateExpressionScriptHost- the game-owned boundary for executable content. The expression host is the statement-free default. The mainmwlentry point exports its types without loading a scripting VM. Projects that opt in to the optional@datamoc/pyroeisdependency can importcreatePyroeisScriptHostfrom@datamoc/mwg/mwl/pyroeis. It provides Lua 5.4 evaluation, chunks, named function calls, JSON-shaped context values, andmwg_emit(name, payload)events. The adapter opens only the sandbox-safe standard libraries (base, coroutine, math, string, table, utf8):io,os,debugandpackagewith itsrequireare never constructed, identically in Node and the browser, the base library'sload,loadfileanddofileare removed, and chunks compile in text mode only, so a binary\x1bLuadump is rejected. It replacesmath.randomwith seeded deterministic random, and enforces an instruction budget and amemoryLimit(bytes of string and buffer data one call may allocate, 32 MB by default, parsing included), since one..orstring.repinstruction can allocate without bound. Scripts getlocal x <const>,local x <close>(with__close, run on a normal exit,break,returnand error, and unwound by a limit error too) andcoroutine.close, from fork 0.3.0; a__closecannot yield. The byte budget is the VM's per-call allocation limit (limits.totalAllocationonPyroeisVm), so a script's ownpcallsees the standardnot enough memorywhile the host still rejects the call.getmetatable("")returnsfalse. Globals persist between calls on one host, so mutually untrusted scripts need a host each. Lua VM state is not save data: reload scripts from the game's entry point.createPyroeisRuntime(the same optionalmwl/pyroeissubpath) returns aPyroeisRuntimefor games that need synchronous API responses, live state and resumable handlers. It keeps the smallerScriptHostcontract intact.defineFunction(path, callback)exposes a host function with arguments and a result, andget/setuse dotted global paths made of identifiers.runexecutes text and returns its first result;callaccepts a path or aPyroeisReference.reference(path)retains a Lua table or function's identity, including callbacks registered by Lua; dispose references when finished, or dispose the entire runtime.PyroeisValueincludes plain values, arrays, objects, Maps, BigInts, binary strings as Uint8Arrays and owned references. Ordinary tables are copied on read; contiguous sequences become arrays. Bound tables retain their references even inside lists. Nil maps to null; assigning it removes a Lua table entry. Lua VM state and references are not JSON save data.createTable(PyroeisTableBinding)creates a live proxy:getand optionalsetrun on ordinary indexed reads/writes, with optionalkeysforpairsandlengthfor#. Nested bindings can return other proxy references, allowing a game to expose a side list, each side's resources, a variable namespace or an entity lookup. Its metatable is hidden; Luarawget/rawsetstill bypass bindings, so callbacks must enforce game rules themselves.PyroeisRuntimeOptions.modulessnapshots exact module IDs and embedded source strings. Luarequirecaches the first result with Lua identity;dofilereruns a source and returns all results. A game can aliasgame.require = requirewithout copying a loader. Unknown IDs, cyclic loads and binary chunks are rejected. No filesystem, fetch or XHR is involved;io,os,debug,package,loadandloadfilestay absent. Closing a suspended module load clears its loading marker.defineYieldingFunctionreturns request values to the host; aPyroeisCoroutinecreated bycoroutine(pathOrReference)exposesstatus,resume(args)andclose(). It can also adopt a thread created by Lua, including one already suspended by the script; each thread has one host controller. Each resume returns{ done, values }, with scene answers supplied as the next resume's arguments. Close handlers run on explicit closure or runtime disposal; failed handlers retain their error until closure. Instruction and string/buffer allocation budgets cover compilation, modules, host-side indexing and coroutine resumes. Nested host calls share the current budget; a later top-level call/resume gets a fresh budget. OptionaltimeLimitbounds wall-clock time, including callbacks, but cannot preempt host JavaScript. Randomness is seeded; boundedmath.randomintervals require safe integers. Total JavaScript heap and some native operations remain outside these bounds.luaToJs(source, { format?, header? })(same subpath) translates Lua to readable JavaScript without running it and returns{ code, warnings, exports, freeGlobals }. Metatables, coroutines, generalgotoand some patterns stay asTODO(lua2js)comments with a matching warning; invalid Lua throwsLuaSyntaxError. Pyroeis's runtime numericcompileis not used: it is skipped whenever execution limits are on.
MwlRuntime exposes fireEvent(id) for named event execution. Event conditions compare
numeric variables numerically, set_variable accepts the bounded MWL expression syntax,
filters can match a unit id, unit_at can constrain a side, and both moveto events and unit
filters take coordinates as comma-separated lists or inclusive ranges (x=1,2,4-5). A
set_variable (either spelling, [set_variable] or [command] name="set_variable") may
declare what its own value= means with mode=literal|number|expression: literal keeps the
text exactly as written, number parses it and refuses anything else, expression evaluates
it and treats a missing variable as an error. Without a mode the value is inferred from its
shape ($name copies a variable, a finite number is a number, operator characters or a
numeric variable's name evaluate), which is what hand-authored content relies on and what a
compiler target cannot, since a literal such as Raise Walking Corpse (8 Gold) would be read
as an expression. path= is that same write with a target built from variables
(zombies[$index].allow_recruit, or $target for a variable holding a path), expanded before
the path is walked; a reference that names nothing throws rather than leaving an empty segment.
name=/target= stay literal (a $ there is not a reference today, so it is not read as one
now), which keeps every existing write meaning exactly what it did. Top-level
say commands execute normally. Dialogue choices are
delivered in MwlMessage.choices and answered with answerDialogue; pending choices are
included in save data. Commands after a dialogue in the same event run immediately, before
the answer, so deferred follow-up commands belong in the selected choice event.
The WML action vocabulary (item 250) is on the same event surface. fire_event runs another event
by id. store_unit writes matching units into a world variable, unstore_unit and recall write
them back (recall can move one to a chosen hex and side), modify_unit changes what its [set]
names and heal_unit adds amount (or sets hp). A unit carries its own name, role/function
and can_recruit leader flag: [unit] sets them, a side's own leader is can_recruit, and
unitMatchesFilter reads all three, so [filter] role=courier, name=Kalenz and
can_recruit=yes select on them ([store_unit] and the save keep them too). set_terrain changes
one map cell, capture_village records ownership in MwlWorld.villages, and clear_shroud
records the hexes a side has uncovered in MwlWorld.clearedShroud; the fog itself stays a game
layer. At scenario level, [role] fills MwlWorld.roles and stamps the role onto the units it
matches, [object] lands in MwlWorld.objects, and [story] in MwlWorld.story as data
(rendering a story screen is its own item), all kept in the save. [item] is deliberately not
among them: the tag already means an inventory item definition here, so WML's map-placement shape
would have collided with it.
Typical build-time usage:
npm run mwl -- build content/ -o generated/
The executable mwl-content example shows the full
path: authored content, generated catalog and manifests, then a small game reading the result
at runtime. A Wesnoth adapter can convert native .cfg files to the same MWL input model
before invoking this build step.
mwl/pyroeis
The optional Lua 5.4 entry point, @datamoc/mwg/mwl/pyroeis, kept off the mwl barrel
so the Pyroeis VM (the optional @datamoc/pyroeis dependency) loads only when a game imports
it. The mwl section above describes its behaviour in full; the exports are:
createPyroeisScriptHost(options?)- a sandboxedScriptHost(PyroeisScriptHostOptions:instructionLimit,memoryLimit,seed). Only the base, coroutine, math, string, table and utf8 libraries are opened,loadis removed,math.randomis seeded, and every call is bounded; globals persist per host.createPyroeisRuntime(options?)- aPyroeisRuntimefor synchronous host calls and live state:run,call,get/seton dotted paths,reference,defineFunction,defineYieldingFunction,createTable(live proxy tables),coroutineanddispose. Types:PyroeisValue,PyroeisReference,PyroeisCoroutine,PyroeisTableBinding,PyroeisRuntimeOptions.luaToJs(source, options?)- translates Lua to readable JavaScript without running it (LuaToJsOptions/LuaToJsResultare re-exported).
ai
Renderer-free decision runners. The game owns perception, navigation, combat rules and goals; these modules provide the execution boundary, deterministic choice support, explicit actions, serialisable state, diagnostics and budgets.
NeuralPolicyoverNeuralModel/DenseLayer: dependency-free dense inference, ReLU hidden layers and linear output scores, with flat row-major weights and an explicitobservationVersion. Construction validates dimensions and finite values before copying the weights; limits are 32 layers, 4096 neurons per layer and one million parameters.predict(input, output?)reuses activation buffers and can write into a caller-ownedFloat64Array.exportModel()returns a plain-data copy suitable for bundling in a local-file game.selectAction(NeuralObservation, random?)masks illegal actions, chooses the first best score on ties and returnsnullwhen all actions are masked. A seeded random callback enables stable-softmax sampling.behavior(id, actions, observe, sample?)maps the same policy intoJavaScriptAI, using its seeded random callback when sampling.predictBatchAsync(inputs, { signal?, timeout? })runs a whole inference batch on one worker. The caller owns stale-result rejection if a scene or position changes while awaiting it. Tiny predictions are usually cheaper synchronously; measure batches before offloading them.JavaScriptAI- registers named behaviours, checks them in declaration order, and returns a JSON-shaped action or an idle decision. A behaviour receives perception, mutable plain state, seededrandom(),emit()and a cooperativecheckpoint()for cancellation and step or wall-clock budgets. JavaScript cannot interrupt a synchronous function that never checks its checkpoint, so long-running behaviours must cooperate.Suggester- the suggestion mode (roadmap 397): the same decision call a game makes for the AI, pointed at a hint UI instead of the world, throttled by a frame-count budget (every, never wall clock) so a hint does not tax the frame budget. The firstupdatedecides immediately, throttled updates return null whilelatestkeeps the previous suggestion, anidledecision is a suggestion the UI reads (decision.action === null), andcleardrops it. The decider must not mutate game state to compute a suggestion; what the player does with one is the game's.alphaBetaSearch/AlphaBetaGame- deterministic minimax with alpha-beta pruning over a game-owned immutable-state adapter. The adapter supplies legal moves, transitions, terminal detection, current player and evaluation from the root player's perspective.depth,maxNodes, cancellation and node diagnostics bound the search; the same primitive works for a local actor and for a top-level controller such as a chess master.rootSplits/firstWinsare the root-split halves of an off-thread search: the split pairs each root move with its child in game order, and first-wins reduces scored moves so ties keep the earlier move.personalScoreView/sideScoreView/scoreWithoverScoreSubject/ScoreView/ScorePersonality- the numbers a mind that weighs outcomes needs, for the case where a search is the wrong tool. The game owns what anything is worth (scoreOf), the framework owns how a view is assembled (own, allies and enemies kept apart, plus aseencount), and the mind owns what it cares about: a personality is weights, so a selfish scout and a loyal one are content rather than two AI implementations. Both scopes read through the same code and differ only in the visibility set they are handed, a unit's own sight orFactionFog.sees(side).ScoreSubjectalso carries an optionaltype,role/function,can_recruitleader flag andname, andsubjectsWhere/ScoreSubjectFilterselect a world on any of them, so a mind finds "the enemy leader" ({ side: 'red', can_recruit: true }) without re-deriving it from ids.The heuristic AI (item 269):
Aspects/AspectValues/AspectValue- the named tuning knobs (aggression,caution,keep_away,recruitment_pattern, ...) with typed readers and no built-in values, since the numbers are content;Difficulty/DifficultyLevel- named levels as aspect overrides on a base set;Goals/Goal/GoalKind- per-unit orders a stage'sweighreads;RecruitmentPattern- a side's recruit order, cycled, with a fallback.HeuristicAIruns candidate actions throughHeuristicStages in order, the first whosewhenpasses scoring everyHeuristicCandidateand taking the best (earlier candidate on a tie); a candidate may carry the acting unit's owntype/role/can_recruit/namefor a customweighto read.defaultWeighis each factor times the aspect of the same name withkeepAwayScorefor a candidate'sdistance;goalScoreis what a customweighadds for a goal.AIDecision/AIDecisionInput/AIAction- the shared contract for perception, explicit action objects, status (action,idle,cancelled, orbudget-exceeded), emitted diagnostics and state snapshots.AICancelledError/AIBudgetExceededError- the cooperative control-flow errors used by a JavaScript behaviour when it observes cancellation or exceeds its configured budget.exportState/importState- versioned, JSON-shaped agent state. Function closures, VM state and arbitrary object graphs never become save data.LuaAI/createLuaAIfrom@datamoc/mwg/ai/lua- optional Lua 5.4 provider with the same action and state envelope. A Lua function receives(perception, state)and returns an action table or{ action, state, events }. It uses the existing optional Pyroeis host, which removes filesystem, process, module-loading, clock and debug access, supplies seeded random, and enforces an instruction budget.AIAgentDefinition/AIBehavior- JavaScript registrations; Lua registrations instead provide a source chunk and an entry function name. Both remain game-owned content and do not include any reference game's code or data.
MWL [ai] profiles describe the boundary without embedding executable game rules: scope
is actor or controller, provider is javascript or lua, algorithm can be
rules or alpha_beta, and depth, max_nodes, player, moves, apply, terminal
and evaluate identify search limits and game-owned operations. A game resolves those names
to JavaScript behaviours or Lua functions and supplies the actual state, legal moves and
evaluation.
Skeleton extensions (417-421)
skeleton.solveTwoBoneIK(skeleton, first, second, target, bend, pose)solves a connected two-bone chain in world coordinates.bendis +1 or -1; positive bends clockwise on screen. Positive finite lengths and a zero origin on the second bone are required. Unreachable targets throw with the chain names. Rest rotations and rotated ancestors are supported.Bone.limits: { min, max }bounds the absolute local rotation in degrees, including rest rotation.skeleton.validatePosechecks an authored pose;samplePoseandsampleTransformPoseaccept an optional skeleton for validation. Mixers andresolvevalidate automatically. Use a nonnegative or nonpositive interval to constrain bend direction.Keyframe.translationsandKeyframe.scalesmap bone names to{ x, y }channels.skeleton.sampleTransformPoseandskeleton.advancePosereturn aTransformPose, withrotations,translationsandscales. Missing channels interpolate toward zero translation or unit scale. Looping, reverse playback, holding, priority layers and crossfades share the rotation timeline. LegacysamplePoseandadvancereturn rotations only.SkeletalSprite.applyaccepts either rotation poses orTransformPose; its mixer usesadvancePoseautomatically.setDiagnostics(true)explicitly enables bone lines, pivot crosses, joint circles and tips. Omit it in release builds; disabled rigs allocate no overlay.assets.loadSvgParts(sourceOrPath, { resolution, groups })returns borrowed textures keyed by top-level SVG group name. It preserves the full SVG canvas, group attributes, shared defs and styles. Repeated loads reuse textures; resolutions have separate cache entries. Compiled asset paths decode directly from data URIs underfile://. Unknown groups and invalid resolutions throw by name.releaseSvgParts(parts)unloads them once no sprite uses them.node tools/check-skeleton-extensions-browser.mjs [local-vehicle.svg]builds a classic local-file page, verifies WebGL rendering, texture reuse, resolution isolation and diagnostic cleanup, and writes screenshots underbenchmark-results/skeleton-extensions/. The default vehicle is generated; passing a local consumer SVG embeds it only into ignored scratch output.
Deformation, articulated SVG loading and asset filtering (414-416)
PinnedDeform/DeformableSpriteexpose pinned control points.dragVertex,nudgeVertexandresetVerticeschange GPU vertex positions without changing UVs or rotating the sprite.dragVertices([{ index, x, y }, ...])validates a batch before applying it and uploads once; the purePinnedDeform.dragManyperforms the same batch operation without a renderer.loadSvgSkeleton(sourceOrPath, { skeleton, bindings?, pivots?, resolution?, mixer? })returns{ sprite, textures }.spriteis aSkeletalSpritewith named SVG groups already bound. By default matching groups bind in SVG document order. Explicitbindingsprovide draw order, group-to-bone mapping and optional pivot/rest-angle overrides. Pivots come fromdata-pivot-xanddata-pivot-y(root SVG user coordinates), or apivotsmap keyed by group id. Missing pivots, groups, bones and duplicate bindings fail by name. Separately bound descendants are removed from parent textures; ancestor transforms, shared styles and defs survive. Artwork is normalized against each bone's rest angle so poses rotate the correct limbs about their pivots. Browser DOMParser/XMLSerializer are required. Compiled paths work underfile://. Destroy the sprite with{ children: true }, then callreleaseSvgParts(textures)when all users are done.mwgPage({ assetFiles: ['hero.svg', 'music/theme.ogg'] })embeds only those asset-folder paths. OmitassetFilesto keep all supported assets;[]embeds none. Unknown or unsupported named entries fail before replacing compiled asset scripts. BothemitPageandcompileResourcesaccept the same option.credits.jsongeneratesCREDITS.txtwhen selected, or with no filter.- Browser checks:
node tools/check-sprite-deform-browser.mjs [local-bird.svg]andnode tools/check-svg-skeleton-browser.mjs [local-character.svg] [local-manifest.json]build local-file WebGL pages and save rest/posed screenshots underbenchmark-results/. Defaults use generated art; consumer inputs remain in ignored scratch output only.