Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Planes and slots

Two ideas decide where things appear on screen. A render plane decides what draws on top of what. A slot is a named place on the character plane where one character stands.

Everything on one screen: planes stack from back to front, slots hold characters left to right.

Most stories

You can ignore most of this page until you need it.

  • Background at the back, characters in the middle, dialogue panel in front.
  • Default layout gives you three slots: left, center, right. Write [hana left] and you are done.
  • Put a character over the panel with draw_layer: "above_panel" on the char command (or in their pack / slot).
  • Switch arrangements mid-scene with char_layout once you have named profiles in game.json.

Where this lives

IdeaFile / surfacePath
Slot layoutgame.jsonlayout.characters
Character over/under panelchar command or packdraw_layer / ui.draw_layer
Screen stackingscreens.jsonrole (scene / hud / modal)
Sprite bandsprite / obj commandlayer (bg_front / fg / overlay)
Panel chrome slotsthemes/default.jsonpanel.<preset>.slots
{
  "layout": {
    "characters": {
      "mode": "adaptive",
      "adaptive": { "slot_count": 3, "slot_width": 600 }
    }
  }
}
{ "title": { "role": "modal" } }
{ "type": "sprite", "value": "alert", "layer": "overlay" }

Keys: Choice and character layout · Theme: panel for panel slots.

Render planes

Planes are fixed. You cannot add or renumber them; you choose which plane your content goes to.

PlaneLayerWhat lives thereHow you put something there
bg0Background image, bg_front spritesbg, sprite with layer: "bg_front"
scene_ui0Screens with role: "scene"screens.json
chars0Character slots, fg spriteschar, sprite with layer: "fg"
panel0The dialogue panelpanel command, node panel* fields
above_panel1Characters lifted over the panelchar with draw_layer: "above_panel"
overlay3overlay spritessprite with layer: "overlay"
hud10Screens with role: "hud"screens.json
notify50Toast notificationsnotifications in game.json
modal75Screens with role: "modal" (the default)screens.json
cursor90The software cursorcursors.json / cursor
transition100Full-screen fade between scenestransitions

Higher layer numbers draw on top. Four planes share layer 0, so their order comes from scene tree order instead, bottom to top: background, scene UI, characters, panel. That is why a role: "scene" screen sits behind your characters while a role: "hud" screen sits in front of everything in the story.

Where the screen shader goes

screen_shader is not on the ladder above. It moves itself based on target. Details: Shaders.

targetLayerEnds up covering
"game" (default)2Background, scene UI, characters, panel
"scene"74Everything below modal screens
"all"99Everything except the transition fade

Sprite layers

The sprite command has three layers, and they are not one contiguous band — they interleave with characters and the panel:

layerDraws aboveDraws below
"bg_front"Background imageCharacters, panel
"fg"BackgroundPanel, above_panel characters
"overlay"Panel and above_panel charactersHUD screens

Character slots

A slot is a rectangle on the chars plane that holds one character. Each slot has two names:

  • A slot id (slot_0, slot_left, …) — internal, used inside a layout profile.
  • An alias (left, center, right, …) — what you write in a story: position: "left".

You always write aliases. The alias-to-slot mapping belongs to a layout profile, and switching profiles is how you rearrange the stage mid-scene.

Profiles come from layout.characters in game.json, in one of two modes.

Adaptive mode (default)

You state how many characters can be on screen and how big they are; slot rectangles are generated to fit the window.

{
  "layout": {
    "characters": {
      "mode": "adaptive",
      "base_texture_size": 600,
      "adaptive": {
        "slot_count": 3,
        "slot_width": 520,
        "snap": "bottom",
        "margin": 0
      }
    }
  }
}
KeyTypeDefaultEffect
slot_countint ≥ 23How many slots are generated
slot_widthint ≥ 1base_texture_sizeSlot width in pixels
slot_heightint ≥ 1slot_width if width was set, else base_texture_sizeSlot height in pixels
side_marginint ≥ 020Inset from the left and right window edges
slot_gapint or nullnullPixels between slots. null divides the remaining width evenly. Negative values overlap slots on purpose
snap"bottom" | "top""bottom"Which window edge slots align to
marginint0Distance from the snapped edge
z_patternstring"center_front""center_front" gives the middle slot z: 2 and the rest z: 1. Any other value leaves every slot at z: 1
auto_selection_policystring"nearest_center_out"Order used when placing a character that has no position
alias_strategy"canonical" | "indexed" | "custom""canonical"How aliases are named
aliasesarray of strings—Required when alias_strategy is "custom"; must have exactly slot_count unique, non-empty entries

