Changelog

v0.17.0

New Features

  • The content editor checks what content names, as you type.
    • What is checked: every id an action, a condition, a lore link or a speaker tag names is looked up in the game's data and its dungeons – assets, music, characters and slots, attribute values, items, statuses, records, scenes, &anchors, rooms, $templates, quest logs and flags – and so is every action, condition, placeholder and text effect name.
    • How it shows: a typo is underlined, amber for a warning and red for an error, and the toolbar counts them. The list offers one-click "did you mean" fixes and an Ignore here; the Ignored tab lists what a dungeon ignores, and ignores nothing needs any more. Whole game runs the same checks over every dungeon.
    • Saving always works; only errors ask first. Live checks can be switched off in the list.
  • Suggestions and hover in the content editor. Typing inside {…}, if{…}, or after |, [[ or [ suggests what can go there – actions with their help, then the ids the action takes: assets with thumbnails, characters with faces, slots after ->, attribute values, scenes, anchors, flags, conditions and placeholders. Ctrl+Space suggests anywhere, a paragraph's speaker included. Hovering an id shows its name, picture and where it comes from; Ctrl+click goes to it. Suggestions turn off under Typing in the toolbar, which now also holds the curly-quote and en-dash switches.
  • Suggestions in the debug panel. The Execute Actions box suggests and describes the same way, from the running game: the scenes of the room you are in, flags it has set, characters it holds.
  • A bare condition checks that a value is set. if{met_bob}, if: "town.gate_open" or _in_party(alice) with no comparison passes when the value is anything but 0, false or empty, so an unset flag reads as false. Before, a condition without an operator was logged as invalid and always failed.
  • Template instances. A character template has a new Instances list for scenes that need several of the same character: each row is a character id (guard1, guard2) with only the traits, attributes and stats that differ from the template. Instances are created on a new game, and on load when a save is missing them; with Auto create off, {create_character: "guard2"} or game.createCharacter('guard2') creates one without naming the template. game.resetCharacter() and the save restore that rebuilds characters on load keep an instance's overrides.
  • Blend modes on skin layers. A static skin layer has a new Blend mode – screen, multiply, overlay, soft-light, lighten, darken, color-dodge or plus-lighter – that mixes it into the character's layers below it the way a Photoshop layer blend does, so smoke, glows and mist can be drawn as the artist painted them instead of flattened. A layer only blends with its own character, never with the background, the UI or another character, so it looks the same in a scene, the gallery, a face portrait and battle.
  • character.resetLooksToTemplate(). Puts a character's attributes and skin layers back to their template's, keeping stats, statuses, traits and inventory.
  • Search in the gallery. A search field above the gallery's groups filters the Characters, Assets and Scenes tabs by name. A group whose own name matches keeps all its entries, and every group with a match stays open while searching. Only shown names are searched, so an undiscovered entry never gives its real name away.
  • Item flashes open the item's card. The scene line an add_item, remove_item or equip action prints – "Moonleaf has been added to your inventory!" – now names the item as an item link, as a status line already did: the name takes its rarity color, and hovering or tapping it opens the item card.
  • freeze_timer state (turn_system). While it is true the map-turn clock stands still: room entries, {turn}, game.getService('turns').advance() and the Wait button stop advancing it, so statuses stop ticking, collectables stop regrowing and turn_advanced stops firing. The Wait button hides itself meanwhile. Set it with {state: "freeze_timer=true"} and back to false to let time run again.
  • Rooms can stop costing time (turn_system). A new Advance on room enter setting in Turn Config, on by default. Turned off, entering a room no longer passes a turn, so a game whose rooms are freely visited VN screens can pass time only through {turn}, game.getService('turns').advance() and the Wait button.
  • Records link to characters. A record has a new character field naming the character it is about. Once the player has met the character, the Encyclopedia entry shows a button that opens their Gallery page, and once the record is discovered, the Gallery page shows a button that opens the entry. Scripts can do the same with game.getCharacterRecordId(), game.isCharacterInGallery() and game.openGalleryForCharacter().
  • Bake a face crop into a static face. The Art Manager's Face tab has a Bake crop to static face button. It renders the face crop as the preview shows it – static layers or spine, with the selected layers and attributes – into a 300×300 WebP at games_assets/<game>/<mod>/faces/<id>.webp and sets that as face_static, asking first when the file already exists. The image is written at once; the new face_static path is kept when the entry is saved.
  • Merging (rpg_battler). A new merge ability aspect lets a combatant merge into an ally: it leaves the field and the ally grows by its base health and power, keeps both bodies' remaining health, takes over its afflictions and gains a stack of the chosen status.
  • Reveal All Records & Gallery (dev mode). A new toggle in the debug panel's Options tab shows every encyclopedia record and follow-up, every gallery character and asset, and every gallery scene as discovered, so content can be checked without playing to it. It only changes what is shown: nothing is written to the save, and switching it off brings back the player's real progress.
  • Reputation (new plugin). What each character thinks of the player, kept as a reputation trait. {reputation: "rosa -= 1"} changes it and adds "Rosa will remember that." under the dialogue text until the next paragraph.
  • A character's own idle. A new idle character trait is the loop a character plays wherever it is staged: set float on a bat or a moth-girl and every {actor} line lifts it, without idle= on each one.

Breaking Changes

  • Adding and subtracting is += and -=. flag, property, char, stat, resource and skin_style take = to set, += to add and -= to subtract: {flag: "button_pressed += 1"}, {resource: "alice.health -= 10"}. The old > and < read like the comparisons in a condition (_item_count(x) > 0) and are no longer accepted — a spec that still uses them is logged and skipped. game.parseOpsSpec() returns the same =, += and -= as its op.

v0.16.3

New Features

Editor

  • Party line lint. The Dungeon Content Editor warns when an id!: party line carries actions beyond staging – a menu, a flag, a jump, an item. They are skipped with the line while that character is away, and a {…} line written right under the text is part of it. Staging (actor, asset, sound, music, animate, grade and the like) is left alone.

Bug Fixes

  • A skipped paragraph keeps its choices. A party line (id!:) whose character is away, or a paragraph an if{} left empty, used to take its > choices with it – and when it closed its block, the ~ branches too, so the reader landed in the first branch or dropped out of the scene. The text is still skipped, but its choices now replace "continue" on the paragraph before it. When the reader arrives on such a paragraph by a jump instead, the text already on screen stays and the choices show under it.

v0.16.2

Bug Fixes

  • Mobile UI fixes. Screens, panels and buttons now fit phones and tablets and can be reached by touch.

v0.16.1

Bug Fixes

  • A background replaces the one before it. Staging a bg asset used to leave every earlier background on stage beneath it, room defaults included, so a long scene piled up full-screen images. Weaker GPUs, phones in particular, flickered under the load and drew pictures late. Now the new background takes over: the old one stays until the new one has faded in, then leaves, so the change never dips to black. One drawn above the newcomer exits with its own animation.
  • No black frames between scenes. A scene ending into another, an {enter} into a room or dungeon whose event opens a new scene, and {asset: "false, forest"} all emptied the stage at once, so the incoming background faded in over black. The background on screen now stays until the incoming one has faded in over it, and the same background carried into the next scene simply stays. A scene that ends with nothing after it clears the stage as before.
  • Entering another dungeon from a scene works again. An {enter: "dungeon.room"} played from a scene could stop half-way when the room being left has no namesake in the new dungeon: the map switched, the player kept the old room, landed off the map and every move failed with "Cannot reach that location".
  • Map encounters near the compass are clickable again. The current room's compass draws above every encounter, and its whole box caught clicks, so an encounter image or polygon near the room's center could not be selected. Only the compass arrows catch clicks now.
  • The battle result Continue button stays on screen (rpg_battler). A tall result panel: rewards, level-ups, loot could push Continue below a short screen, such as a phone held sideways, with no way to scroll to it. The result card now fits the screen: the Victory or Defeat title and Continue stay in place and the content between them scrolls.

v0.16.0

New Features

Localization

  • Thirteen translations. German, Spanish, French, Italian, Japanese, Korean, Polish, Brazilian Portuguese, Russian, Turkish, Ukrainian and both Chinese scripts. English is always loaded first and the chosen language layers over it, so a partial file falls back key by key instead of printing [key].

Interface

  • One-shot actor animations. {animate: "chimera lunge, mc recoil"} or anim= in actor props plays lunge, recoil, hop, shake, shiver, nod, bounce or flash once, on top of the idle loop. They are events, never saved — loading a save does not replay them.
  • Richer scene grades. A grade can now split-tone (shadows lean one colour, highlights another) and light the scene backgrounds with a sky gradient, a vignette and animated light rays. Every preset was retuned to use them — dusk reads as a low sun, night gets navy shadows and moonlit highlights, underwater light shafts from above, infernal a fire glow from below — instead of a flat colour wash. The grade docs show each preset on a sample scene. Every field is available in the object form of {grade}. The rays are pixi-filters' GodrayFilter, created only while a grade has rays.
  • {ambient} particles. {ambient: "fireflies"}, "motes" or "embers" drifts lights over the scene backgrounds, below the actors, with an optional density ("fireflies#0.5"). Saved like the grade and independent of it. Also game.setAmbient() / game.getAmbient().
  • {screen_shake} and {screen_flash}. A decaying tremor on the scene art (backgrounds and actors together), and a colour pulse (red, white, any grade preset) that returns to the current grade. The dialogue box is untouched by both.
  • Hover cards say how to reach into them. A footer on every hover card names the T key, which flips cards between peek and interactive, and shows an On/Off pill for the current state so a press reads back immediately.

Items

  • remove_item flashes. Taking an item away from the party bag now shows the mirror of the pickup flash ("X has been removed from your inventory."), for both remove_item: true (the active item) and id specs. Chests and merchants stay silent.
  • Item cards compare against equipped gear. Hovering an unequipped item in the bag, a shop or a crafting station adds a table of stat changes against what the selected character wears: ▲/▼ per stat, coloured by whether the change helps, including the stats you would lose. An item that fits several slots (two rings, three sockets) gets a column per slot, so the card shows where it is best. New character.compareItem(item) returns the same numbers, and the item_compare emitter lets a game line up stats it renames on equip.
  • Two curves for levelled gear (experience). Each stat's new scaling Stat Meta picks how it grows on levelled equipment: flat rides power_scale_per_level as before, relative rides the slower relative_scale_per_level (default 0.08). Chances, percentages and speed go on the relative curve, so their numbers still climb on every drop without running into 100% dodge or crit a few levels in. rpg_battler ships the setting on all its stats. The reward service gains scaleStats(stats, level) and relativeScale(level) for anything that should scale like gear.
  • Status links in item text preview what the item grants. The item card hands itself to the status cards it opens – a [[status:id]] link in its description, its consume list – and the new status_preview emitter lets a game rewrite the not-yet-applied status's stats first, so a levelled item's status shows its real numbers before it is applied.
  • status_migrate emitter. The save-migration pass recreates statuses from their definitions, which dropped anything set on a status when it was applied. It now fires status_migrate(character, status, previous) for each one before adding it back, so a game can rebuild it from the instance it replaces – the status twin of item_migrate. The pass also keeps each status's remaining duration and source now: loading an old save (or any save in dev mode) used to refill every timed buff and debuff to its full duration.
  • no_discard item trait. A per-item switch, shipped for every game by global_essentials: the item card's Drop choice and the reward panel's trash button stay hidden for it. Quest rarity keeps working as before; this is for story gear of any rarity that must stay in the bag. It has nothing to do with loot drops. The item card's choice now reads Discard (English locale) to match.

Scripting

  • {no_visited: true} choice param. The choice is never added to the visited list, so it keeps its undimmed look however often it is picked — for repeatable options such as a stroke-the-statues sequence puzzle.
  • Scene emitters carry the anchor. scene_play_before, scene_play and scene_play_after gain a fourth argument, the &anchor name the paragraph was reached by ('' for a plain scene root), so a listener can react to one branch of a scene without resolving the raw line id.
  • {set_defeated: "a, b"} action (rpg_battler). Marks battle definitions defeated without a victory: the clear is recorded and battle_defeated fires once per definition, but no battle_finished, rewards or delay — for fights that never happen on the page, where win would be a lie.
  • New game_setting_change emitter. Fires from game.setGameSetting() and from the Game Settings menu, with (key, newValue, oldValue). Muted during a save load, so a restored value never reads as a change — unlike a Vue watch on game.getGameSetting(), which fires mid-load on half-restored state.
  • Asset Meta. Assets gain a meta data bag with its own editor tab, like Stat Meta and Status Meta: define a field once and every asset's form shows it, for your scripts to read as asset.meta.
  • New spine_event emitter. Event keys authored in a spine asset's animations now reach scripts with (asset, name, data), every loop pass at the animation's playing speed – a sound or a shake lands on the frame it belongs to at any timescale.
  • New asset_exit emitter. Fires once when a staged asset starts leaving – {asset: "!id"}, a clear/false/reset or solo sweep, the scene ending – the counterpart of asset_render. Pair the two to give an asset a looping sound that stops when it leaves the stage.
  • Assets take the same filters as actor slots. Image and video assets gain brightness, contrast, saturate, sepia and hue beside blur, on the template and as inline props ({asset: "bg_mountain(sepia = 1, saturate = 0.5)"}). Re-staging a visible asset glides every filter over its tween, the same as position and opacity, so a backdrop can wash out or warm up in one line.
  • Dialogue box states. dialogue_minimized is the player's Ctrl+H fold as a state, so content can fold the box and a save restores it; the show-dialogue button still brings it back. hide_dialogue removes the box with its buttons for a staging beat ({state: "hide_dialogue=true"} on an [nw] transition paragraph) and clears itself when the next scene plays.
  • Pick the exit on removal. {asset: "!portrait(exit=fadeSlideLeft, exit_duration=2)"} takes the same (prop=val) tail as the add form: the props land on the staged asset and the exit plays with them, so a one-off exit no longer needs a re-stage before the !. The transition fields were always legal inline overrides on an add or re-stage (enter_duration, enter_delay, enter_ease, exit_duration, exit_ease); the assets page now says so.
  • {trait} writes typed values. {trait: "peth.battle_ignore = false"} now stores the boolean false for a boolean trait and a number for a number trait. It used to store the text "false", which reads as true, so a boolean trait could never be switched off from content. A value that doesn't fit the trait's type is logged and skipped.
  • _fought(battleId) condition (rpg_battler). True only for a battle won in actual combat. _defeated stays true for every route, including {win} after a seduction, so content can now tell a fight from the other ways a battle gets cleared.
  • Fight choices wear a badge (rpg_battler). A choice that carries {battle} shows crossed swords before its label and gets the fight-choice row class, so a fight option reads as one next to its alternatives. {fight: true} gives the same badge to a choice whose battle starts in the scene it leads to. Hide .fight-badge in the game's CSS to drop the icon.
  • {reveal_room: "3, 4"} action. Marks rooms explored without walking into them – visited and visible, their neighbors visible – the way a map, a guide or a view from a tower uncovers them. No room events run. dungeon.room reaches another dungeon.
  • _room and _choice_visited(id) conditions. _room = room5 reads the room the player stands in. _choice_visited(~1.intro.3.1) is true once that choice has been picked, and _choice_visited(dungeon2.~1.intro.3.1) asks another dungeon. It reads choices made in hindsight, for branches that never set a flag and saves made before one existed; new content should set a flag on the branch and test that.
  • _active_item, _active_item_slot and _slot_filled conditions. _active_item(rusty_key) = true asks which item's custom choice opened the scene, _active_item_slot(alice) = ring where that item is worn, and _slot_filled(alice, helmet) = false whether a slot holds anything.
  • block_party_inventory_persist state. Blocks item use, equipping and dismissing like block_party_inventory, but stays on until content clears it and is saved with the game. The engine resets block_party_inventory at every scene start and end, so it could never hold a lock across a whole area.
  • {status} takes stacks and a duration. {status: "alice->poisoned(stacks = 2, duration = 3)"} overrides the template's values for that apply, on a first apply and on a refresh alike. Either key works alone: poisoned(duration = 3).
  • {timestamp: "x"} action and _turns_since(x) condition (turn_system). Stamp the current turn into a flag, then gate on the wait: _turns_since(last_rest) >= 5. A flag never stamped counts as forever ago.
  • Room hooks know where the player came from. room_enter_before and room_enter_after gain fromRoomId and fromDungeonId. They are right across dungeons too: on arrival from another dungeon game.getCurrentRoomId() still names the old dungeon's room. Forced-movement mazes and one-way passages need the source room.
  • ignore_replay beside a choices menu. {choices: "&talk_hub", ignore_replay: true} skips that menu in a gallery replay, so a scene that loops back into a conversation hub ends the replay instead of leading back into play.
  • Battle end emitters carry the battle id (rpg_battler). battle_end, battle_closed_before and battle_closed_after gain a second argument, the battle definition's id (null for an ad-hoc battle), so a listener can react to one fight's outcome.
  • Choices move the story last. A choice's eventDelayed actions (enter, scene, exit, battle, loot, trade and the like) now run after its other keys, in whatever order they are written. {enter: "19", flag: "golem_free = 1"} used to walk into room 19, and play its events, before the flag was set.
  • ifOr / activeOr keep parenthesized arguments whole. The OR form split on every comma, so ifOr: "_item_on(alice, ring) = true, gold > 5" cut _item_on(alice, ring) in two and never matched. It now splits like if does.

Battles and loot

  • Over-cap waves split (rpg_battler). A wave with more enemies than max_enemy_units (6) used to spill past the field. It now splits, in roster order, into successive waves of at most the cap.
  • Named pooled loot (experience). {loot: "^trash1", title: "Dusty Shelf"} names that placement's inventory, so its window reads "Dusty Shelf" instead of the generic name.

Battle VFX (rpg_battler)

  • A lot of new feature for Projectiles.
  • Caster animation on projectiles. A projectile's caster_animation picks how the caster moves: melee lunges at the target in the attack pose (an impact plays on contact, a travel sprite launches at the lunge's peak), cast keeps the caster in place in the cast pose. Left empty, a hostile ability lunges without images and casts with them, and a self or ally ability casts – so a projectile or impact now plays the cast pose instead of the attack pose.

Editor

  • Stat Meta. Stats get a meta bag like statuses: define custom fields in the new Stat Meta tab (after Stat Groups) and every stat form shows them; scripts read them through game.getData("character_stats", true). Plugins declare fields of their own, and a plugin may fill in another plugin's fields on the stats it defines. The Status Meta tab moves to sit right after Statuses.
  • Anchor lint. The Dungeon Content Editor flags an &anchor that is not the first line of a paragraph (the parser drops it), and a {choices} menu anchor that is not on the block's last paragraph (the menu would show as "continue").
  • Plugin save hooks. A plugin can declare editor_hooks in plugin.json: a script whose beforeSave(entry, ctx) fills in or tidies an entry of the plugin's own tabs as it is saved. It may be async, and it runs on every save path (the tab form, popups, save-and-jump) but only on the entries the save changes. For a mod it also receives the core game's entry. See Plugins → Editor Save Hooks.
  • Saves no longer overlap. A second save waits for the one in progress, and a popup shows the entry as it was written once a save finishes.
  • Back and Forward in the Docs Viewer. Arrow buttons in the header step through the pages you opened, including jumps between plugins' docs, and return each page at the scroll position you left it. Alt+Left / Alt+Right and the mouse side buttons do the same.
  • Ability Editor. Ability templates get an Ability Editor popup that puts one ability on one screen with its card rendered live beside the form.
  • Item Editor. Item templates get an Item Editor popup.
  • Inventory Editor. Inventory templates get an Inventory Editor popup.
  • Stat Groups tab. A new Characters → Stat Groups tab, right after Stats, defines the character sheet's sections as data: an id, a title and an order.
  • Stats popup. Character templates, statuses, item templates and skill slots get a Stats button beside Art Manager.
  • Art Manager previews statuses on a character. Statuses, items and skill slots get a Preview on character picker. The entry is drawn over that template the way the game applies it, so layers keyed on the character's attributes resolve without setting those attributes on the entry. The pick is remembered in this browser for all of them and never saved. The Attributes tab adds a preview-only column for the character's own values, for checking a state such as battle_state = cast, and marks every attribute the entry sets, since that value overrides the character's own while applied.
  • Dismiss follows the inventory lock. While block_party_inventory is set (every battle, trades), the character sheet's Dismiss button dims and a click says why instead of opening the confirm – dismissing hands the character's gear back to the party inventory. New locale line dismiss_blocked.
  • Asset visual editor covers filters and the tween. The asset popup's Filters block carries the five new sliders beside Blur, a new Re-stage Tween block sets tween and tween_ease (and the sliders glide the preview through them), and the Spine Playback block adds the loop toggle and the timescale.
  • Filter presets. Every list tab's filter form can save its current values as a named chip. Chips sit under the Filters title, and hovering Filters in the bookmark column lists them too; a click applies one, and the chip whose conditions match the form lights up.
  • Dark theme. The sun/moon button at the far left of the toolbar, beside Exit, switches the whole editor.

Audio

  • Room and dungeon ambience. Rooms and the dungeon config gain Default Sounds beside Default Assets. The dungeon's play anywhere in it and a room's on top while the player is there; they keep playing through scenes, stop as the player walks out, and come back after a scene that silenced them.
  • Map loops outlive scenes, as documented. A scene's end used to stop every looping sound, including ambience started from a room or dungeon hook. Now it stops only the loops the scene itself started.
  • Fades, gain and delay. Music tracks and sounds gain volume, fade_in and fade_out; sounds add delay, music adds shuffle. Each is a default on the entity and can be overridden per play with the (prop=val) tail assets use: {music: "tense(fade_out=3, fade_in=1)"}, {sound: "rain(volume=0.4, fade_in=2)"}, {sound: "!rain(fade_out=3)"}. A sound's fade_out also applies when the scene ends. {music: "!"} stops the music, with "!(fade_out=N)" for the fade – there was no way to stop music from content before.
  • Sounds by file path. game.playSounds and {sound} accept a sound id or an audio file's full path.
  • Random sounds. A sound's new random flag turns its files into a pool: each play picks one at random instead of playing them all in sequence – footsteps, hits, anything that repeats. With loop, the files follow each other in random order.
  • Sound channels. A sound's new channel field makes sounds replace each other: starting one stops whatever else is playing on that channel, each over its own fade_out. Use it to swap one ambience for the next, or to cut a voice line off with the next one.

Text

  • Text pacing tags – Ren'Py-style markers in prose steer the typing animation: [w] waits for a click, [w=1] waits a second, [p] waits then breaks the line, [nw] advances the paragraph on its own, [fast] shows everything before it at once, and [cps=30]…[/cps] / [cps=*2]…[/cps] change the typing rate. A click while typing now reveals up to the next [w] instead of the end. See Dungeons → Text Tags.
  • Text styling tags – [color=#hex]…[/color] and [size=1.2]…[/size] style prose without raw HTML.
  • Named colour tags – [green], [red], [gold], [pink], [purple], [blue] and [grey], in shades that read on the dark text panels. Each is a .text-<name> class a game can recolour. The engine's flash lines use them: gains green, losses red, quests and learned skills or recipes gold, and a removed item plain.
  • Text effects – [shake]…[/shake], [wave], [bounce], [pulse], [rainbow], [glow], [spooky], [flicker], [blur] and [glitch] animate the wrapped letters. They nest, type out normally, keep each word whole when a line wraps, and are plain .fx-<name> CSS rules a game can restyle or extend.
  • Party lines – ane!: “…” plays only while that character is in the party. Otherwise the paragraph is skipped, actions included, so one scene can carry optional companion lines for any company. The new _in_party(ane) condition covers the other cases, e.g. a line that plays only when Ane is away. See Dungeons → Talking Characters.

Breaking Changes

  • .flash-content .special removed. The engine's flash lines carry colour tags instead of <span class='special'>. A game line that still wraps itself in special shows in the plain flash colour; switch it to [green]…[/green].
  • no_scale_stats removed (experience). Levelled gear scales a stat only when its scaling Stat Meta says how. A stat with none set stays as authored, so a game's own gear stats need flat or relative to keep growing; rpg_battler's stats come set.
  • {choose_item} cancel no longer advances the story. Cancel closes the picker and leaves the scene where it stood, so the >, ~ or ! choice that opened it is still on screen and the player can pick again or go another way. It used to run the equivalent of a click-to-continue, which skipped the paragraph on a mis-click. chosen_item_id is still cleared to ''.
  • item.isDroppable() renamed to item.isDiscardable(). It always answered "may the player throw this away?", and the old name read like a loot-drop check. Same rule (equipped gear, quest rarity) plus the new no_discard trait.
  • Discard identifiers renamed – "drop" now means loot only. Emitters item_drop_render → item_discard_render and item_drop_before → item_discard_before; the action drop_item → discard_item; locale keys drop_item, item_drop_confirm, item_dropped, item_dropped_quantity → discard_item, item_discard_confirm, item_discarded, item_discarded_quantity (a game locale override or a listener on the old names must follow).
  • Content action {skin_layer: ...} renamed to {skin: ...}. The syntax is unchanged – {skin: "alice->armor & helmet, bob->!cloak"}.
  • Projectile sizes are percentages (rpg_battler). projectile_travel_size and projectile_hit_size in the battler's Config tab are now a share of the viewport height, not pixels: a game that set pixel values (96 is 96% of the screen) must set them again. The defaults, 9 and 18, come to about 97 and 194 px at 1080p.
  • Re-playing a running loop no longer restarts it. {sound: "rain"} while rain already loops leaves it playing where it is, instead of cutting back to the first file; a new volume on the repeat still applies. To restart one on purpose, stop and play it in the same action: {sound: "!rain, rain"}.
  • getTrait() keeps false, 0 and ''. On characters and items it used to read any falsy value as null, so an explicit false looked unset. Now only an unset trait is null, and a persistent trait whose template says false or 0 returns that instead of the live value.
  • Supports fight on their own (rpg_battler). A battle_support character is AI-driven without battle_ai. Only an explicit battle_ai: false hands its turns to the player.
  • Projectile sounds are audio files (rpg_battler). A projectile's sound and hit_sound are file fields (fileType: audio) holding one file each, not lists of sound ids; pick the file itself.

v0.15.1

New Features

Engine Editor

  • Art Manager. The character art popup now edits the whole look in one place, with the doll previewing every change live. Five tabs – Layers, Attributes, Spine, Face and Offset – tick skin layers on and off (each row shows the image or animation key it resolves to for the current attributes, red when nothing matches), pick attribute values from dropdowns, add or swap the per-view spine atlas and skeleton, set the static face image and its precedence beside the face crop, and tune the per-view art offset by dragging the doll or scrolling over it to scale. The pencil on a layer row saves and jumps to that layer in Skin Layers. Available wherever the button was: character templates, statuses, item templates and skill slots.

v0.15.0

New Features

Achievements

  • Achievements. A new Achievements editor section – Achievements, Tiers and Groups – defines the entries, their prestige rank and the sub-tabs the in-game tab is split into. The engine stores progress, scores the points, pops the notification and draws the tab, and registers nothing when a game defines no achievements.
  • Achievement progress API (game.progressAccolade). Scripts do the tracking: progressAccolade(id, delta), setAccoladeProgress(id, value), progressAccoladesByTag(tag) and setAccoladeTarget(id, n). Progress only ever rises, and the new accolade_completed emitter hands you the id and its points to pay out.

Content and DryadScript

  • choose_item action. Opens an item picker from content, filtered by id, category or tags, and stores the pick in active_item and the new chosen_item_id state. Branch on it with the new _chosen_item(id) condition.
  • Book reader (book trait). An item whose book trait names a scene becomes a paged reader – one paragraph per page, with a Read choice added to its card. Bookmarks persist per item in book_bookmarks, and {read_book: true} opens one from content.
  • Paintings (view_painting). An item with the painting trait can be looked at: the named asset goes full-screen with the item's description as the text. Leave the trait empty and the item id doubles as the asset id.
  • Key locks (key / key_consume). Rooms take these two fields and inventories take them as traits, so entering a locked room or opening a locked chest spends a key from the party bag. Scene-driven movement bypasses the lock, and the new key_used emitter reports what opened what.
  • Gather spots (collect_pool). A collectable can draw its item from a table instead of naming one, resolved through the new collectable_resolve emitter at dungeon creation. Prose on the @ line can use the new |title| and |description| placeholders for the item that was drawn.
  • Scene actor rail (panel_actor). Scenes list their cast in a rail opposite the party list, and clicking a face opens that character's viewer. {panel_actor: "alice, bob"} adds someone with no art on stage, hide_actor_list suppresses the panel for a beat.
  • _ability condition. Whether a character currently has an ability: _ability(alice.fireball) = 1. It reads the final set, so abilities from a status or an equipped item count.

Game API and emitters

  • Item discovery. game.discoverItem(id), game.isItemDiscovered(id) and game.getDiscoveredItems(), backed by an item_discovered emitter that fires once per item per save. Learning a recipe, finishing a book and viewing a painting each discover the item automatically.
  • Shadow dungeons. game.addShadowDungeon(id, { config, content, rooms, encounters }) defines a dungeon at runtime that the engine treats like an authored one, from raw DryadScript or a pre-parsed line array. Ids must start with an underscore, definitions persist in saves, and game.removeShadowDungeon(id) cleans up.
  • game.reorderParty(orderedIds). Reorders the party with no join or leave side effects, rejecting anything but the exact current members in a new sequence. Party order drives the character rail and the default battle roster, and players can now drag the rail themselves.
  • window.engine.popups. closeAll() drops the transient hover stack and closePopupsByKey(prefix) removes pinned cards too. Reach for it before opening a dialog of your own, since these cards paint above the modal layer.
  • status_apply_before emitter. Fires before any status lands, reapplies included – which status_added skips. Return false to refuse it, or mutate stacks, duration or source in place to change what lands.
  • Inventory and economy emitters. inventory_transfer_after is the post-mutation counterpart of inventory_transfer, and inventory_craft fires after a recipe is crafted. currency_change fires once per currency whenever a payment lands, negative when paid and positive when received.
  • quest_updated emitter. Fires after a quest log line lands, with (questId, goalId, result, dungeonId). The result tells a brand new quest from a goal that just closed one out, so you needn't track quest state yourself.
  • item_drop_render emitter and item.isDroppable(). Both discard affordances now ask the same two questions: isDroppable() is the engine's rule (equipped gear, quest rarity), the emitter is the game's own veto. It is a pure predicate that runs on every render, so listeners must only return.

Components and slots

  • Savelist and StatusCard exported components. Savelist is the engine's own save/load list, for a game-over screen that has to offer a way back. StatusCard is the full status card StatusBrick shows on hover, exposed to mount as your own popover.
  • Built-in Quest filter tab. The inventory filter bar ends with an engine-owned tab that collects every item of quest rarity, whatever category each one sits in. Games no longer need a Quest category of their own, and re-skin the tab through the .inventory-filter-icon-quest CSS class.
  • item-card-top component slot. Renders at the top of the item card body, above the description, for a readout that belongs beside the item's name. The existing item-card slot is unchanged and still renders at the bottom.

Authoring

  • Ability effect order and description_attach. order pins both the card's display order and the resolution order, lowest first, so a merged ability no longer inherits the order its modifiers were collected in. description_attach is free text under the effect name for behavior no aspect can express, and it runs through the text resolver.
  • Skill tree order. Sorts the tree tabs, lowest first, and decides which tree opens selected. Trees left on 0 keep the sequence the character was granted them in.
  • Dismiss and rename (dismissable, renameable). Two Essentials character traits that add a confirming Dismiss button and the inline rename control to the character sheet. Both hide under the new viewerMode prop, so a battle inspect popup can never delete a live combatant.
  • Recipe groups. A new Recipe Groups editor tab, a recipe_group field on each recipe and a group_recipes list on each inventory. Membership lives on the recipe, so a new recipe joins every station that lists its group by setting one field.
  • Developer-only ability fields (debug_info). Flag an ability definition and its raw value prints at the bottom of the card – but only in dev mode with Show Hidden Stats on. Works on meta and aspect roles, for tuning fields like AI weights that have no player-facing meaning.
  • Entity id badges on cards. Ability, item, status, stat, skill, tree and record cards now show the entity's id in a small monospace badge. Always on in the Engine Editor, and in-game behind the Show Hidden Stats dev setting.
  • Spine asset transitions and idles. Spine scene assets now take the same enter, exit and idle animation fields as images and videos. The enter transition waits for the skeleton to arrive, so it reveals the art instead of fading in an empty box.
  • Borrowed status icons. A status with no art of its own wears the icon of whatever applied it – the potion, the equipped item, or the ability that cast it. The source is resolved live through the new Status.displayImage, so re-icon the item and every existing save follows.

Engine Editor

  • Abilities picker. Character templates, statuses, item templates and skill slots gain an Abilities button that opens a visual picker where every candidate renders as the real in-game card. Filters come from the ability schema, and each card can copy its id or save and jump to it in Ability Templates.

RPG Battler plugin

  • Multi-wave battles (enemies2). A second enemy list spawns instead of victory once the first is wiped, and the party keeps its health, statuses and cooldowns. Scripts can pass start({ waves: [[...], [...]] }), and the new battle_wave_start emitter carries the wave index and incoming ids.
  • Support combatants (battle_support). A party member who joins every battle outside the party limit and acts from the sidelines as a floating portrait. They can never be targeted or damaged, so they drop out of targeting, splash, ally counts and the defeat check alike.
  • AI-driven party members (battle_ai). Hands a player-side member to the battle AI, which picks from that character's own kit. The trait is read fresh each turn, so a status carrying it takes control mid-fight and gives it back when it expires.
  • Battle roster traits (battle_ignore, battle_always). battle_ignore keeps a member off the battlefield entirely, for shopkeepers and pets who travel with you but don't fight. battle_always locks one into the picker, preselected and not toggleable.
  • Pre-battle party picker. A roster larger than the battle limit now opens a picker instead of silently taking the first N. start() returns { ok: false, reason: 'party_select_pending' } while it is up, and the Battle Config field max_party_size is now max_battle_units.
  • Unit caps (max_total_units, max_enemy_units). Two summon budgets in Battle Config: the player cap counts the whole battle including the dead, the enemy cap counts only the living, so summoners refill but never overflow. The new summonFromTemplate(templateId, side) service checks the cap before creating the character, and atUnitCap(side) exposes the same check.
  • Turn-order grace window (round_start_delay). Every round opens with a "Turn N starts" banner held for a configurable beat, default 1500 ms, with the camera pulled out. Initiative is already settled behind it, so the lineup is readable before anyone moves.
  • Multi-hit strikes (flurry). flurry: 3 hits the same target three times, re-resolving the effects only – no cost, cooldown or charge is re-paid, and armor is paid per strike. Reach for bounce instead when you want hops between random targets.
  • free_action aspect. The cast costs no action, so the caster acts again instead of ending their turn. It is per effect rather than in meta, so pairing it with chance gives a chance to act again.
  • Cast-baked stat bonuses (crit_chance, crit_multi, accuracy). Three effect aspects that add to the caster's own stats for the single cast carrying them. They fill a gap a self-applied status cannot, since a status lands after the damage loop and can only help the next cast.
  • lifesteal character stat. Lifesteal is now a stat every ability reads, so gear and passives grant sustain across a whole kit, with the effect aspect adding on top. Aspects sum across effects instead of the highest one winning.
  • reflect status and stat. A multi-stack positive status returning 1% per stack of the damage that reaches health, unmitigated and without consuming stacks. It is the mirror of thorns: reflect answers the wound, so a shield stops it, while thorns answers the blow and now fires through a full absorb.
  • Status splash (splash_statuses). Spills a percentage of the target statuses to the same neighbors splash_count reaches. It is independent of the damage share, so a pure crowd-control ability can splash with no damage at all.
  • summon_amount aspect. Spawns several copies of one template from a single summon effect, so the card reads "Summon 3 Slime". Each copy is checked against the unit cap before it is created, so an overflowing clutch spawns what fits and stops.
  • Passive obstacles (meta.prevents_action). A status whose holder forfeits every battle action while it is held, for obstacles and training dummies. They still take their turn slot, so DoT ticks, durations and turn emitters all fire.
  • battle_finished emitter. Fires the moment a battle is decided, every time including a re-fight, with (result, battleId). The Experience plugin's loot and XP now run from it, so a re-fought battle pays out where it used to pay nothing.
  • Battle service helpers. Four additions to rpg_battle: heal(targetId, amount, opts) pushes a raw number through the full heal pipeline, getSide(charId) answers player or enemy, and getNeighbors(targetId, count, includeSelf) returns the living combatants beside a target. getRoster(battleId) expands a battle into one entry per body across every wave.

Experience plugin

  • Smith station (smith action). {smith: true} opens a forge where level-stamped equipment is reforged upward or broken down for stones. New smith_upgrade / smith_break emitters veto either operation, and a smith service exposes smithableItems(), breakYield() and upgradeCost().
  • Environments (environment / environments). A new Environments config tab defines the game's own vocabulary, read by the environment dungeon trait and the environments item trait. The plugin's gather pool then draws only ingredients legal for the place.
  • Enemy dungeon-scale curve (enemy_scaling). Experience Config takes {level, coef} pins, read back through game.getService('reward').enemyScale(level). The plugin only publishes the curve – the game applies it where enemies are built, typically a stat computer.
  • Drop simulators (simulateBattleLoot, simulatePool). Two reward service calls that dry-roll loot into a throwaway inventory and grant nothing, twenty rolls by default. They run the same code a victory and a chest use, so what they report cannot drift from what the game drops.
  • Leave loot behind. Every party-loot brick on the reward panel gains a trash toggle, which marks the line and removes it again on continue. It respects item.isDroppable() and the game's item_drop_render veto, so protected kinds stay put.
  • reward-panel-bottom slot. The reward panel ends with a component slot, so a game can hang its own block below the rewards. Context arrives as props, so the component declares props: ['result'].

Turn System plugin

  • Stack bleeding (stack_bleeding). A status meta key that strips N stacks per adventure turn, oldest instance first, removing the status at 0. Reach for it where the stack count itself is the timer, so power and remaining time are one number.
  • A lot of bug fixes and improvements

v0.14.2

New Features

  • game.hasState(key). Whether a state key has been registered. getState throws on an unregistered key, so a base-game code path that reads a state a MOD registers had no safe way to ask. Now it does: game.hasState('mod_flag') && game.getState('mod_flag').
  • replay_mode_unlock_scenes state. Opens every gallery scene for replay, including ones the player never reached, and reveals their names. Defaults to false. Its existing companion replay_mode_unlock_choices ungates the choices inside a scene that is already unlocked.
  • game.closeMenu(). The counterpart openMenu() never had. Needed before opening a popup from a menu-before / menu-after slot: the menu paints above the popup layer, so a popup opened from inside the menu stays invisible until the menu closes.

v0.14.1

New Features

  • reset_character action. Rebuild a character from its template under the same id — a narratively fresh entity in the same role. Party membership is kept; statuses, resources, the private inventory and learned skills are discarded. When nothing with that id is live it creates the character instead, so one call guarantees a pristine character without the content having to know whether one exists yet.

v0.14.0

New Features

  • Inline choices (>). Attach a choice menu directly to a paragraph with >Label{...} lines — the buttons replace the normal "click to continue".
  • Hidden encounters (discover). @x{discover: "perception#6"} keeps an encounter invisible until any party member meets the stat threshold, then reveals it permanently
  • Action gates. Registered actions can now define a gate(value, ctx) that runs before a paragraph renders and before any of its actions fire; returning a scene id aborts the paragraph — its prose is skipped, its other actions never run, and the reader jumps elsewhere.
  • Choice hints (clue). Mark a choice {clue: true} and it renders highlighted until the player takes it. In map and screen dungeons the encounter holding an untaken clue glows on the map too, so players can see there's something here they haven't done.
  • Scene colour grade (grade). {grade: "night"} darkens and cools the world art — background assets, character art and the exploration map — crossfading over about a second, so daylight art can carry a night scene with no second painting. It follows the art wherever it is drawn, so a battle fought at night grades its backdrop and its fighters too. UI never grades: dialogue, choices, toolbar, and in battle the health bars, ability panel, turn order, floating damage and log all stay at full brightness. Presets cover time of day, weather and place, elemental and magical light, states of mind, and utility looks like sepia and greyscale — plus a #0.5 strength suffix and an object form for manual brightness/saturation/tint. Persists across rooms and saves until you clear it with {grade: false}.
  • Collectables. A new collectable encounter type: pick an item, place it on the map, and the player can collect it while on the map.
  • encounter_selected emitter. Fires when the player selects an encounter on the map or screen, so a script can act on the click itself with no choice to press. Return false to block the selection.
  • Dungeon name in the toolbar. The map toolbar now shows the current dungeon's name, and a spinner in its place while the dungeon's art loads.
  • Dungeon art preloads. Entering a dungeon fetches its background, fog mask and encounter images before the map is drawn, so it appears in one piece instead of popping in image by image. Each dungeon is warmed once.
  • Wait a turn (Turn System plugin). A Wait button in the map toolbar passes one turn on the spot: statuses tick, collectables regrow, and hidden encounters are re-scanned. Turn Config can show the turn counter beside it.
  • Skin-layer masks: targeting and clip-to-shape. A static layer's mask can now name which layers it applies to (mask_targets, empty = all below) and choose mask_mode: hide cuts a hole (existing behavior), keep clips the targeted layers TO the polygon — e.g. hair trimmed to fit under a hat.
  • Status-gated abilities (meta.require_status). An ability exists for a character only while they hold any of the listed statuses — hidden from the panel, sheet, battle and AI otherwise. Lives in meta, so modifiers (e.g. equipped items) can inject the gate at runtime.
  • Spine asset slot tints and removals. Spine scene assets take slot_colors (RGB multipliers × brightness per slot — values above 1 re-brighten dark base art, e.g. recoloring red-based hair) and slot_remove (hide a slot's attachment). Re-applied automatically after skin changes; scripts can inject both at play time through the asset_render emitter.
  • transfer_item action. Move items between inventories from content: {transfer_item: "gold#200 -> merchant"}. Source defaults to the party inventory and the transfer aborts cleanly when the source lacks the quantity — pair with {active: "_item_count(gold) >= 200"} for pay-to-play choices.
  • _item_count condition. How many of an item an inventory holds (party by default): _item_count(pickaxe) > 0, _item_count(chest.gold) >= 100.
  • _char(x.status.y) condition. The _char condition can now check whether a character holds a status: _char(mc.status.blessed) = 1.
  • Skin-layer visibility via attr. Simple show/hide layers no longer need a dedicated on/off attribute: when the key of an attr action is a skin layer id instead of an attribute, true/false shows or hides the layer — {attr: "mc.wings = true"}. Works in conditions too: _char(mc.attribute.wings) = true. On an id collision the attribute wins (a warning is logged at load).
  • Attribute order. Character attributes take an order field that sorts the attributes list and controls the segment order of multi-attribute skin-layer image keys (applied when the layer is saved in the editor).
  • game.setFlag(id, value). The flag setter is now on the public API next to getFlag (supports dungeon.flag scoping).
  • Battle opening statuses (RPG Battler plugin). The battle action takes a statuses list applied to every party member as the fight opens — {battle: {battleId: "bats", statuses: ["advantage"]}} for ambushes and similar setups.
  • item_view action. Show an item's card under the dialogue text: {item_view: true} for the active item (the one whose custom choice opened the scene), {item_view: "item_id"} for a party-inventory item; cleared when the event ends. Pairs with the bare |item| placeholder and no-item-id _item_on(char) condition, which now both resolve the active item.
  • max keyword for resources. Resource ops take max in place of a number, resolved per character to that resource's cap: {resource: "ane.health = max, ordelia.health = max"} is a full heal for the party. Works with every operator and on can_overflow resources, where a large literal would keep going instead of stopping at the cap.

Improvements

  • State markers +text+ / ++text++. Mark prose as the initial or altered state of a changeable thing — rendered as .initial (purple) and .altered (orange) spans, restylable from game CSS. Stray pluses like +43 health stay literal, and both markers highlight in the content editor.
  • Choice labels are now resolved. |placeholders|, **bold** and *italic* in a choice's label now render, the same as body prose.
  • Map encounters fade in and out when they show or hide, using each encounter's fadeTime / fadeOutTime.
  • Save restoration is declared, not called. game.registerSaveMigration('_core', {...}) at script-load time replaces calling the migration from a game_initiated listener — the engine merges every declaration and runs one pass itself on save load, before your listeners see the data. Mods and plugins register under their own id and the merge is restrictive, so a mod can widen coverage for its own content but a false or a skip from any source always wins. It covers every section it can touch — stats, traits, attributes, abilities, skin layers, spine views, static art placement, item slots, skill trees, learned skills, statuses and the four item sections — each taking true, false, { only: [ids] } or { skip: [ids] }. Sections nobody mentions follow mode: opt-out (the default) syncs them, opt-in skips them, so you can re-push one thing (say a recalibrated face crop, spine and static art) without disturbing the rest of the save. Item slots are repositioned as well as backfilled, so moving a slot on the doll in the editor now reaches old saves too.
  • A lot of bug fixes and inner improvements

v0.13.0

New Features

  • Item slot accepts — general vs character-specific equipment. A slot type can now list extra slot tokens it also admits, on top of its own id.
  • Item Categories — player inventory filtering. New Items > Item Categories registry (name + icon image + order) and a category field on item templates. The party inventory now shows a filter bar: a built-in All tab plus one tab per category (its icon, or the name as a chip when no icon is set) and a name search box.
  • Art Manager on items, statuses & skill slots. The visual Art Manager (spine/static art positioning, art_dx/art_dy/scale, face crop) is now available on Item Templates, Character Statuses, and Skill Slots — not just character templates.
  • Actor animation inherit. Scene actor slots now accept inherit on their enter, exit, and idle fields (the new default). When an actor moves to another slot, inherit keeps its current animation instead of resetting.
  • equip scene action. Equip and unequip items by live character and item ids: equip: "riko -> armor1 & ring1", riko -> !armor1 to unequip, riko.slot_type -> ring1 to target a slot type, !riko.slot_type to clear a slot, and !riko to clear all. Missing items/slots log a warning instead of breaking the scene. Prefer it over the uid-based equip_item/unequip_item.

Improvements

  • A lot of bug fixes and inner improvements

v0.12.0

Breaking Changes

  • **Content action {discover_lore: ...} renamed to {lore: ...}.

New Features

  • game.isOldSave() – Returns true when the loaded save was made on a previous (game + mods) version, false for new games or version-matched saves. Useful for retroactive migration hooks in game_initiated listeners (e.g. adding new slots / states / data to existing saves after a version bump).
  • [[item:id]] and [[status:id]] lore-link syntax. Narrative text now supports kind-prefixed lore links — [[item:lust_shift]] renders an item card on hover; [[status:fox_1]] renders a status card. Bare [[id]] continues to resolve to records (backwards-compatible). Custom labels ([[item:lust_shift>that pink shift]]) and the record: prefix work too. The ! discovery marker only applies to records.
  • Record parent_record field — layered, discovery-gated encyclopedia entries. A record can declare parent_record: "some_id" to attach itself as an addendum to that parent.

Improvements

  • item_slot content action — [x, y] is now optional. Coordinates default to 0, 0 when omitted. Lets text-based games (no character doll) add slots without dummy coordinates: {item_slot: "mc->outfit & collar & panties"}. The bracketed form charId.slotId[x, y] continues to work for games that position slots on a doll.
  • A lot of bug fixes and inner improvements

v0.11.0

New Features

  • RPG Battler – A lot of new features, better camera movement, difficulty settings.

Improvements

  • Game Settings – readable via getData – game.getData('game_settings') now returns the merged setting definitions keyed by id, so scripts can read a setting's values/options (e.g. to build a custom settings UI) the same way other data files are read.
  • A lot of bug fixes

Breaking Changes

  • Unified party action – join_party and leave_party are merged into a single party action. A bare character id joins, a !-prefixed id leaves: {party: "alice, !bob"}. Update any scenes still using the old action names.

v0.10.0

Breaking Changes

  • Text emphasis now follows standard markdown – *text* renders italic and **text** renders bold.

New Features

  • Multi-stack statuses – New multi_stack boolean on character_statuses. When true, each apply creates an independent instance with its own duration (DoT/poison-style). When false (default), reapply refreshes the single instance.
  • Status metadata bag – New meta field on character_statuses, populated from a new Characters → Status Meta editor tab. Plugins (and games) define their own metadata keys here (e.g. power_scaling, dot_damage_type, is_battle)
  • Per-instance source tracking – Statuses store an optional source field on each instance, captured from character.addStatus(status, { source: casterId }). Useful for reflecting damage back to the caster, attributing log entries, etc.
  • Status lifecycle emitters – status_added, status_removed, status_expired are now part of CORE_EMITTER_SIGNATURES. Listen for any status transition without polling.
  • save_load_before emitter – Fires with the raw save JSON immediately before deserialization. Listeners may mutate saveData in place to migrate old-shape data, or return false to abort the load. General-purpose primitive for game-side schema migrations.
  • Unified popover system – v-popover now drives every floating UI surface (status / item / stat / skill bricks, ability tooltips, record links, [v:status] references).
  • Stat breakdown popup – Hovering a stat now lists every status contributing to its value, grouped by the source(Base, equipped items, learned skills, other statuses).

v0.9.0

New Features

  • Save restoration – When manifest.version (or a mod's version) changes between saves, the engine can rebuild every character and every item from the current data definitions. See the new Advanced → Save Restoration doc.

Editor Improvements

  • Better Spine DX – Character/asset popups now show a Spine stats panel listing every animation and skin from the loaded skeleton.

Breaking Changes

  • Unified character canvas – Static and Spine characters now render into the same canvas shape.
  • Action consolidation – add_status, add_skin_layer/remove_skin_layer, and add_item_slot/remove_item_slot are gone. Replaced by three new content actions with a shared targetId->item & item, targetId->!item syntax (! prefix = remove):
    • {status: "alice->buff1 & buff2, bob->!debuff"}
    • {skin_layer: "alice->armor & helmet, bob->!cloak"}
    • {item_slot: "alice->ring, bob->!belt"}

v0.8.0

New Features

  • New main screen – Cinematic per-game landing page with full-bleed background asset, glassy header, accent theming, ambient soundtrack, and a Continue/New Game buttons.
  • Visual Dungeon Content Editor – New popup editor on Dungeon → Config for authoring DryadScript without touching raw text or relying on Google Docs.
  • Lore & Encyclopedia – Author lore/tutorial records once, reference them inline as hoverable [[link]] tooltips (BG3-style stacking on nested links) and as a browseable Encyclopedia tab. Supports discover-on-encounter syntax ([[!id]]), custom labels ([[id>label]]), the {discover_lore} action, and progressive reveal via auto_discovery.
  • Multiple App instances – Saves now work correctly across multiple opened instances of the app.
  • HTML editor asset picker – The Insert Image popup now lets you search and pick any project image (with thumbnails) alongside the existing URL paste, and a new Insert Video button does the same for .mp4/.webm/other video assets.

v0.7.0

New Features

  • Mobile compatibility — Phones and tablets are now supported in landscape. Portrait orientation shows a full-screen rotate-device prompt.
  • Text dungeon overhaul — New text-dungeon layouts with a side column for custom components and a refreshed scene flow.
  • a lot of bug fixes and quality of life improvements

v0.6.0

New Features

  • staticFaceForce prop on CharacterFace — Forces spine characters to use face_static image instead of live spine rendering. Use in logs, sidebar, and turn order to save performance.
  • face_static_precedence trait — Per-character opt-in to prefer face_static over spine crop in CharacterFace.
  • Spine viewport controls — Background spine assets support dx, dy, and zoom viewport adjustments in the editor.
  • is_hidden status property — Statuses can be marked hidden from the UI via the is_hidden field.
  • Context-aware string resolution — game.resolveString() accepts an optional context parameter. New |stat(statId)| placeholder reads character stats from context.
  • game.getResolveContext() — Public API for accessing resolve context in custom placeholders.
  • Create Game from Template — New wizard in the editor (New → New Game) lets you create a game from a pre-made template in seconds. Pick a template, enter a name, and you have a working game ready to edit.
  • Ability Groups — New ability_groups editor tab under Characters. Assign abilities to groups via the group meta field. The RPG Battler ability panel and character sheet ability viewer render tabs per group. If no groups are defined, abilities display as a flat list.
  • getGroupedAbilities() — New Character API method that returns abilities organized by their assigned groups.
  • Experience Plugin — New engine plugin for XP and leveling. Characters with a level trait automatically gain a managed xp resource. Configurable progression formulas (linear/exponential percentage scaling), multi-level overflow, character_level_up emitter, _level(characterId) condition, {xp: amount} action, and XP bar UI in the character sheet.
  • RPG Battler Plugin — Turn-based RPG battle system with player-controlled party combat, ability groups, splash targeting, status durations, shared cooldowns, dynamic turn order, combat stats display, battle log service, defeated battle tracking, and more. See the plugin docs for full details.

Bug Fixes

  • Fixed a bug where items could not be consumed.
  • Fixed broken character equipped item slots position
  • Fixed ability card effects without id being treated as new (golden outline) instead of core (yellow outline).

v0.5.0

Version 0.5.0

Improvements

Empty Paragraph Auto-Skip

  • Paragraphs that resolve to empty text after inline if{}/fi{} processing are now automatically skipped. The engine advances to the next paragraph without displaying a blank dialogue box.
  • This allows conditional paragraphs that produce no output (e.g., if{flag = 0}Optional text.fi{}) to be silently bypassed when the condition fails, without needing manual {redirect} workarounds.
  • Only triggers when both the output text is empty and no actions are attached to the paragraph.

Intro Scenes ({intro: true})

  • Scene paragraphs can now use {intro: true} in params to play block 1 (column 1) only on the first visit. On subsequent visits, the engine automatically redirects to block 2 (column 2).
  • Useful for first-time introductions, name reveals, or one-time exposition that should be replaced by a shorter greeting on repeat visits.

Spine Character Dolls

  • Characters can now use Spine skeletal animations as an alternative to static layered images. Configure atlas, skeleton, and default animation in the character template's Spine section.
  • CharacterDoll component auto-detects spine config and renders the Spine animation – no component changes needed for game scripts.
  • Convention-based skin mapping – character attribute values are used directly as Spine skin names. Changing an attribute reactively updates the Spine skins.
  • New animation type for the char action: {char: "mc.animation=idle"} switches the playing animation from content without code.
  • Script API: character.setSpineAnimation(name), character.isSpineCharacter(), character.getSpineSkins().

Character Views

  • New Character Views system for rendering characters from different perspectives (back, side, etc.). Define views in Characters > Views, then tag skin layers or add spine_views entries on templates/statuses.
  • View-tagged layers are excluded from default rendering (scenes, portraits, galleries) and only appear when explicitly requested via the views prop on CharacterDoll or CharacterSlot.
  • Static and Spine rendering are independent per view -- a static character can have a Spine back-view, or vice versa.
  • Script API: character.isSpineForView(view), character.getSpineForView(view), character.getImageLayersForViews(views).

Bug Fixes

  • Fixed a bug where items could not be equipped.
  • {actor: false} now properly triggers exit animations before removing characters from the scene. Previously it cleared all actors instantly without playing exit transitions.

v0.4.0

Version 0.4.0

New Features

Consumable Items

  • Added first-class consumable item support. Mark items with is_consumable to enable one-time-use effects (potions, scrolls, buff foods).
  • Consumable fields: consume_duration (status duration, -1 = permanent), consume_max_stacks (stacking cap, -1 = unlimited), consume_percentage (% of max resource), consume_absolute (flat resource amount).
  • Consume order: apply status → percentage resources → flat resources → reduce quantity.
  • Same item type produces the same status ID, enabling automatic stacking via the existing status system.
  • Added item_consume_before / item_consume_after emitters. item_consume_before is cancellable (return false to prevent consumption).
  • Added item_consume_before / item_consume_after action script slots on item templates.
  • Added inventory.reduceItemQuantity(item, amount) helper method.
  • ItemCard tooltip now displays consume effects (restore/reduce) with color coding and duration.

Custom Vue Directives

  • Added v-persist directive for keeping images in browser memory cache. Prevents decoded image data from being evicted when elements are removed from DOM (e.g., panels using v-if). Usage: <img :src="iconPath" v-persist />. Cache holds up to 600 entries with automatic FIFO eviction.
  • Added v-fit directive that auto-shrinks font size so text fits within its container. Reacts to text changes and container resizes. Usage: <div v-fit>{{ name }}</div>. Supports { min } option for minimum font size.

Trait Merge Mode

  • Added is_merge flag to character trait schema. When enabled, trait values accumulate across statuses instead of last-wins. Merge behavior is type-aware: chooseMany deduplicates, array concatenates, schema deep-merges.

Stat Display Improvements

  • Added reduction_is_good flag to character stats. When enabled, indicates that reducing the stat is beneficial (e.g., cooldowns). When disabled (default), increasing the stat is beneficial (e.g., health, damage). Used mainly for UI coloring.
  • Added color field to resource stats. Hex color (without #) used for resource bar fill.
  • Added game.registerStatGroupResolver(resolver) method. Registers a function that receives a character and returns an array of stat tag names. The engine builds groups by filtering stats matching each tag, sorts by order, and resolves display names from locale key group.{tag}. Enables per-character stat group control (e.g., show breeding stats only for MC).
  • Removed character_stat_groups state. Replaced by the stat group resolver pattern above.

Ability Description Auto-Generation

  • Added ingame_description field to ability definitions (htmlarea, aspect role only). Template for auto-generating player-facing descriptions. Supports [v] for the aspect's value and [sibling_id] for other aspect values in the same effect.
  • Added ingame_description_ref field to ability definitions. Dot-path to the display name in fromFile-referenced data (e.g., "name", "traits.name"). Falls back to name, then raw ID.
  • Added game.buildAbilityDescription(abilityId, characterId?, isFlat?) method. Returns auto-generated description lines per effect, or flat if isFlat is true.
  • Added name field to ability template effects and ability modifier effects. Optional display name returned by buildAbilityDescription.

Ability UI Components

  • Added AbilityCard component for displaying detailed ability information with icon, name, cooldown, resource costs, description, and auto-generated effects.
  • Added AbilitiesViewer component for listing and selecting character abilities.

Engine-Level Locale System

  • Added native Locale tab under General in the editor. Locale entries have id and val fields, supporting |placeholder| syntax for dynamic content.
  • Added game.getLine(lineId, params?) method. Looks up a locale entry by ID and substitutes |placeholder| tokens with provided params. Returns [lineId] if entry not found.

Plugin Script Loading

  • Breaking Change: Removed automatic script loading from plugins. Plugins must now explicitly specify scripts to load via the scripts field in plugin.json. This enables proper ES6 module usage without double-loading imported files.

Data API

  • Changed game.getData() return type to any. Most paths return a Map, but plugin single-file configs return plain objects.
  • game.drawFromPool() now returns PoolDrawResult[] ({ id: string, quantity: number }[]) instead of flat string[]. Duplicate draws are stacked by quantity, eliminating manual counting loops in callers.

Editor Schema Field Improvements

  • Added "Hide empty" checkbox to both schema and schema[] field headers in the editor. "Hide empty" hides fields with undefined, null, empty string, or empty array values. The setting is global and persists across sessions.

Template Variants (~N)

  • Templates ($template_id) now support automatic random variant selection. Add numbered variants with ~2, ~3, etc. suffixes (e.g., $greeting, $greeting~2, $greeting~3). When |$greeting| is resolved, the engine picks one at random. No special syntax needed from the caller.

Component Registration API

  • Added game.registerComponent(name, component) method. Allows plugins to register reusable Vue components to window.engine.components.
  • Added game.getComponent(name) method. Retrieves a registered component by name(you can still access them via window.engine.components).

Settings API

  • Added game.getEngineSetting(key) and game.setEngineSetting(key, value) methods. Read and write engine-level settings (music_volume, sound_volume, font_size, typing_speed). Shared across all games, persisted to browser localStorage.
  • Added game.getGameSetting(key) and game.setGameSetting(key, value) methods. Read and write per-game settings defined in game_settings.json. Persisted in save files.

Narrative System

  • Added data-driven narrative composition system. Define slots (insertion points), states (matching conditions), tags (categorized value pools), and segments (text fragments with conditions). The engine selects the best-matching segment at runtime based on live game state.
  • Slots are referenced in any text with |@slotId| syntax — resolved inline through the standard text pipeline. Slots can nest other slots for hierarchical composition.
  • States support two modes: gate (hard filter — mismatch eliminates the segment) and identity (soft preference — match increases priority, mismatch ignored). Supported types: boolean, number, range, chooseOne, chooseMany.
  • Identity states with chooseMany use per-overlap scoring — each matching tag adds specificity independently, so a segment matching 3 of 5 tags ranks higher than one matching 1.
  • Selection pipeline: collect by slot → filter by gates → score identity → keep top specificity tier → anti-repeat (last 2 per slot) → weighted random → resolve content.
  • Added game.registerNarrativeState(id, evaluator) method. Registers a function that provides runtime values for state matching.
  • Added 4 editor subtabs under Narrative: Tags, Slots, States, Segments.
  • Added fromFileType: 'values' option for chooseOne/chooseMany schema fields. Reads item.values arrays instead of item.id — enables tag-based option lists filtered by category.

Plugin Documentation System

  • Plugins can now ship their own documentation.

Built-in Plugins

  • Added Auto Battler plugin — an ATB combat system with grid positioning, token-based status effects, AI decision-making, and autocast. Includes full documentation.
  • Added Gender & POV plugin — dynamic text substitution with two subsystems: POV (1st/2nd person) for the main character, and Gender (masculine/feminine pronouns) for NPCs.

Action Naming Convention

  • Breaking Change: All built-in action names standardized to snake_case. Renamed: joinParty → join_party, leaveParty → leave_party, createCharacter → create_character, updateCharacter → update_character, deleteCharacter → delete_character, addStatus → add_status, addSkinLayer → add_skin_layer, removeSkinLayer → remove_skin_layer, addItemSlot → add_item_slot, removeItemSlot → remove_item_slot, equipItem → equip_item, unequipItem → unequip_item, consumeItem → consume_item, addItem → add_item, learnRecipe → learn_recipe, choicesOver → choices_over.

Art Manager Improvements

  • Improved character Art Manager popup with game-accurate preview. Shows game padding zone and character-sheet boundary line scaled to rendered image size.
  • Art offset (art_dx, art_dy) is now draggable directly on the character image.

v0.3.0

Version 0.3.0

New Features

HTML Editor

  • HTML editor forms now support adding local images using relative paths (e.g., assets/games_assets/my_game/_core/images/icon.webp).

Template Validation

  • Custom Vue Components are now validated at registration time. Includes tag mismatch analysis to help find mismatched HTML tags.

Character API

  • Breaking: character.getStat(name) now returns a number directly instead of a reactive ref. If you were using character.getStat("health").value, change it to character.getStat("health").
  • Added character.getStatRef(name) for when you need the reactive ComputedRef (e.g., for watch())

Inventory API

  • Breaking: inventory.getCurrencyAmount(id) renamed to inventory.getItemQuantity(id)

Data API

  • game.getData(path, original?) now returns a deep copy of the data by default, making it safe to modify without affecting the source. Pass true as the second parameter to get the original data for better performance (read-only).

Random Pools

  • Added game.drawFromPool(entry, settings?) for weighted random selection using editor-defined pool entries with filters, entity groups, and multiple selection modes.
  • Added game.drawFromCollection(collection, settings?) for simple weighted draws from any array or Map without defining pool templates.
  • For more details see 'Random Pools' documentation.

Constant Properties

  • Properties can now be marked as constant (is_constant: true) in the editor.
  • Constant properties cannot be modified at runtime and are excluded from save files.
  • Use for game constants that modders can override (e.g., base stats, multipliers, config values).

Game Events

  • room_enter_before and room_enter_after events now receive dungeonId as a second parameter.

Localhost Development

  • Added standalone development server (dev-server.cjs) for running the engine in localhost mode without Electron.
  • Run npm run dev-server to start the data API server, then npm run start for the Vite dev server.

Docs

  • Added Dungeons API documentation.
  • Updated exported components documentation with grouped categories and detailed prop descriptions.

UI

  • Improved Character Sheet UI with flexible stat groups layout and resource bars for resource-type stats.

Bug Fixes

  • Fixed choices not appearing for text-type dungeons in scene mode.
  • Plugin array data is now converted to Map format in the data registry, consistent with other data types. Use getData("path").get(id) to access items.
  • Fixed resource values not clamping to new max when a status is removed and can_overflow is false.
  • Fixed plugin schema[] fields showing numeric indexes (0, 1, 2) instead of property IDs as field labels.
  • Fixed fromFileTypeAnd and fromFileTypeOr not working for plugin-generated forms.
  • Fixed file search not matching paths with Windows-style backslashes.

v0.2.0

Version 0.2.0

Breaking Changes

Character API

  • createCharacterFromTemplate() has been removed. Use createCharacter() instead, which now accepts both template objects and template ID strings.
    // Before
    game.createCharacterFromTemplate("npc_1", "npc_template");
    
    // After
    game.createCharacter("npc_1", "npc_template");
    

Item API

  • createItemFromTemplate() has been removed. Use createItem() instead, which now accepts both template objects and template ID strings.
    // Before
    game.createItemFromTemplate("iron_sword");
    
    // After
    game.createItem("iron_sword");
    
  • createInventoryFromTemplate() has been removed. Use createInventory() with optional second parameter instead.
    // Before
    game.createInventoryFromTemplate("shop_template");
    
    // After
    game.createInventory("shop_id", "shop_template");
    

New Features

Character API

  • createStatus() accepts both template objects and template ID strings.
    // From template ID
    const buff = game.createStatus("strength_buff");
    
    // From custom object
    const custom = game.createStatus({ id: "my_buff", stats: { strength: 5 } });
    
  • addToParty(), removeFromParty(), and deleteCharacter() now accept both Character instance and ID string.
    game.addToParty("alice");       // Using ID string
    game.removeFromParty("bob");    // Using ID string
    game.deleteCharacter("npc_1");  // Using ID string
    

Changelog Viewer

Click on the engine version number in the main menu to view the changelog.

File Browser

  • Added "Clear Cache" button in file picker dropdown to refresh file list after adding new assets.
  • Added "Resources/File Browser" documentation page.

UI Improvements

  • Improved UI for the Editor Form Buttons.
  • Added key filtering for schema fields - filter input appears when schema is expanded.

Character Templates

  • added starting_statuses field to Character Templates Form for adding statuses on character creation.

Bug Fixes

File Browser

  • Fixed WebP auto-conversion triggering on typed search strings instead of only on selected files.

UI

  • Fixed UI styling issues when OS dark mode is enabled.