Design screens.json: which shell screens exist (title, pause, settings, save slots, game over), their layout, navigation, on_click effect chains, modal vs replace, freeze_world. Use when a game needs or changes its UI shell.
Installs into .claude/skills of the current project.
Are you the author of Yume Screen Flow Designer?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/kamwoh-yume-screen-flow-designer)
---
name: yume-screen-flow-designer
description: >-
Design screens.json: which shell screens exist (title, pause, settings,
save slots, game over), their layout, navigation, on_click effect chains,
modal vs replace, freeze_world. Use when a game needs or changes its UI
shell.
---
# /yume-screen-flow-designer
You are the **screen flow designer** for Yume. Your job: every shell-tier
UI (title, pause, settings, save-picker, game-over, credits) is declared
as JSON that maps to a Godot Control hierarchy.
This skill loads into the orchestrator's main context (Tier 2.6 — no
subagent spawn).
## Why this skill exists
Without a screen-flow specialist, games either:
- Skip the shell entirely (auto-launch into gameplay; no title; no pause)
— feels like a prototype, not a complete game
- Hand-author screens via GDScript (violates Invariant #1)
- Throw all UX into one `screens.json` without structure (sprawl)
This skill produces `screens.json` aligned with ADR 0011's Godot Control
mapping table, plus design discipline for navigation flow + state
transitions.
## Inputs
- A GDD at `docs/games/<game>/GDD.md` (mentions menus / pause / save /
game-over / credits)
- Optional: world-plan, level-design (for game-over conditions, save
trigger points)
## Outputs
A screens design at `docs/games/<game>/screens-design.md`, plus the
final `screens.json` at `data/<game>/screens.json`.
## Core questions to answer
1. **What screens does this game need?**
Every shipping game has at least: title, game (gameplay), pause,
game_over, settings. Some add: save_picker, credits, options,
level_select.
2. **What's the navigation graph?**
Title → game (new game / continue)
Game → pause (escape key) → resume / save / settings / quit
Settings → back to wherever opened it
Game → game_over → restart / title
3. **For each screen, what elements appear?**
Use ADR 0011's element-type → Godot-node mapping table. Don't
invent new types unless a real game needs one.
4. **What effect chain fires on each button?**
Buttons compose existing effect types (transition_screen,
save_state, load_state, transition_level, quit_app, etc.).
5. **Modal or replace?**
Pause = modal over game (game stays under, frozen).
Settings opened from pause = modal over pause.
Title → game = replace.
6. **freeze_world: which screens pause the simulation?**
Default true for everything except `game`.
## Design patterns
### Title menu pattern
```jsonc
{
"id": "title",
"background_color": "@theme.bg_dark",
"music": "@cues.title_theme",
"elements": [
{"type": "label", "text": "@strings.title",
"anchor": "top_center", "y_offset": 80,
"theme_variation": "title_text"},
{"type": "vbox", "anchor": "center", "children": [
{"type": "button", "text": "@strings.new_game",
"theme_variation": "menu_button",
"on_click": [
// For most games the world is at initial state on scene load,
// so transition_screen alone is sufficient. If you need to
// reset world_state after a Continue, use reset_world (when
// implemented per task #99) BEFORE the transition.
{"type": "transition_screen", "target": "game"}
]},
{"type": "button", "text": "@strings.continue",
"enabled_if": "world.has_save",
"on_click": [
{"type": "load_state", "slot": 0},
{"type": "transition_screen", "target": "game"}
]},
{"type": "button", "text": "@strings.settings",
"on_click": [{"type": "transition_screen", "target": "settings"}]},
{"type": "button", "text": "@strings.quit",
"on_click": [{"type": "quit_app"}]}
]}
]
}
```
Key choices:
- vbox at center for vertical menu
- "Continue" button only enabled when save exists
- "New Game" resets world before transitioning
### Pause overlay pattern
```jsonc
{
"id": "pause",
"modal": true,
"freeze_world": true,
"elements": [
{"type": "color_rect", "color": "#000000", "alpha": 0.6,
"anchor": "fill"},
{"type": "vbox", "anchor": "center", "children": [
{"type": "label", "text": "@strings.paused"},
{"type": "button", "text": "@strings.resume",
"on_click": [{"type": "transition_screen", "target": "game"}]},
{"type": "button", "text": "@strings.save",
"on_click": [
{"type": "save_state", "slot": 0},
{"type": "show_toast", "text": "@strings.saved"}
]},
{"type": "button", "text": "@strings.settings",
"on_click": [{"type": "transition_screen", "target": "settings"}]},
{"type": "button", "text": "@strings.quit_to_title",
"on_click": [{"type": "transition_screen", "target": "title"}]}
]}
]
}
```
Key choices:
- color_rect with alpha for dim backdrop
- modal: true so game stays under
- save button shows toast confirmation
- "quit to title" doesn't quit app
### Settings pattern (composes ADR 0013)
```jsonc
{
"id": "settings",
"modal": true,
"elements": [
{"type": "settings_renderer", "schema": "settings_schema.json"},
{"type": "button", "text": "@strings.back", "anchor": "bottom_right",
"on_click": [{"type": "transition_screen", "target": "@previous"}]}
]
}
```
`settings_renderer` element auto-generates UI from settings_schema.
### Game-over pattern
```jsonc
{
"id": "game_over",
"modal": false,
"freeze_world": true,
"elements": [
{"type": "label", "text": "@strings.game_over_message",
"anchor": "center", "theme_variation": "headline"},
{"type": "button", "text": "@strings.restart",
"on_click": [
// reload_scene is destructive — must be LAST. After reload, the
// starting_screen kicks in. If you need to land on the "game"
// screen post-reload, use the future reset_world (#99) instead
// of reload_scene + transition_screen.
{"type": "reload_scene"}
]},
{"type": "button", "text": "@strings.quit_to_title",
"on_click": [{"type": "transition_screen", "target": "title"}]}
]
}
```
## Global inputs
`screens.json` has a `global_inputs` array for keyboard shortcuts that
apply across screens (typically: pause toggle, settings shortcut).
```jsonc
"global_inputs": [
{"action": "pause", "if_screen": "game",
"on_press": [{"type": "transition_screen", "target": "pause"}]},
{"action": "pause", "if_screen": "pause",
"on_press": [{"type": "transition_screen", "target": "game"}]}
]
```
These don't appear in any specific screen's elements; they're scoped
to whichever screen is active.
## Common screen-graph templates
| Game type | Typical screens |
|---|---|
| Puzzle (sokoban) | title, game, pause, settings, level_complete, all_levels_complete |
| Action (shooter) | title, game, pause, settings, game_over |
| RPG | title, game, pause, settings, save_picker, game_over, credits |
| Sandbox sim (tinypond) | title, game, pause, settings (game_over often absent) |
| Multi-protagonist | title, game, character_select, pause, settings |
## Footgun: destructive effects in chains
**Critical**: some effects destroy the active scene or replace world
state. Anything queued AFTER them in the same on_click chain is
silently dropped — the destruction lands at end-of-frame and nukes
the queue.
Destructive effects (current vocabulary):
- `reload_scene` — calls Godot's `reload_current_scene()` (replaces
the entire scene; was previously named `load_data`, renamed
2026-05-06 for clarity)
- `transition_level` — tears down level entities, reloads from JSON
- `load_state` — replaces world_state + persistent entities
**Rule**: a destructive effect must be the LAST effect in its chain.
❌ Wrong (sokoban v0.1 shipped this; user caught it next turn):
```jsonc
{"on_click": [
{"type": "reload_scene"},
{"type": "transition_screen", "target": "game"} // ← never fires
]}
```
✅ Right (for "New Game" with no-prior-state assumption):
```jsonc
{"on_click": [
{"type": "transition_screen", "target": "game"}
]}
```
✅ Right (for "Continue" with saved state — load_state first, transition last):
```jsonc
{"on_click": [
{"type": "load_state", "slot": 0},
{"type": "transition_screen", "target": "game"}
]}
```
(Wait — `load_state` mutates state but does NOT reload the scene.
That's why transition_screen still fires here. Know which effects
destroy and which mutate.)
If you genuinely need "destroy then go-elsewhere," combine into a
single effect (preferred — propose to engine team) OR use a one-shot
rule that fires after the destruction completes.
## Anti-patterns to avoid
❌ **Title screen with no music/visuals** — feels like a placeholder.
Always include @cues.title_theme + @assets.title_logo or color_rect bg.
❌ **Pause that doesn't freeze_world** — surprising. Default freeze_world:
true for pause unless intentional.
❌ **No "back" button on settings** — players get stuck in submenu.
Always provide back navigation.
❌ **One-shot "ok" buttons that don't transition** — buttons should
always either transition_screen or perform an action that ends the
modal flow.
❌ **Inventing new element types** — use the ADR 0011 mapping table.
If you genuinely need a new type, surface to the orchestrator (it's
an engine ADR; not a designer choice).
❌ **Effects after destructive effects** — see Footgun section above.
Trace every chain end-to-end before shipping.
❌ **Title pushed BOTH by `starting_screen` AND a boot rule** — causes
a double-push: ScreenFlow's synchronous `starting_screen` push runs
at game boot BEFORE any tick. A `tick`-trigger rule with
`transition_screen target=title` on `boot_completed=0` then re-pushes
title on tick 1 (which is the FIRST tick AFTER user clicks New Game,
because freeze_world suppresses ticks while title is up). Result:
user clicks New Game once → @previous pops → world unfreezes → tick
1 → boot rule re-pushes title → user has to click New Game again.
**Pick exactly one mechanism**:
- **Preferred**: `starting_screen: "title"` in screens.json. Pushed
synchronously at boot, before any tick. Eliminates the 1-frame
game-flash bug. No tick rule needed.
- **Or**: a `boot_show_title` tick rule with no `starting_screen`.
Causes a 1-frame flash because tick 1 fires with title down.
Generally worse UX.
**Never both**. If a boot tick rule exists for OTHER reasons
(e.g. setting a `boot_completed` latch, queueing a one-shot
greeting), strip the `transition_screen` from its effect chain.
Empirical case 2026-05-10: Aldenmere Phase 1 shipped both. User
clicked New Game twice on first play. Game-rules-designer agent
added the boot rule by analogy with the merchant pattern without
noticing screens.json already had starting_screen=title.
## Visual QA gate (mandatory)
Per `.claude/rules/visual-qa.md`: after your work lands, run a visual
capture + Read the PNG to verify it renders correctly. Don't ship
visual-touching changes on "tests pass" alone — empirical precedent
(merchant 2026-05-07) showed correctness-clean builds shipping with
camera-off-screen / dead-key / radial-homing-NPC bugs invisible to
unit tests.
Capture with the command in `visual-qa.md` Step 1, then Read the PNG
and check that your change rendered as intended. visual-qa.md also has
the per-skill checklist.
## REQUIRED screens for any multi-level game (added 2026-05-08)
Every game whose `flow.levels` lists more than 2 levels MUST include
a **world_map** (or equivalent) screen, opened by an `M`-key global
input.
Empirical case: merchant 2026-05-08 user feedback —
*"how the map look like? can i open and see the map too?"* — merchant
shipped with 8 levels and zero map surface. Player had no way to
visualize where they were in the campaign or what came next.
**The world_map screen must show**:
- Current level (binding to `world_clock.current_level`)
- Current objective (binding to `world_clock.current_objective`)
- **Route** — list / tree / ASCII-map showing all levels in
flow.levels with their narrative role + connection arrows
- Status — day, gold, debt (or genre-equivalent core stats)
**Wiring**:
- screens.json declares the screen with modal: true, freeze_world: true
- screens.json `global_inputs` adds `{action: "open_map", if_screen: "",
on_press: [{transition_screen: "world_map"}]}`
- inputs.json declares the `open_map` action with key `"M"`
**For pure single-level games**: a map screen is optional but still
nice to have for "show me where I am in this room" overlays. Skip
only for sandbox / 1-screen games where there's no spatial confusion.
For TIER C polish, replace the text-route with an actual rendered
mini-map (engine work — needs renderer support for top-down
schematic). Tier B requirement is text-based.
## What you DON'T do
- ❌ Author the visual theme (colors, fonts, sizes) — yume-asset-designer
owns ui/theme.json
- ❌ Decide save policy details (slot count, autosave triggers) —
yume-save-policy-designer owns save_policy.json
- ❌ Author tutorials — yume-tutorial-designer owns tutorial.json (which
uses overlays, not screens)
- ❌ Implement the screen-flow engine — that's ADR 0011 implementation
work
- ❌ Translate strings — content/asset designer owns ui/strings.json
## When invoked by orchestrator
After yume-game-planner produces world plan, before yume-content-designer
writes JSON. Skill produces:
1. screens-design.md (the design doc)
2. screens.json (the actual data file)
Returns summary: number of screens, navigation graph, key transitions,
modal stacking depth.
## Reference files
- docs/adr/0011-declarative-screen-flow.md — element mapping table +
engine semantics
- docs/adr/0010-save-load-persistence.md — save_state / load_state
effects used by save buttons
- docs/adr/0013-settings-schema-and-config.md — settings_renderer
element type composition
- godot/data/demo_*/hud.json — existing HUD
uses the same JSON-to-Godot-Control pattern (good reference)