base_texture_size sits one level up, directly under layout.characters, and defaults to 600.

Canonical alias names depend on the slot count:

slot_countAliases, left to right
2left, right
3left, center, right
4far_left, left, right, far_right
5far_left, left, center, right, far_right
6 or moreslot_1 … slot_N

"indexed" always gives slot_1 … slot_N regardless of count. "custom" uses your aliases array in left-to-right order; if the array is the wrong length or has duplicates, it falls back to canonical names and warns.

Explicit mode

You place every slot by hand. Use this when characters should not be evenly spaced, when slots overlap deliberately, or when different scenes need different arrangements.

{
  "layout": {
    "characters": {
      "mode": "explicit",
      "explicit": {
        "active_profile": "default",
        "profiles": {
          "default": {
            "aliases": { "left": "slot_left", "right": "slot_right" },
            "auto_selection_policy": "nearest_center_out",
            "slots": [
              { "id": "slot_left",  "x": 0,   "y": 20, "width": 700, "height": 700, "z": 1, "order": 0 },
              { "id": "slot_right", "x": 580, "y": 20, "width": 700, "height": 700, "z": 1, "order": 1 }
            ]
          },
          "closeup": {
            "aliases": { "center": "slot_big" },
            "slots": [
              { "id": "slot_big", "x": 290, "y": 0, "width": 900, "height": 900, "z": 2, "order": 0 }
            ],
            "transition": { "kind": "fade", "duration": 0.35 }
          }
        }
      }
    }
  }
}

A profile has four keys:

KeyTypeEffect
slotsarrayThe slot rectangles
aliasesobjectMaps alias to slot id. An alias pointing at a slot id that does not exist is dropped with a warning
auto_selection_policystring"nearest_center_out" (default) orders aliases by slot order, breaking ties alphabetically. "center_priority" tries the alias literally named center first, then falls back to that same order
transitionobjectDefault { "kind", "duration" } used when a char_layout command switching to this profile does not name its own

A slot has these keys:

KeyTypeDefaultEffect
idstringrequiredSlot id referenced by aliases; must be unique in the profile
x, ynumber0Top-left corner, in window pixels
width, heightnumberbase_texture_sizeSlot size. The character’s art is composed inside this box
znumber0Draw order within the character plane; higher is in front
ordernumberindexTie-breaker for z, and the sequence used by auto placement and remapping
draw_layer"behind_panel" | "above_panel"inheritedOverrides the plane for characters in this slot
modulateHTML color#ffffffSlot tint, multiplied with the char command tint and any pack drive tint
scalenumber or [sx, sy]1Slot scale, multiplied the same way

Which plane a character ends up on

draw_layer is resolved in this order, first match wins:

  1. draw_layer on the char command
  2. ui.draw_layer in the character’s pack JSON
  3. draw_layer on the slot in the layout profile
  4. "behind_panel"

Placing a character without naming a slot

When a char command omits position:

  1. If that character is already on screen, they stay in their current slot.
  2. Otherwise the first free alias in auto_selection_policy order is used.
  3. If every alias is taken, nothing is shown and a warning is logged.

Showing a character in a slot also removes them from any other slot, instantly and without a transition, so a character can never be in two places.

Switching profiles mid-scene

The char_layout command activates a different profile.

[char_layout closeup fade 0.35]
[char_layout reset]

Characters keep their alias across the switch. If the new profile still defines that alias, the character moves to the new rectangle. If it does not, the character overflows: they are hidden with a 0.2 second fade, and a warning names the alias and character. Two characters resolving to the same slot id is also an overflow for the second one, ordered by the old slot order.

policy decides what happens when anything overflows:

policyBehaviour
"best_effort_remap" (default)Apply the new profile, hide the overflowing characters
"strict"Roll back to the previous profile and change nothing

transition decides how slots move. "instant" snaps. "fade" glides surviving slots to their new position over duration seconds, easing out. If the command omits transition, the target profile’s own transition block is used; if that is also missing, the switch is instant. A fade with no duration anywhere uses 0.3 seconds.

Only slots that exist before and after the switch glide. Characters appearing or disappearing are handled by their own char show and hide transitions.

Panel slots

The dialogue panel has its own small slot system for chrome such as a speaker portrait pinned to the panel. Those are declared in the theme under panel.<preset>.slots, not in layout.characters, and they follow the panel rather than the window. See Theme: panel. Stage props: Sprites and objects. UI screens: Title, pause and settings screens.