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 thecharcommand (or in their pack / slot). - Switch arrangements mid-scene with
char_layoutonce you have named profiles ingame.json.
Where this lives
| Idea | File / surface | Path |
|---|---|---|
| Slot layout | game.json | layout.characters |
| Character over/under panel | char command or pack | draw_layer / ui.draw_layer |
| Screen stacking | screens.json | role (scene / hud / modal) |
| Sprite band | sprite / obj command | layer (bg_front / fg / overlay) |
| Panel chrome slots | themes/default.json | panel.<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.
| Plane | Layer | What lives there | How you put something there |
|---|---|---|---|
bg | 0 | Background image, bg_front sprites | bg, sprite with layer: "bg_front" |
scene_ui | 0 | Screens with role: "scene" | screens.json |
chars | 0 | Character slots, fg sprites | char, sprite with layer: "fg" |
panel | 0 | The dialogue panel | panel command, node panel* fields |
above_panel | 1 | Characters lifted over the panel | char with draw_layer: "above_panel" |
overlay | 3 | overlay sprites | sprite with layer: "overlay" |
hud | 10 | Screens with role: "hud" | screens.json |
notify | 50 | Toast notifications | notifications in game.json |
modal | 75 | Screens with role: "modal" (the default) | screens.json |
cursor | 90 | The software cursor | cursors.json / cursor |
transition | 100 | Full-screen fade between scenes | transitions |
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.
target | Layer | Ends up covering |
|---|---|---|
"game" (default) | 2 | Background, scene UI, characters, panel |
"scene" | 74 | Everything below modal screens |
"all" | 99 | Everything 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:
layer | Draws above | Draws below |
|---|---|---|
"bg_front" | Background image | Characters, panel |
"fg" | Background | Panel, above_panel characters |
"overlay" | Panel and above_panel characters | HUD 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
}
}
}
}
| Key | Type | Default | Effect |
|---|---|---|---|
slot_count | int ≥ 2 | 3 | How many slots are generated |
slot_width | int ≥ 1 | base_texture_size | Slot width in pixels |
slot_height | int ≥ 1 | slot_width if width was set, else base_texture_size | Slot height in pixels |
side_margin | int ≥ 0 | 20 | Inset from the left and right window edges |
slot_gap | int or null | null | Pixels between slots. null divides the remaining width evenly. Negative values overlap slots on purpose |
snap | "bottom" | "top" | "bottom" | Which window edge slots align to |
margin | int | 0 | Distance from the snapped edge |
z_pattern | string | "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_policy | string | "nearest_center_out" | Order used when placing a character that has no position |
alias_strategy | "canonical" | "indexed" | "custom" | "canonical" | How aliases are named |
aliases | array 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_count | Aliases, left to right |
|---|---|
| 2 | left, right |
| 3 | left, center, right |
| 4 | far_left, left, right, far_right |
| 5 | far_left, left, center, right, far_right |
| 6 or more | slot_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:
| Key | Type | Effect |
|---|---|---|
slots | array | The slot rectangles |
aliases | object | Maps alias to slot id. An alias pointing at a slot id that does not exist is dropped with a warning |
auto_selection_policy | string | "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 |
transition | object | Default { "kind", "duration" } used when a char_layout command switching to this profile does not name its own |
A slot has these keys:
| Key | Type | Default | Effect |
|---|---|---|---|
id | string | required | Slot id referenced by aliases; must be unique in the profile |
x, y | number | 0 | Top-left corner, in window pixels |
width, height | number | base_texture_size | Slot size. The character’s art is composed inside this box |
z | number | 0 | Draw order within the character plane; higher is in front |
order | number | index | Tie-breaker for z, and the sequence used by auto placement and remapping |
draw_layer | "behind_panel" | "above_panel" | inherited | Overrides the plane for characters in this slot |
modulate | HTML color | #ffffff | Slot tint, multiplied with the char command tint and any pack drive tint |
scale | number or [sx, sy] | 1 | Slot scale, multiplied the same way |
Which plane a character ends up on
draw_layer is resolved in this order, first match wins:
draw_layeron thecharcommandui.draw_layerin the character’s pack JSONdraw_layeron the slot in the layout profile"behind_panel"
Placing a character without naming a slot
When a char command omits position:
- If that character is already on screen, they stay in their current slot.
- Otherwise the first free alias in
auto_selection_policyorder is used. - 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:
policy | Behaviour |
|---|---|
"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.