Skip to content

Tutorials · Interfaces · Chapter 16

A gallery of interfaces

295 minutes Uses: Canvas, Text, Grid, LayoutElement, Clip, Scroll, Scrollbar, Toggle, Slider, Dropdown, TextEntry, Focus, FocusRing, Shortcut, Modal, PopupMenu, Tooltip, TooltipStyle, CanvasGroup, RadialProgress, ContentSizeFitter, Button, Signal, floor, held, letters, pulse_by

By the end

  • Six screens in scenes, each opened from the title’s Gallery and left by Escape: a shop, a dialogue, a level select, rings and meters, key hints and quitting
  • Twenty-two new scripts, and title_menu.rgs with a fourth button
  • For each, its values in tables, what it costs, and which fields change it in your own game
On this page

The first fifteen chapters built The Delve’s interface, one control at a time. A game has screens The Delve does not, and this chapter builds six of them, each a case you will meet again: a shop, a dialogue, a level select, rings and meters, key hints, and a question asked before the game is left. Each is a scene of its own, opened from a new Gallery button on the title and left by Back or Esc. They use the controls you know, and every field of them that The Delve’s own screens left as it came.

Twenty-two new scripts run them, each short, and title_menu.rgs gains the Gallery button.

ScreenThe caseWhat carries it
A shopA merchant, an upgrade screen, a vending machine.A Grid of wares with tooltips, a field that takes only whole numbers, arrows that step it by a click, a key or a pad, a Buy that refuses what the purse cannot pay, and a Clip on a window in the room
A dialogueSomeone talking: a shopkeeper, a tutorial, a cutscene.A box that grows to its line, rich text that wraps and types itself out, a name drawn from a distance field, Next, a Speed slider and two cinema bars
A level selectA world map, a list of stages, a chapter menu.A Grid of six levels in a strip that scrolls sideways, its Scrollbar, locked levels, links that wrap round, a code typed as dots and a list that is not open yet
Rings and metersHealth round a portrait, a charged shot, an ability waiting.Three Radial Progress rings with a Start, a Direction and Segments, a Button held down, and a signal from the meter to the pie
Key hintsThe controls in a corner, which the player may hide.Shortcut labels on hints in a Stack that closes up, lit while their key is held, and tick boxes that hide them, one of them off
QuittingAsking before the game is left with work unsaved.A Modal with three answers, the first off until the notes change, over a page a Canvas Group dims and makes inert

An exported game opens the project’s startup scene, the title, and the Gallery button is on it, so the six ship with the game. In your own game, lift the one you need and give it your own art and words.

Every recipe has the same parts, in the same order:

  • What it is for: the case, in a game.
  • The steps, with a picture at each place the result changes.
  • Why: a note wherever the result is not what you would guess.
  • The values, in tables, each with the reason for it. Every field this chapter sets for the first time is defined where you set it; the others were defined in the chapter that first used them.
  • What it costs: the draws its picture takes, as the player counts them.
  • Make it yours: which field gives which result in your own game.

The pictures, the controls and the words

The screens take four more pictures from UI Pack: a star earned and a star not yet, for the level select, and two plain arrows, for the shop. The padlock a locked level wears, lock.png, came with the starter.

  1. Copy the four files below into art, each from where the list says, and press Refresh in the Assets panel.

Three new actions go in the input map: one a button is held down for, and two for the shop’s arrows.

  1. Choose View → Input map…. Type charge into New action and press Add. With charge selected, press Record a control and press C, then press Record a control again and pull a pad’s right trigger. The window names them KeyC and RightTrigger.
  2. Add shop_fewer the same way, on - and a pad’s left shoulder: Minus and LeftShoulder.
  3. Add shop_more, on = and a pad’s right shoulder: Equal and RightShoulder.
  4. Press Save. The map now holds 17 actions.

Every word on the six screens is in the string tables, as chapter 13 put the game’s. There are seventy-six new keys.

  1. Choose Language → Edit tables…. For each key below, press Add key, type the key, and type its English and its French in the two columns. Press Save.
KeyEnglishFrench
title.galleryGalleryGalerie
gallery.nameGalleryGalerie
gallery.shopA shopUne boutique
gallery.dialogueA dialogueUn dialogue
gallery.levelsA level selectChoix du niveau
gallery.ringsRings and metersAnneaux et jauges
gallery.hintsKey hintsAide des touches
gallery.quitQuittingQuitter le jeu
gallery.backBackRetour
shop.nameThe shopLa boutique
shop.waresWaresArticles
shop.windowIn the windowEn vitrine
shop.how_manyHow manyCombien
shop.fewerFewerMoins
shop.moreMorePlus
shop.buyBuyAcheter
shop.soldSold!Vendu !
shop.chooseChoose a ware.Choisissez un article.
shop.totalTotal: {total} coins.Total : {total} pièces.
shop.shortTotal: {total} coins. You have {coins}.Total : {total} pièces. Vous en avez {coins}.
shop.purseCoins: {coins}Pièces : {coins}
tip.gemGem: it sparkles, and it sells.Gemme : elle brille, et elle se vend.
dialogue.nameA dialogueUn dialogue
dialogue.speakerThe keeperLe gardien
dialogue.line_1Welcome, delver. This shrine keeps the [color=#fee761]light[/color] of every hero who walked these halls before you, and it will keep yours.Bienvenue, aventurier. Ce sanctuaire garde la [color=#fee761]lumière[/color] de chaque héros venu ici avant vous, et il gardera la vôtre.
dialogue.line_2Take what you need from the room, but [b]mind the spikes[/b] by the door: they never sleep, and they never miss.Prenez ce qu'il vous faut, mais [b]gare aux pics[/b] près de la porte : ils ne dorment jamais, et ne manquent jamais leur coup.
dialogue.line_3Go now, and [wave]may the dark be kind[/wave].Partez, et [wave]que l'ombre vous soit douce[/wave].
dialogue.nextNextSuivant
dialogue.speedSpeedVitesse
levels.nameChoose a levelChoisissez un niveau
levels.codeSecret codeCode secret
levels.code_promptType the codeTapez le code
levels.difficultyDifficultyDifficulté
levels.normalNormalNormale
levels.hardHardDifficile
levels.hard_tipFinish every level to choose Hard.Finissez chaque niveau pour choisir Difficile.
levels.chooseChoose an open level, or type the secret code.Choisissez un niveau ouvert, ou tapez le code secret.
levels.chosenLevel {n}: off you go.Niveau {n} : en route.
levels.openThe secret level is open.Le niveau secret est ouvert.
levels.wrongThat is not the code.Ce n'est pas le code.
rings.nameRings and metersAnneaux et jauges
rings.healthHealthVie
rings.chargeChargeCharge
rings.cooldownCooldownRecharge
rings.hurtHurtBlesser
rings.healHealSoigner
rings.holdHold to chargeMaintenir
rings.fillsFillsRemplit
rings.bothBoth waysDes deux côtés
rings.clockwiseClockwiseSens horaire
rings.anticlockwiseAnticlockwiseSens antihoraire
rings.secondsSecondsSecondes
rings.afterFills when the charge is let go; the meter waits until it empties.Se remplit au lâcher ; la jauge attend qu'elle se vide.
hints.nameKey hintsAide des touches
hints.bagOpen the bagOuvrir le sac
hints.journalRead the journalLire le journal
hints.useUse the first slotUtiliser la case 1
hints.backGo backRevenir
hints.show_bagBagSac
hints.show_journalJournalJournal
hints.show_useSlot 1Case 1
hints.show_backBackRetour
hints.showShowAfficher
hints.pressPress a key: its hint lights up.Appuyez sur une touche : son aide s'allume.
quit.nameQuittingQuitter le jeu
quit.notesNotesNotes
quit.notes_promptWrite a noteÉcrivez une note
quit.gameGameJeu
quit.saveSaveSauvegarder
quit.quitQuitQuitter
quit.titleQuit the game?Quitter le jeu ?
quit.bodyYour notes are not saved yet.Vos notes ne sont pas encore sauvegardées.
quit.unchangedYour notes have not changed.Vos notes n'ont pas changé.
quit.dont_saveDon't saveNe pas sauvegarder
quit.cancelCancelAnnuler
key.EnterEnterEntrée

The keeper’s three lines are written in markup, which a Text with Rich ticked reads: [color=#fee761] and [/color] colour a word, [b] and [/b] make it bold, and [wave] and [/wave] set it moving. The dialogue’s recipe says how. key.Enter renames the Enter key, which Next shows, as chapter 13’s key. keys rename the others: Entrée in French.

The gallery’s menu

The menu is a screen built as the settings screen is: a canvas that fits any window, the screen’s name across the top, a plate holding six buttons in a Stack, and Back. It starts as a copy of the settings screen.

  1. Open scenes/settings.ron and choose File → Save as…. Save it as scenes/gallery.ron. The editor now holds the copy.
  2. In the Hierarchy, rename Settings to Gallery, settings_canvas to gallery_canvas and settings_name to gallery_name. Select settings_frame and press Delete: its rows and its Back go with it.
  3. Select gallery_name. In its Rect, set Anchor Min and Anchor Max both to 0.5, 0, Offset Min to -360, 24, Offset Max to 360, 104 and Pivot to 0.5, 0: the top of the screen. Set its Text to Gallery, and its Localized Text’s Key to gallery.name. Its Scale, outline, shadow and role stay the settings name’s.
  4. Select gallery_canvas, click + Add node and choose Panel. Rename it gallery_frame. In its Rect, set Anchor Min and Anchor Max both to 0.5, 0.5, Offset Min to -200, -157 and Offset Max to 200, 237. In its Panel, set Texture to panel_brown.png and Border to 8.
  5. Select gallery_frame, click + Add node and choose Stack. Rename it gallery_menu. Its Rect stays as it comes, filling the frame. In its Stack, set Fit to Fixed, Item to 49, Spacing to 12, Padding to 20 and Align to Center, as the title’s menu is: six rows of 49, five gaps of 12 and 20 above and below make the frame’s 394.
  6. Click Add component and choose AudioSourceComponent. Untick Positional in its Spatial fold, and add two triggers, as the title’s menu has: Name click, Clip audio/click_001.ogg, and Name back, Clip audio/back_001.ogg, each with Mode Shuffle, and Pitch and Volume from 1 to 1.
  7. Select gallery_menu, click + Add node and choose Button. Rename it shop. Its Rect stays as it comes: the Stack places it. In its Panel, set Texture to buttonLong_beige.png and Border to 8. In its Text, set Text to A shop, Scale to 0.75 and Align to Center. In its Button, set Action to shop, Hovered Texture to buttonLong_grey.png, Pressed Texture to buttonLong_beige_pressed.png and Disabled Texture to buttonLong_blue.png, as the title’s buttons have.
  8. Click Add component and choose LocalizedTextComponent. Set its Key to gallery.shop.
  9. With shop selected, press Ctrl+D five times. Rename the copies, top to bottom, dialogue, levels, rings, hints and quit, and give each the Text, Action and Key the table below gives it.
  10. Select gallery_canvas, click + Add node and choose Menu Button. Rename it back. In its Rect, set Anchor Max to 0, 0, Offset Min to 24, 24, Offset Max to 274, 73 and Pivot to 0, 0: the top left corner, 250 by 49. Anchor Min stays 0, 0.
  11. Give back the faces the buttons have: Texture buttonLong_beige.png, Border 8, Text Back, Scale 0.75, Align Center, Action back, and the same three textures. Set its Shortcut’s Action to ui_cancel, so Esc and a pad’s East press it.
  12. Give back a LocalizedTextComponent with Key gallery.back, and an AudioSourceComponent with Positional unticked and one trigger, Name back, Clip audio/back_001.ogg.
  13. Give each of the six buttons and back a FocusComponent. Then link them in a ring: each one’s Down and Next are the one below it, quit’s are back and back’s are shop; each one’s Up and Previous are the one above it, shop’s are back and back’s are quit.
  14. Select gallery_canvas and press Apply theme at the foot of its Theme. It writes the palette into the ring, the name, the frame, the six buttons and Back.

Apply theme writes every colour the tables of this chapter leave out, on every screen, from the canvas’s palette: a Panel’s Color white, 255, 255, 255, alpha 255; a screen’s words the cream 236, 227, 206, and an Accent word the gold 254, 174, 52; a control’s words the plum 63, 38, 49; a Button’s, a Toggle’s and a field’s Normal and Hovered white, and their Pressed 211, 211, 211, the white sunk by the Press Sink of 0.35; a list’s lit row and a field’s Selection the light gold 254, 231, 97; a field’s Hint and a menu’s Disabled and Rule the plum at half strength, alpha 128; and the Focus Ring’s Color the gold. Press it again after any change, and every control under the canvas takes them.

The menu’s script sends each button to its screen. Each screen is a scene called gallery_ and the button’s action, so shop opens gallery_shop.

  1. In Assets, click Assets in the path bar over the tree, so the script is written at the project’s top, then press Create, choose New script, type gallery_menu, and press Enter.
  2. In the Code tab, replace everything in gallery_menu.rgs with the script below, and press Ctrl+S.
  3. Select gallery_menu, click Add component and choose GalleryMenu. Press Ctrl+S.
rgs gallery_menu.rgs
/// The gallery's menu: each button opens a small screen of its own, which
/// shows one kind of interface at work, and Back goes to the title.
component GalleryMenu {
    fn loop() {
        // A click on a button, Enter or a pad's South while it holds the
        // focus, presses its action on the game's next step. Each screen is
        // a scene of its own, named gallery_ and its button's action.
        if pressed("shop") {
            play_sound("click");
            change_scene("gallery_shop");
        }
        if pressed("dialogue") {
            play_sound("click");
            change_scene("gallery_dialogue");
        }
        if pressed("levels") {
            play_sound("click");
            change_scene("gallery_levels");
        }
        if pressed("rings") {
            play_sound("click");
            change_scene("gallery_rings");
        }
        if pressed("hints") {
            play_sound("click");
            change_scene("gallery_hints");
        }
        if pressed("quit") {
            play_sound("click");
            change_scene("gallery_quit");
        }
        // Back, or Escape or a pad's East through its Shortcut, presses back.
        if pressed("back") {
            play_sound("back");
            change_scene("title");
        }
    }
}

pressed("shop") is true for one step, the first after A Shop is pressed: by a click let go on it, or by Enter or a pad’s South while it holds the focus. Each of the other five is the same for its button. pressed("back") is true the same way for Back, and also on the step after Esc or a pad’s East, because Back’s Shortcut presses its action.

The title gets a fourth button, Gallery, between Settings and Quit.

  1. Open scenes/title.ron. Select title_frame and set its Offset Max to 150, 592: one more button of 49 and a gap of 12 taller than it was.
  2. Select settings and press Ctrl+D. The copy lands under it. Rename it gallery, and set its Text to Gallery, its Action to gallery and its Key to title.gallery.
  3. Relink the ring: settings’s Down and Next become gallery; gallery’s Up and Previous are settings, and its Down and Next are quit; quit’s Up and Previous become gallery.
  4. In the Code tab, open title_menu.rgs and replace everything in it with the script below. Press Ctrl+S.
rgs title_menu.rgs
/// The title's menu: Play opens the save slots, where a game is started or
/// carried on, Settings opens the settings screen, Gallery opens the gallery
/// of interfaces, and Quit closes the game.
component TitleMenu {
    fn loop() {
        // A button pressed by a click, by Enter while it holds the focus, or
        // by its Shortcut presses its action on the game's next step.
        if pressed("play") {
            // A sound asked for on the step that changes scene plays on after
            // it.
            play_sound("click");
            change_scene("slots");
        }
        if pressed("settings") {
            play_sound("click");
            change_scene("settings");
        }
        if pressed("gallery") {
            play_sound("click");
            change_scene("gallery");
        }
        if pressed("quit") {
            play_sound("back");
            // The game closes at the end of the step that asks, which would
            // cut the sound off, so it waits a tenth of a second first.
            await wait(0.1);
            quit();
        }
    }
}

The new lines are pressed("gallery") and what follows it: true for one step after Gallery is clicked, or after Enter or a pad’s South while it holds the focus. Press F5, and choose Gallery.

The gallery’s menu, opened from the title: Back with ESC at its right edge in the top left corner, GALLERY in gold across the top, and a brown plate holding six beige buttons one under another, A SHOP, A DIALOGUE, A LEVEL SELECT, RINGS AND METERS, KEY HINTS and QUITTING; A Shop, which holds the focus, and Key Hints, under the pointer that pressed Gallery, are drawn on the grey face
The gallery’s menu. A Shop takes the focus as the screen opens; Key Hints is lit by the pointer, which rests where Gallery was on the title.
The same menu in French: RETOUR with ÉCHAP at its right edge, GALERIE in gold, and the six buttons UNE BOUTIQUE, UN DIALOGUE, CHOIX DU NIVEAU, ANNEAUX ET JAUGES, AIDE DES TOUCHES and QUITTER LE JEU, the first holding the focus
In French. The longest of the six, ANNEAUX ET JAUGES, set the buttons’ width of 360.

The gallery’s menu, in one table

NodeValuesWhy this value
GalleryA Node, the scene’s root, with nothing on itThe settings screen’s root, renamed.
cameraScaling Letterbox, Extent 180, Width 320, Background 118, 59, 54, alpha 255The title’s camera, on the same dark rock, so every screen opens on the colour the last one closed on.
gallery_canvasScale Mode ShrinkToFit; its Theme the title’s palette: Panel and Button 255, 255, 255, alpha 255, Panel Accent 254, 231, 97, Panel Danger 232, 69, 55, Text 236, 227, 206, Text Accent 254, 174, 52, Text Danger 255, 228, 220, Button Text 63, 38, 49, each alpha 255, Hover Lift 0.15, Press Sink 0.35; a Focus Ring, Color 254, 174, 52, alpha 255 by Apply theme; and TextSize, Small 1536, 864, Normal 1280, 720, Large 1066, 600The settings canvas, renamed: chapter 6’s text size reaches every screen, and the ring shows the keys where they are.
gallery_nameAnchor Min and Anchor Max 0.5, 0, Offset Min -360, 24, Offset Max 360, 104, Pivot 0.5, 0; Text Gallery, Scale 1.5, Align Center, Outline 63, 38, 49, alpha 255, Outline Width 2, Shadow 0, 0, 0, alpha 153, Shadow Offset 3, 3, Shadow Softness 0; role Accent; Key gallery.nameThe settings name, moved to the top of the screen, where every screen of the gallery puts its own.
gallery_frameAnchor Min and Anchor Max 0.5, 0.5, Offset Min -200, -157, Offset Max 200, 237; Texture panel_brown.png, Border 8400 by 394: the buttons’ 360 and the padding, a little below the middle.
gallery_menuFit Fixed, Spacing 12, Padding 20, Item 49, Align Center; Audio Source, Positional unticked, triggers click on click_001.ogg and back on back_001.ogg, Mode Shuffle; GalleryMenuThe title’s menu, and its sounds.
shop, dialogue, levels, rings, hints, quitA Button each: Texture buttonLong_beige.png, Border 8, Scale 0.75, Align Center, Hovered Texture buttonLong_grey.png, Pressed Texture buttonLong_beige_pressed.png, Disabled Texture buttonLong_blue.png; Text A shop, A dialogue, A level select, Rings and meters, Key hints and Quitting; Action its own name; Key gallery. and its name; Focus links in a ring through backThe title’s buttons, one a screen. The action names the scene, so the script needs no list.
backA Menu Button: Anchor Max 0, 0, Offset Min 24, 24, Offset Max 274, 73, Pivot 0, 0; the buttons’ faces; Text Back, Action back; Shortcut Action ui_cancel; Key gallery.back; Audio Source, Positional unticked, trigger back on back_001.ogg; Up and Previous quit, Down and Next shopThe top left corner, where every screen keeps its Back, and Escape through the input map’s ui_cancel.

Every other colour, the buttons’ tints and their words’, came from Apply theme. On the title, title_frame’s Offset Max is 150, 592, and gallery is settings copied, with Text Gallery, Action gallery, Key title.gallery, and its links between settings and quit.

Every screen starts the same way

Each of the six screens is a copy of the gallery’s menu with the menu taken out: its camera, its canvas, its name and its Back stay. Back goes back to the gallery, not to the title, through a script of its own. Write it once, now.

  1. In Assets, click Assets in the path bar over the tree, so the script is written at the project’s top, then press Create, choose New script, type gallery_back, and press Enter. In the Code tab, replace everything in gallery_back.rgs with the script below, and press Ctrl+S.
rgs gallery_back.rgs
/// The way back from one of the gallery's screens: Back goes back to the
/// gallery.
component GalleryBack {
    fn loop() {
        // A click on Back, Enter or a pad's South while it holds the focus,
        // or Escape or a pad's East through its Shortcut, presses back on the
        // game's next step. Escape answers an open question, or shuts an open
        // list or menu, first, and goes back on the next press.
        if pressed("back") {
            // A sound asked for on the step that changes scene plays on after
            // it.
            play_sound("back");
            change_scene("gallery");
        }
    }
}

pressed("back") is true for one step after Back is clicked, after Enter or a pad’s South while it holds the focus, or after Esc or a pad’s East, through its Shortcut. An Esc that closes something first, a question or an open list, does not reach it: the next one does.

Then each screen begins with these steps. Each recipe gives the names: its scene, its root, its canvas and its name, with the name’s words and key.

  1. Open scenes/gallery.ron and choose File → Save as…. Save it under the screen’s scene name, in scenes.
  2. Rename Gallery to the screen’s root, gallery_canvas to its canvas and gallery_name to its name. Set the name’s Text and Key.
  3. Select gallery_frame and press Delete: the menu and its six buttons go with it.
  4. Select back. Remove its FocusComponent with the bin at the right of its header: its links named buttons that are gone. Click Add component and choose GalleryBack.
  5. Build the screen. Then drag back onto the canvas’s row in the Hierarchy: a node dropped onto another goes in as its last child, so Back comes last again.
  6. Select the canvas, press Apply theme, and press Ctrl+S.

A shop

What it is for. A merchant, an upgrade screen, a vending machine: wares with a price, a choice of how many, and a purchase the player can or cannot afford. It joins a Grid that fits as many fixed cells as its width holds, buttons that wear a tooltip, a field that takes only whole numbers, arrows that step it by a click, a key or a pad, and a Buy that refuses. Behind it, in the room, a window display shows a Clip on a node that is not part of the interface.

  1. Start the screen as every screen starts: the scene gallery_shop.ron, the root Shop, the canvas shop_canvas, and the name shop_name, its Text The shop and its Key shop.name.
  2. Select shop_canvas, click Add component and choose TooltipStyleComponent. Set Texture to panel_brown.png, Border to 8, Scale to 0.625 and Padding to 14, 8. Click Edge and set its alpha to 0, leaving its colour, 54, 67, 63, as it comes: no frame. Set Max Width to 260. Apply theme writes its Panel and Ink.
  3. Select shop_canvas, click + Add node and choose Localized Label. Rename it display_caption. In its Rect, set Anchor Max to 0, 0, Offset Min to 860, 128, Offset Max to 1196, 168 and Pivot to 0, 0. In its Text, set Text to In the window, Scale to 0.625 and Align to Center, and its Key to shop.window.

The wares

  1. Select shop_canvas, click + Add node and choose Panel. Rename it wares_frame. In its Rect, set Anchor Max to 0, 0, Offset Min to 48, 128, Offset Max to 348, 492 and Pivot to 0, 0. In its Panel, set Texture to panel_brown.png and Border to 8.
  2. Select wares_frame, add a Localized Label and rename it wares_title: the bag’s title, chapter 3’s. In its Rect, set Anchor Max to 1, 0, Offset Min to 16, 0, Offset Max to -16, 36 and Pivot to 0.5, 0. In its Text, set Text to Wares, Scale to 0.75, Outline to 63, 38, 49, alpha 255 and Outline Width to 1. Give it a ThemeRoleComponent with Role Accent, and set its Key to shop.wares.
  3. Select wares_frame, click + Add node and choose Grid. Rename it wares. In its Rect, set Offset Min to 0, 36: the plate below its title. In its Grid, set Fit to Cell, Cell to 132, 150, Spacing to 12, 12 and Padding to 12. Columns stays 4.
wares selected, its Inspector narrowed with the search box, grid, to its GridComponent: Fit Cell, Columns 4, Cell 132.0 and 150.0, Spacing 12.0 and 12.0, Padding 12.00; the Viewport, through the game camera, shows the shop: Back, THE SHOP, the Wares plate with four wares in two columns, Potion 2, Elixir 4, Key 6 and Gem 3, outlined as the selection with the Move gizmo at its middle, the till’s plate with HOW MANY, the two arrows, the field reading 1, CHOOSE A WARE., BUY and COINS: 5, and IN THE WINDOW over the window display with a gem, a flask and a key in it
The wares’ Grid: cells of 132 by 150, as many across as fit. Two do.
Fit
A Grid’s Fit: what decides how many columns it has. Columns, as a new Grid has it and the bag uses, makes exactly Columns columns, each as wide as the grid’s width divides into, and reads only the height of Cell. Cell makes as many columns of Cell’s width as fit, so a wider plate holds more wares, not wider ones. The wares take Cell: the plate is 300 wide, the padding takes 24, and two cells of 132 and a gap of 12 fill the 276 left.
Columns
How many columns a Grid with Fit Columns has, 1 or more; a Grid with Fit Cell ignores it. The wares leave it at 4, as it comes. The level select’s Grid sets 6, one column for each level.

Each ware is a button with no words of its own: its picture, its name and its price are three children, and a tooltip says what it does.

  1. Select wares, click + Add node and choose Button. Rename it ware_1. Its Rect stays as it comes: the Grid places it. Remove its Text: press the bin at the right of its TextComponent header.
  2. In its Panel, set Texture to buttonSquare_brown.png and Border to 8: the hotbar’s face. In its Button, set Action to ware_1 and Pressed Texture to buttonSquare_beige_pressed.png.
  3. Click Add component and choose TooltipComponent. Set Text to Potion: restores 3 health., Delay to 0.3 and Placement to Right, as chapter 8’s hotbar tooltips. Add a LocalizedTextComponent and set its Tooltip to tip.potion.
  4. Select ware_1, add a Panel and rename it ware_1_picture. In its Rect, set Anchor Min and Anchor Max both to 0.5, 0, Offset Min to -24, 14, Offset Max to 24, 62 and Pivot to 0.5, 0. In its Panel, set Texture to items.png and Atlas Region to potion: the flask, 48 units square.
  5. Select ware_1, add a Localized Label and rename it ware_1_name. In its Rect, set Anchor Max to 1, 0, Offset Min to 4, 68, Offset Max to -4, 98 and Pivot to 0.5, 0. In its Text, set Text to Potion, Scale to 0.625 and Align to Center, and its Key to item.potion.
  6. Select ware_1, add a Panel and rename it ware_1_coin. In its Rect, set Anchor Min and Anchor Max both to 0.5, 0, Offset Min to -28, 104, Offset Max to -4, 128 and Pivot to 1, 0. Set its Texture to coin.png.
  7. Select ware_1, add a Label and rename it ware_1_price. In its Rect, set Anchor Min and Anchor Max both to 0.5, 0, Offset Min to 4, 100, Offset Max to 52, 132 and Pivot to 0, 0. In its Text, set Text to 2 and Scale to 0.75. A number is a number in either language, so it has no key.

A ware’s script says which it is and what it costs, and draws the chosen one lit.

  1. In Assets, click Assets in the path bar over the tree, so the script is written at the project’s top, then press Create, choose New script, type ware, and press Enter. In the Code tab, replace everything in ware.rgs with the script below, and press Ctrl+S.
  2. Select ware_1, click Add component and choose Ware. Ware 1 and Price 2 are the potion’s, as the script gives them. Lit is the coin’s light gold, written 0.99, 0.8, 0.12, 1 in the script, and Plain is white; both stay as they come.
rgs ware.rgs
/// A ware in the shop: a click on it, or Enter or a pad's South while it
/// holds the focus, chooses it, and the till, the quantity and Buy follow the
/// choice. The chosen ware is drawn lit.
component Ware {
    /// Which ware it is: 1 the potion, 2 the elixir, 3 the key, 4 the gem.
    /// Its button presses ware_ and this number.
    ware: f32 = 1.0,
    /// What one of it costs, in coins.
    price: f32 = 2.0,
    /// The tint its face is drawn in while it is the chosen ware: the coin's
    /// light gold.
    #[color]
    lit: Vec4 = vec4(0.99, 0.8, 0.12, 1.0),
    /// The tint its face is drawn in otherwise: white, which leaves the face
    /// its own colours.
    #[color]
    plain: Vec4 = vec4(1.0, 1.0, 1.0, 1.0),

    fn loop() {
        if pressed("ware_" + text(ware)) {
            // The shop keeps which ware is chosen and what one costs, which
            // the till and Buy read.
            keep_number("shop_ware", ware);
            keep_number("shop_price", price);
        }
        // A button's face is drawn in its Normal tint while the pointer is
        // not on it and the ring is not round it.
        if kept_number("shop_ware", 0.0) == ware {
            set_colour("ButtonComponent", "normal", lit);
        } else {
            set_colour("ButtonComponent", "normal", plain);
        }
    }
}

pressed("ware_" + text(ware)) is true for one step after the ware is clicked, or after Enter or a pad’s South while it holds the focus: text(ware) makes the number 1 into the word 1, so the potion listens for ware_1. Every ware reads shop_ware each step, and the chosen one sets its Button’s Normal to its Lit gold.

  1. Select ware_1 and press Ctrl+D three times. Rename the copies ware_2, ware_3 and ware_4, and the four children of each to its own number: ware_2_picture, ware_2_name, ware_2_coin and ware_2_price, and so on. A copy’s children keep their names.
  2. Give each copy the values in the table below: its Action, its tooltip’s Text and Tooltip key, its Ware and Price, its picture’s Atlas Region, its name’s Text and Key, and its price’s Text.
WareAction, Ware, PriceTooltip Text, Tooltip keyAtlas Region, name Text and Key, price Text
ware_1ware_1, 1, 2Potion: restores 3 health., tip.potionpotion, Potion, item.potion, 2
ware_2ware_2, 2, 4Elixir: you walk faster for 5 seconds., tip.elixirelixir, Elixir, item.elixir, 4
ware_3ware_3, 3, 6Key: opens the locked door., tip.keykey, Key, item.key, 6
ware_4ware_4, 4, 3Gem: it sparkles, and it sells., tip.gemgem, Gem, item.gem, 3

The till

The till is a second plate: how many, typed into a field or stepped by an arrow either side of it; what that comes to; Buy; and the purse.

  1. Select shop_canvas, add a Panel and rename it till_frame. In its Rect, set Anchor Max to 0, 0, Offset Min to 372, 128, Offset Max to 812, 492 and Pivot to 0, 0. In its Panel, set Texture to panel_brown.png and Border to 8.
  2. Select till_frame, add a Localized Label and rename it how_many. In its Rect, set Anchor Max to 0, 0, Offset Min to 20, 20, Offset Max to 420, 52 and Pivot to 0, 0. In its Text, set Text to How many and Scale to 0.625, and its Key to shop.how_many.
  3. Select till_frame, add a Button and rename it fewer. Remove its Text, as a ware’s. In its Rect, set Anchor Max to 0, 0, Offset Min to 20, 64, Offset Max to 100, 113 and Pivot to 0, 0: 80 by 49. In its Panel, set Texture to buttonLong_beige.png and Border to 8. In its Button, set Action to fewer, tick Disabled, and set the three textures the gallery’s buttons have.
  4. Click Add component and choose FocusComponent, and set its Mode to Click. Then add a ShortcutComponent and set its Action to shop_fewer.
  5. Select fewer, add a Panel and rename it fewer_arrow. In its Rect, set Anchor Min and Anchor Max both to 0, 0.5, Offset Min to 10, -18, Offset Max to 42, 14 and Pivot to 0, 0.5. Set its Texture to arrow_basic_w.png.
  6. Select fewer, add a Localized Label and rename it fewer_label. In its Text, set Text to Fewer, Scale to 0.625 and Align to Center, and its Key to shop.fewer. Untick its Visible: it never draws. It names the arrow for a screen reader and for the Problems panel, as each save slot’s Delete has one.
  7. Select fewer and press Ctrl+D. Rename the copy more, and its children more_arrow and more_label. Set its Offset Min to 236, 64 and Offset Max to 316, 113, its Action to more, its Shortcut’s Action to shop_more, its arrow’s Texture to arrow_basic_e.png, and its label’s Text to More and Key to shop.more.
  1. Select till_frame, click + Add node and choose Text Entry. Rename it quantity. In its Rect, set Anchor Max to 0, 0, Offset Min to 108, 64, Offset Max to 228, 113 and Pivot to 0, 0. In its Panel, set Texture to input_rectangle.png and Border to 8.
  2. In its Text Entry, set Action to quantity, Text to 1, clear Placeholder, and set Max Length to 2, Mode to Integer, Padding to 12, Scale to 0.75 and Caret Width to 3. Tick Disabled.
  3. Select quantity, add a Localized Label and rename it quantity_label: Text How many, Scale 0.625, Align Center, Key shop.how_many, and Visible unticked. A field that is off has no words to be read out by.
Mode
A Text Entry’s Mode: what the field takes. Text, as a new field has it, takes any letter. Integer takes only digits, and its action’s amount answers the number typed, 0 while it is empty. Decimal takes digits and one point, and answers the same way: the rings’ Seconds field. Password takes any letter and draws each as a dot: the level select’s code. The quantity is Integer.
Disabled
A Text Entry’s Disabled: whether it is off. An off field takes no click and no keystroke, is drawn greyed, and the ring passes it by. The quantity starts off, as there is nothing to count before a ware is chosen; a script turns it on.

One script waits on the choice for all three controls, told which component to switch.

  1. Create the script waits_for_ware and replace everything in it with the script below. Give fewer and more a WaitsForWare each, its Control left at ButtonComponent, and give quantity one with Control TextEntryComponent.
rgs waits_for_ware.rgs
/// Keeps a control of the shop off until a ware is chosen: the quantity's
/// field and its two arrows, which have nothing to count before then.
component WaitsForWare {
    /// The control it switches on: ButtonComponent for an arrow,
    /// TextEntryComponent for the field.
    control: String = "ButtonComponent",

    fn loop() {
        // A ware's button keeps shop_ware, from 1, once it is chosen.
        set_flag(control, "disabled", kept_number("shop_ware", 0.0) == 0.0);
    }
}

The field and the arrows read shop_ware, which a ware keeps once it is chosen, and turn their own Disabled off with set_flag.

  1. Create the script quantity and replace everything in it with the script below. Give quantity a Quantity, Most 99.
rgs quantity.rgs
/// The shop's quantity: how many of the chosen ware to buy, a whole number
/// typed into the field, or stepped by the arrows beside it from 1 to most.
component Quantity {
    /// The most the arrows step it up to.
    most: f32 = 99.0,

    fn loop() {
        // A field in Integer mode takes only digits, and its action answers
        // the number typed in it, 0 while it is empty.
        let count = amount("quantity");
        // A click on an arrow, Enter while one holds the focus, or its
        // Shortcut, - or = or a pad's shoulders through the input map's
        // shop_fewer and shop_more, presses fewer or more on the game's next
        // step. An arrow that is off presses nothing, by its keys either. The
        // field's words are written here, and its number read back on the
        // next step.
        if pressed("fewer") && count > 1.0 {
            set_text("TextEntryComponent", "text", text(count - 1.0));
        }
        if pressed("more") && count < most {
            set_text("TextEntryComponent", "text", text(count + 1.0));
        }
    }
}

amount("quantity") is the number in the field, read as the step began: 1 as it opens, the number typed, and 0 while it is empty. pressed("fewer") and pressed("more") are true for one step after an arrow is clicked, after Enter while it holds the focus, or after its Shortcut’s key or shoulder. The script writes the new number into the field’s Text, which amount reads on the next step.

  1. Select till_frame, add a Localized Label and rename it till. In its Rect, set Anchor Max to 0, 0, Offset Min to 20, 133, Offset Max to 420, 197 and Pivot to 0, 0. In its Text, set Text to Choose a ware., Scale to 0.625, and tick Wrap: a long total runs onto a second line, as it does in French.
  2. In its Localized Text, set Key to shop.choose, and add two Substitutions: Name total and Name coins, each with Value 0.
  3. Create the script till and replace everything in it with the script below. Give till a Till.
rgs till.rgs
/// The shop's till: what the chosen ware comes to in the quantity asked for,
/// and, when the purse holds less, how much it does hold. It asks for a ware
/// while none is chosen.
component Till {
    fn loop() {
        let ware = kept_number("shop_ware", 0.0);
        let total = kept_number("shop_price", 0.0) * amount("quantity");
        // The purse keeps its coins as gallery_coins from the step the shop
        // opens on.
        let coins = kept_number("gallery_coins", 0.0);
        // Each of the till's sentences is a key in the string tables, which
        // the Localized Text beside this script writes in the game's
        // language, its substitutions total and coins filled in.
        let words = "shop.choose";
        if ware > 0.0 && total > coins {
            words = "shop.short";
        } else if ware > 0.0 {
            words = "shop.total";
        }
        set_text("LocalizedTextComponent", "key", words);
        set_text("LocalizedTextComponent", "substitutions.0.value", text(total));
        set_text("LocalizedTextComponent", "substitutions.1.value", text(coins));
    }
}

amount("quantity") is the field’s number, as in the quantity’s script. The till picks one of three keys, and its Localized Text fills {total} and {coins} in the game’s language.

  1. Select till_frame, add a Button and rename it buy. In its Rect, set Anchor Max to 0, 0, Offset Min to 20, 213, Offset Max to 240, 262 and Pivot to 0, 0. Give it the gallery buttons’ faces, Text Buy, Action buy, and tick Disabled. Add a LocalizedTextComponent with Key shop.buy, and an AudioSourceComponent, Positional unticked, with the trigger click on audio/click_001.ogg.
  2. Create the script buy_button and replace everything in it with the script below. Give buy a BuyButton, Sold For 1.
rgs buy_button.rgs
/// The shop's Buy button: it spends the chosen ware's price for each one in
/// the quantity, and refuses, greyed out, while the purse holds less than
/// that, or while there is nothing to buy. After a sale it reads Sold! for a
/// moment.
component BuyButton {
    /// How long it reads Sold! after a sale.
    #[suffix("s")]
    sold_for: f32 = 1.0,
    /// How much longer it reads Sold!, in seconds, counted down every step.
    #[hidden]
    selling: f32 = 0.0,

    fn loop(dt: f32) {
        // The quantity's field answers its number; the ware chosen keeps its
        // price, 0 while none is chosen. The purse keeps its coins as
        // gallery_coins from the step the shop opens on.
        let total = kept_number("shop_price", 0.0) * amount("quantity");
        let coins = kept_number("gallery_coins", 0.0);
        // A click, or Enter or a pad's South while it holds the focus,
        // presses buy on the game's next step. A disabled button presses
        // nothing, so a purchase the purse cannot pay for never happens.
        if pressed("buy") && total >= 1.0 && total <= coins {
            keep_number("gallery_coins", coins - total);
            play_sound("click");
            selling = sold_for;
        }
        set_flag("ButtonComponent", "disabled", total < 1.0 || total > coins);
        // dt is how long a step is, in seconds.
        if selling > 0.0 {
            selling -= dt;
            set_text("LocalizedTextComponent", "key", "shop.sold");
        } else {
            set_text("LocalizedTextComponent", "key", "shop.buy");
        }
    }
}

amount("quantity") is the field’s number. pressed("buy") is true for one step after Buy is clicked, or after Enter or a pad’s South while it holds the focus, and a Buy that is off presses nothing. dt is how long a step is, in seconds, so selling -= dt counts Sold For down in seconds at whatever step rate Project Settings sets.

  1. Select till_frame, add a Panel and rename it purse_coin. In its Rect, set Anchor Max to 0, 0, Offset Min to 20, 312, Offset Max to 52, 344 and Pivot to 0, 0. Set its Texture to coin.png.
  2. Select till_frame, add a Localized Label and rename it purse. In its Rect, set Anchor Max to 0, 0, Offset Min to 60, 312, Offset Max to 300, 344 and Pivot to 0, 0. In its Text, set Text to Coins: 5 and Scale to 0.75. Set its Key to shop.purse, with one Substitution, Name coins, Value 5.
  3. Create the script purse and replace everything in it with the script below. Give purse a Purse, Start 5.
rgs purse.rgs
/// The shop's purse: the coins the gallery's shopper has, which start at five
/// the first time the shop opens and are kept while the game runs.
component Purse {
    /// The coins in the purse the first time the shop opens.
    start: f32 = 5.0,

    fn on_create() {
        // Nothing is kept under gallery_coins before the shop first opens.
        if kept_number("gallery_coins", -1.0) < 0.0 {
            keep_number("gallery_coins", start);
        }
    }

    fn loop() {
        // The Localized Text beside this script writes the purse's words in
        // the game's language, its substitution named coins filled with the
        // count.
        let coins = kept_number("gallery_coins", start);
        set_text("LocalizedTextComponent", "substitutions.0.value", text(coins));
    }
}

The purse keeps its coins with keep_number, so they last while the game runs, through the gallery and back: a shop left and opened again still holds what was spent.

The shop with the key chosen: its ware tinted gold, the field clicked, holding 1 and its caret, TOTAL: 6 COINS. YOU HAVE 5. under it, BUY greyed out, COINS: 5 at the foot of the till, and at the right the window display, a red flask cut in half by the window’s left edge beside a gem, a blue flask and a key
The key chosen, one of it: 6 coins, and the purse holds 5. Buy is off, and the till says why.
The shop with the potion chosen and the equals key pressed twice: the field reads 3, the till TOTAL: 6 COINS. YOU HAVE 5., BUY greyed out, and the potion’s tooltip, POTION: RESTORES 3 HEALTH. on three lines, over the elixir beside it, the potion drawn plain under the pointer
The potion, and = pressed twice: 3 potions come to 6 coins, one more than the purse holds. The pointer still rests on the potion, so its tooltip is up and its face takes its Hovered tint.
The shop with the potion chosen and drawn gold, TOTAL: 2 COINS. under the field, BUY live, and the pointer on the gem, whose tooltip stands to its right on three lines: GEM: IT, SPARKLES, AND IT, SELLS.
The potion chosen, and the pointer resting on the gem: its tooltip wraps at the Tooltip Style’s Max Width, 260.
Max Width
A Tooltip Style’s Max Width: the widest a tooltip’s box may grow, in UI units, before its words wrap onto another line. 0, as it comes, keeps every tooltip on one line, however long. The shop’s is 260: at Scale 0.625 and Padding 14, a ware’s sentence takes three short lines beside the ware, and none runs off the screen.

The window display

The shop has a window in the room behind its interface, with wares on a shelf that sways past it. The window is a Clip on a node in the room, which cuts everything under it to a rectangle of its own.

  1. Select Shop, click + Add node and choose Node. Rename it window_display, and set its Position to 98, 10: the window’s sill. Click Add component and choose ClipComponent. Set Size to 84, 56 and Anchor to 0.5, 1. Enabled stays ticked.
  2. Select window_display, click + Add node and choose Sprite. Rename it display_wall, and set its Position to 0, -28, the window’s middle. Set Texture to panelInset_brown.png, Size to 126, 84 and Sort Layer to 1: the window’s back wall, larger than the window, which shows its middle.
  3. Select window_display, add a Node and rename it display_shelf. Set its Position to -48, -28.
  4. Select display_shelf, add a Sprite and rename it shelf_1. Set Texture to items.png, Atlas Region to potion, Size to 16, 16 and Sort Layer to 2. Press Ctrl+D five times, rename the copies shelf_2 to shelf_6, and set their Position and Atlas Region from the table: 24 apart, a gem, an elixir, a key, a coin and a potion.
  5. Select Shop, add a Sprite and rename it display_frame. Set its Position to 98, -18, the window’s middle, its Texture to panel_brown.png and its Size to 92, 64: the frame round the window. Its Sort Layer stays 0, behind the wall.
  6. Create the script display_shelf and replace everything in it with the script below. Give display_shelf a DisplayShelf, Sway 48, 0 and Seconds 3.
rgs display_shelf.rgs
/// The shelf behind the shop's window: it sways its wares slowly past the
/// window and back, so the window's Clip cuts them as they go by.
component DisplayShelf {
    /// How far it sways, in world units, before it comes back.
    sway: Vec2 = vec2(48.0, 0.0),
    /// How long each way takes.
    #[suffix("s")]
    seconds: f32 = 3.0,

    fn on_create() {
        // Forever, easing at each end.
        pulse_by(sway, seconds, .InOutSine);
    }
}

pulse_by moves the node by Sway and back, for ever, Seconds each way, easing at each end. The shelf’s six wares stand 24 apart, a row wider than the window’s 84, so the window cuts the wares at its edges as they pass, as the picture of the refused key shows at the window’s left.

window_display selected, its Inspector narrowed with the search box, clip, to its ClipComponent: Size 84.0 and 56.0, Anchor 0.5 and 1.0, Enabled ticked; in the Viewport the shop through the game camera, and the window at the right outlined as the selection, the Move gizmo at the middle of its foot, with a gem, a flask and a key inside it; the Viewport’s foot reads at 98.0, 10.0 size 143.0, 84.0
The window display’s Clip: 84 by 56, standing on its sill.
Size
A Clip’s Size, on a node in the room: the rectangle it cuts everything under it to, in the room’s units, the art’s pixels. 84, 56 is the window. On an interface node the Clip cuts at the node’s own rect and ignores Size, as the bag’s did in chapter 3. A new Clip’s is 128, 128.
Anchor
A Clip’s Anchor, on a node in the room: where its rectangle sits on the node, from 0, 0, the node at the rectangle’s top left, to 1, 1, at its bottom right. 0.5, 1 stands the node at the middle of the rectangle’s foot, so the window rises from its sill at 98, 10. A new Clip’s is 0.5, 0.5, centred.

Last, drag back onto shop_canvas, select shop_canvas, press Apply theme, and press Ctrl+S.

The shop, in one table

NodeValuesWhy this value
Shop, camera, shop_canvas, shop_name, backEvery screen’s, with Text The shop and Key shop.name; shop_canvas also has a Tooltip StyleThe gallery’s frame.
shop_canvas’s Tooltip StyleTexture panel_brown.png, Border 8, Edge 54, 67, 63, alpha 0, Scale 0.625, Padding 14, 8, Max Width 260; Panel and Ink by Apply themeThe HUD’s plate with no frame, and a width the tooltips wrap at.
display_captionAnchor Max 0, 0, Offset Min 860, 128, Offset Max 1196, 168, Pivot 0, 0; Text In the window, Scale 0.625, Align Center; Key shop.windowOver the window, which the room draws at the screen’s 860 to 1196 across.
wares_frameAnchor Max 0, 0, Offset Min 48, 128, Offset Max 348, 492, Pivot 0, 0; Texture panel_brown.png, Border 8A plate 300 by 364.
wares_titleAnchor Max 1, 0, Offset Min 16, 0, Offset Max -16, 36, Pivot 0.5, 0; Text Wares, Scale 0.75, Outline 63, 38, 49, alpha 255, Outline Width 1; role Accent; Key shop.waresThe bag’s title.
waresOffset Min 0, 36; Fit Cell, Cell 132, 150, Spacing 12, 12, Padding 12Two fixed cells across the plate, under its title.
ware_1 to ware_4A Button without its Text: Texture buttonSquare_brown.png, Border 8, Action ware_1 to ware_4, Pressed Texture buttonSquare_beige_pressed.png; Tooltip Delay 0.3, Placement Right, and its words; Localized Tooltip; Ware and Price, Lit and Plain as they comeThe hotbar’s slot face, so a ware reads as a thing you can take; its words in the table above.
ware_1_picture to ware_4_pictureAnchor Min and Anchor Max 0.5, 0, Offset Min -24, 14, Offset Max 24, 62, Pivot 0.5, 0; Texture items.png, Atlas Region its ownThe item, 16 pixels drawn at three times.
ware_1_name to ware_4_nameAnchor Max 1, 0, Offset Min 4, 68, Offset Max -4, 98, Pivot 0.5, 0; Scale 0.625, Align Center; Text and Key its ownUnder the picture, as wide as the ware.
ware_1_coin to ware_4_coinAnchor Min and Anchor Max 0.5, 0, Offset Min -28, 104, Offset Max -4, 128, Pivot 1, 0; Texture coin.pngA coin 24 square, left of the middle.
ware_1_price to ware_4_priceAnchor Min and Anchor Max 0.5, 0, Offset Min 4, 100, Offset Max 52, 132, Pivot 0, 0; Text its price, Scale 0.75The price, right of the coin.
till_frameAnchor Max 0, 0, Offset Min 372, 128, Offset Max 812, 492, Pivot 0, 0; Texture panel_brown.png, Border 8A second plate, 440 wide, beside the wares.
how_manyAnchor Max 0, 0, Offset Min 20, 20, Offset Max 420, 52, Pivot 0, 0; Text How many, Scale 0.625; Key shop.how_manyIts label.
fewer, moreA Button without its Text: Anchor Max 0, 0, Offset Min 20, 64 and 236, 64, Offset Max 100, 113 and 316, 113, Pivot 0, 0; Texture buttonLong_beige.png, Border 8; Action fewer and more, Disabled ticked, the three textures; Focus Mode Click; Shortcut Action shop_fewer and shop_more; WaitsForWare, Control ButtonComponent80 wide: the arrow 10 in from the left, and the Shortcut’s key clear of it at the right.
fewer_arrow, more_arrowAnchor Min and Anchor Max 0, 0.5, Offset Min 10, -18, Offset Max 42, 14, Pivot 0, 0.5; Texture arrow_basic_w.png and arrow_basic_e.pngThe arrow, 32 square.
fewer_label, more_label, quantity_labelHidden: Text Fewer, More and How many, Scale 0.625, Align Center; Key shop.fewer, shop.more and shop.how_manyA name for each wordless control.
quantityA Text Entry: Anchor Max 0, 0, Offset Min 108, 64, Offset Max 228, 113, Pivot 0, 0; Texture input_rectangle.png, Border 8; Action quantity, Text 1, Placeholder empty, Max Length 2, Mode Integer, Disabled ticked, Padding 12, Scale 0.75, Caret Width 3; Quantity, Most 99; WaitsForWare, Control TextEntryComponentWhole numbers, two digits at most: 99, as the arrows go.
tillAnchor Max 0, 0, Offset Min 20, 133, Offset Max 420, 197, Pivot 0, 0; Text Choose a ware., Scale 0.625, Wrap ticked; Key shop.choose, Substitutions total and coins; TillTwo lines’ room, for the French total.
buyA Button: Anchor Max 0, 0, Offset Min 20, 213, Offset Max 240, 262, Pivot 0, 0; the gallery buttons’ faces, Text Buy, Action buy, Disabled ticked; Key shop.buy; Audio Source, trigger click; BuyButton, Sold For 1Off until the purse can pay.
purse_coinAnchor Max 0, 0, Offset Min 20, 312, Offset Max 52, 344, Pivot 0, 0; Texture coin.pngThe coin, 32 square.
purseAnchor Max 0, 0, Offset Min 60, 312, Offset Max 300, 344, Pivot 0, 0; Text Coins: 5, Scale 0.75; Key shop.purse, Substitution coins 5; Purse, Start 5Five coins: the key, at 6, is one too many.
window_displayA Node at Position 98, 10; Clip Size 84, 56, Anchor 0.5, 1The window, standing on its sill.
display_wallA Sprite at 0, -28: Texture panelInset_brown.png, Size 126, 84, Sort Layer 1The back wall, larger than the window.
display_shelfA Node at -48, -28; DisplayShelf, Sway 48, 0, Seconds 3Sways right 48 and back, three seconds each way.
shelf_1 to shelf_6A Sprite each: Texture items.png, Size 16, 16, Sort Layer 2; at Position 0, 24, 48, 72, 96 and 120 across, Atlas Region potion, gem, elixir, key, coin and potionA shelf wider than the window, so it is always cut.
display_frameA Sprite at 98, -18: Texture panel_brown.png, Size 92, 64The frame, outside the Clip.

The camera, the canvas’s Theme, the name and Back are as the gallery’s, and every other colour came from Apply theme.

What it costs. 18 draws, and 19 while a tooltip is up. It has many pictures side by side, the plates, the wares’ faces and items, the coins, the arrows, the field and the words, and the window display behind, and each change of picture between neighbours is a draw.

A dialogue

What it is for. Someone talking: a shopkeeper, a tutorial, a cutscene. A box at the foot of the screen that grows to its line, a line that wraps, colours a word and types itself out, the speaker’s name over it, Next, and a Speed slider; and two dark bars across the whole window, top and bottom, as a film’s. It joins a canvas that ignores the phone’s safe area, a Content Size Fitter on a box whose Text wraps inside its padding, rich text, Visible Characters counted up by a script, and a name drawn from a distance field.

  1. Start the screen as every screen starts: the scene gallery_dialogue.ron, the root Dialogue, the canvas dialogue_canvas, and the name dialogue_name, its Text A dialogue and its Key dialogue.name. Set the name’s Offset Min to -360, 8 and Offset Max to 360, 88: higher than the other screens’, inside the top bar.
  2. Select Dialogue, click + Add node and choose Sprite. Rename it listener, and set its Position to -48, 8. Set Texture to tilemap_packed.png, Atlas Region to hero, Size to 32, 32 and Sort Layer to 1: the hero, at twice a tile.
  3. Add two more Sprite nodes under Dialogue the same way: keeper_top at 48, -8, Atlas Region shrine_top, and keeper_base at 48, 24, Atlas Region shrine_base. The shrine’s gargoyle over its basin is the keeper, who speaks.

The cinema bars

  1. Select Dialogue, click + Add node and choose Canvas. Rename it cinema, and drag it above dialogue_canvas in the Hierarchy: a canvas earlier in the list draws first, so the dialogue draws over the bars.
  2. In its Canvas, set Scale Mode to ShrinkToFit and untick Use Safe Area. In its Theme, type the palette dialogue_canvas has, from the gallery’s table.
  3. Select cinema, add a Panel and rename it bar_top. In its Rect, set Anchor Max to 1, 0, Offset Max to 0, 96 and Pivot to 0.5, 0: the full width, 96 high, at the top. In its Panel, set Color to 63, 38, 49, alpha 255, the plum the words are outlined in, and leave Texture empty. Give it a ThemeRoleComponent with Role Custom, so Apply theme leaves its colour alone.
  4. Select bar_top and press Ctrl+D. Rename the copy bar_bottom. Set its Anchor Min to 0, 1 and Anchor Max to 1, 1, its Offset Min to 0, -96 and Offset Max to 0, 0, and its Pivot to 0.5, 1: the same bar at the foot.
Use Safe Area
Whether the canvas is kept inside the screen’s safe area: the part of a phone’s screen that no notch, rounded corner or camera hole covers. Ticked, as a new Canvas has it and every other canvas in The Delve keeps it, a HUD’s corner is never under the notch. cinema unticks it: its bars are not words to read, and they should reach the very edges of the window, under the notch too. On a monitor the safe area is the whole window, so the two look the same there.

The box

The box is a Panel with the line on it. A Content Size Fitter grows its height to the line, and the line wraps to the box’s width.

  1. Select dialogue_canvas, add a Panel and rename it dialogue_box. In its Rect, set Anchor Min and Anchor Max both to 0.5, 1, Offset Min to -440, -160, Offset Max to 440, -40 and Pivot to 0.5, 1: 880 wide, its foot 40 above the window’s. In its Panel, set Texture to panel_brown.png and Border to 8.
  2. Click Add component and choose TextComponent. Set Text to the keeper’s first line, as dialogue.line_1 holds it in English, and Scale to 0.625. Tick Wrap and Rich, and tick Visible Characters, leaving its number at 0.
  3. Click Add component and choose ContentSizeFitter. Set Padding to 20, Min to 0, 64 and Anchor to 0.5, 1. Horizontal stays Unconstrained, Vertical Preferred, and Max 0, 0.
  4. Add a LocalizedTextComponent with Key dialogue.line_1.
dialogue_box selected, its Inspector narrowed with the search box, textcomponent, to its LocalizedTextComponent, Key dialogue.line_1, and its TextComponent: the line with its [color=#fee761]light[/color] markup, Scale 0.625, a cream Color, Align Left, Wrap, Rich and Visible Characters ticked, Visible Characters 0, Outline Width 0.00, and under Effects, Effect Mode Copies and Shadow Offset 2.0 and 2.0; the Viewport shows the hero and the shrine’s keeper between two dark bars, SPEED and its slider at the top right, and THE KEEPER and NEXT with ENTER over an empty brown box at the foot
The box’s Text. Visible Characters at 0 shows none of it: the editor does not play the script that counts it up.
The same, narrowed with fitter to its ContentSizeFitter: Horizontal Unconstrained, Vertical Preferred, Padding 20.00, Min 0.0 and 64.0, Max 0.0 and 0.0, Anchor 0.5 and 1.0, and below it the TextComponent’s Wrap row, ticked; the Viewport as before, the brown box at the foot outlined as the selection, the Move gizmo at the middle of its foot
The box’s Content Size Fitter: its height follows the line, and its foot stays put.
Wrap
Whether a Text breaks its words onto more lines, at spaces, to fit its rect’s width, or the Max width of a fitter that sizes its width. Off, as it comes, a long line runs out past the rect’s edge. The box ticks it: its line wraps at the box’s 880 less the fitter’s Padding of 20 each side, 840, onto two lines. The shop’s till and the rings’ cooldown words wrap the same way.
Rich
Whether a Text reads its words as markup. Ticked, [b], [i], [color=#rrggbb], [size=N], [wave] and [shake], each closed by its [/…], style the words between them, and the tags are not drawn; a tag it does not know is drawn as it was typed, so a mistake shows. Off, as it comes, every bracket is drawn: a quest name [REDACTED] stays as it is. The keeper’s lines colour light in #fee761, the coin’s light gold, make mind the spikes bold and set may the dark be kind moving.
Visible Characters
A tick box and a whole number: ticked, the Text shows only that many of its letters, counted after the markup is read, so a [color] tag costs no letter. Unticked, as it comes, it shows them all. The box ticks it at 0, so a line starts with nothing showing, and its script counts it up: a typewriter. A script that writes -1 unticks it.

The speaker, Next and Speed

  1. Select dialogue_box, add a Localized Label and rename it speaker. In its Rect, set Anchor Max to 0, 0, Offset Min to 20, -50, Offset Max to 340, -6 and Pivot to 0, 1: over the box’s top left corner. In its Text, set Text to The keeper, Scale to 0.75, Outline to 63, 38, 49, alpha 255 and Outline Width to 2.
  2. Under Effects, set Effect Mode to DistanceField, Shadow to 0, 0, 0, alpha 153, Shadow Offset to 3, 3 and Shadow Softness to 2. Give it a ThemeRoleComponent with Role Accent, and set its Key to dialogue.speaker.
speaker selected, its Inspector narrowed with the search box, effect, to its TextComponent’s Effects fold: Effect Mode DistanceField; in the Viewport THE KEEPER in gold over the box’s top left corner, outlined as the selection, with the Move gizmo at its bottom left corner
The speaker’s name: Effect Mode DistanceField.
Effect Mode
How a Text draws its outline and its shadow. Copies, as it comes, draws them as copies of the words, shifted, behind the words: it works with any face, and its edge is as hard or as soft as the face’s own pixels. DistanceField draws each letter once, from a distance-field copy of the face, and the outline and the shadow come out of the same picture, so they stay smooth at any size and Shadow Softness blurs the shadow’s edge. It needs the project to build that copy, below; without it the Text draws its copies as before. A Text that is Rich draws its outline and shadow as copies either way. speaker is DistanceField: one string in one colour, outlined and dropped a soft shadow.

The distance-field copy is the project’s, not the Text’s: one box in Project Settings builds it for every Text that asks.

  1. Choose File → Project settings… and open its Font tab. Tick Build an SDF atlas, beside Distance field. The paragraph under the box says what it does.
Project settings on its Font tab: Face fonts/kenney-future-narrow.ttf, Size 32 px, Distance field with Build an SDF atlas ticked, the line Found fonts/kenney-future-narrow.ttf, and the paragraph under it saying that with Distance field ticked a Text set to Distance Field that is not rich draws from a distance-field copy of the face, in the editor and in the game, and that without it that Text draws its outline and shadow as stacked copies
Build an SDF atlas, ticked.
Build an SDF atlas
Project Settings’ Font tab: also bakes a distance-field copy of the face, from the same file, at the same Size, which the editor and the game both draw from. Every Text set to DistanceField that is not Rich draws from it and stays sharp at any size; a rich Text draws its outline and shadow as copies either way, and every other label draws from the face itself. It is worth it for a name or a title that is scaled, or drawn with an outline and a soft shadow, as the keeper’s name is. It changes nothing for a plain label, which keeps the face’s hard pixels, The Delve’s look. Ticked here; chapter 1 left it unticked.
  1. Select dialogue_box, add a Menu Button and rename it next. In its Rect, set Anchor Min and Anchor Max both to 1, 0, Offset Min to -320, -55, Offset Max to -20, -6 and Pivot to 1, 1: over the box’s top right corner. Give it the gallery buttons’ faces, Text Next and Action next, and set its Shortcut’s Action to ui_accept, so Enter and a pad’s South press it wherever the ring is. Add a LocalizedTextComponent with Key dialogue.next.
  2. Select dialogue_canvas, click + Add node and choose Slider. Rename it speed. In its Rect, set Anchor Min and Anchor Max both to 1, 0, Offset Min to -288, 120, Offset Max to -48, 136 and Pivot to 1, 0: under the top bar, at the right.
  3. In its Slider, set Action to speed, Value to 30, Min to 10, Max to 60 and Step to 5. Under Textures, set Fill Texture to slide_horizontal_color.png, Track Texture to slide_horizontal_grey.png and Border to 8, as the settings’ sliders have.
  4. Select speed, add a Localized Label and rename it speed_label. In its Rect, set Anchor Max to 0, 0, Offset Min to -176, -16, Offset Max to -16, 32 and Pivot to 0, 0: hung to the slider’s left. In its Text, set Text to Speed, Scale to 0.75 and Align to Right, and its Key to dialogue.speed.
Min
A Slider’s Min: its value at its left-hand end, in the slider’s own units. A new Slider’s is 0, as the settings’ volumes keep it. The Speed slider’s is 10, letters a second, and its Max 60, so the line types no slower than ten a second and no faster than sixty, in steps of 5. Value 30 puts the knob two fifths along.

The typewriter

  1. Create the script typewriter and replace everything in it with the script below. Give dialogue_box a Typewriter: Letters Per Second 30 and Lines 3, as the script gives them.
  2. Last, drag back onto dialogue_canvas, select it, press Apply theme, and press Ctrl+S.
rgs typewriter.rgs
/// The dialogue box: it types its line out a few letters at a time, and Next,
/// or Enter, finishes a line still typing, then shows the next of its lines,
/// and the first again after the last. Each line is a key in the string
/// tables, so it types out in the game's language.
component Typewriter {
    /// How many letters appear each second, until the Speed slider is moved.
    #[suffix("/s")]
    letters_per_second: f32 = 30.0,
    /// How many lines there are: dialogue.line_1 onward.
    lines: f32 = 3.0,
    /// The line it shows, from 1.
    #[hidden]
    line: f32 = 1.0,
    /// How many letters of it have been typed, fractions included; below zero
    /// once the whole line shows.
    #[hidden]
    typed: f32 = 0.0,

    fn loop(dt: f32) {
        // The Speed slider's value, in letters a second, once it reports one.
        let pace = letters_per_second;
        if amount("speed") > 0.0 {
            pace = amount("speed");
        }
        // How long the line is, its markup aside, in the language the game
        // is in, as the step began.
        let length = letters();
        // A click on Next, Enter or a pad's South through its Shortcut,
        // presses next on the game's next step.
        if pressed("next") {
            if typed >= 0.0 {
                // Still typing: the whole line at once.
                typed = -1.0;
            } else {
                line += 1.0;
                if line > lines {
                    line = 1.0;
                }
                typed = 0.0;
                set_text("LocalizedTextComponent", "key", "dialogue.line_" + text(line));
            }
        } else if typed >= 0.0 {
            // dt is how long a step is, in seconds.
            typed += pace * dt;
            if typed >= length {
                typed = -1.0;
            }
        }
        // The field counts whole letters, so the fraction is rounded down,
        // and below zero shows them all.
        set_number("TextComponent", "visible_characters", floor(typed));
    }
}

amount("speed") is where the Speed slider stands, from 10 to 60; it reads 0 until the slider first moves, so until then the script types at its own Letters Per Second. pressed("next") is true for one step after Next is clicked, or after Enter or a pad’s South, through its Shortcut: while a line is still typing, it shows the line whole; once it is whole, it starts the next.

letters
How many letters the Text on this node shows in all, its markup left out, as the step began: 117 for the first line in English, 114 for it in French. The Localized Text writes the step’s words first, so a line has its own length in each language. It reads 0 on a node with no Text, and 0 in the editor while the game is not playing.
floor
A number rounded down to the whole number at or below it: 2.7 gives 2. The typewriter counts typed in fractions of a letter, and Visible Characters takes only whole numbers, so it writes floor(typed).
dt
The number a loop(dt: f32) is given: how long a step is, in seconds. A pace times dt keeps its speed at whatever step rate Project Settings sets: 30 letters a second is half a letter a step at 60 steps a second, and a whole letter at 30.
The dialogue two thirds of a second in: two dark bars across the top and the bottom of the window, A DIALOGUE in gold in the top one, SPEED and its slider under it at the right, the hero facing the shrine’s stone keeper, THE KEEPER in gold over the brown box’s left corner and NEXT with ENTER, drawn grey, over its right, and in the box WELCOME, DELVER. THI, the rest not typed yet
Two thirds of a second in: twenty letters typed, at 30 a second. Next holds the focus.
The same four seconds in: the line whole on two lines, WELCOME, DELVER. THIS SHRINE KEEPS THE LIGHT OF EVERY HERO WHO WALKED THESE HALLS BEFORE YOU, AND IT WILL KEEP YOURS., with LIGHT in light gold; the box the same size as before
Four seconds in: the line whole, wrapped onto its two lines, light in the coin’s light gold.

The name is drawn from the distance field. Beside the copies it drew before Build an SDF atlas was ticked, its plum outline is an even line of the same width round every letter, with rounded corners, where the copies left a ragged edge, a step of pixels at each corner; and its shadow is soft, a blurred dark haze falling right and down, where the copies drew a second hard-edged name behind it.

The dialogue in French, five seconds in: UN DIALOGUE in the top bar, VITESSE by the slider, LE GARDIEN over the box, SUIVANT with ENTRÉE, RETOUR with ÉCHAP, and the French line whole on two lines inside the box’s margins, BIENVENUE, AVENTURIER. CE SANCTUAIRE GARDE LA LUMIÈRE DE CHAQUE HÉROS VENU ICI AVANT VOUS, ET IL GARDERA LA VÔTRE., with LUMIÈRE in light gold
In French: a longer line, typed to its own last letter, and wrapped inside the same padding.

The dialogue, in one table

NodeValuesWhy this value
Dialogue, camera, dialogue_canvas, backEvery screen’sThe gallery’s frame.
dialogue_nameEvery screen’s name, with Offset Min -360, 8, Offset Max 360, 88; Text A dialogue; Key dialogue.nameInside the top bar.
listenerA Sprite at -48, 8: Texture tilemap_packed.png, Atlas Region hero, Size 32, 32, Sort Layer 1The hero, listening.
keeper_top, keeper_baseA Sprite each at 48, -8 and 48, 24: Texture tilemap_packed.png, Atlas Region shrine_top and shrine_base, Size 32, 32, Sort Layer 1The shrine’s keeper, who speaks.
cinemaA Canvas: Scale Mode ShrinkToFit, Use Safe Area unticked; its Theme the gallery’s paletteBars to the window’s edges, drawn before the dialogue.
bar_topAnchor Max 1, 0, Offset Max 0, 96, Pivot 0.5, 0; Color 63, 38, 49, alpha 255; role CustomA plum bar 96 high across the top, which keeps its colour through Apply theme.
bar_bottomAnchor Min 0, 1, Offset Min 0, -96, Pivot 0.5, 1; the same colour and roleThe same bar at the foot.
dialogue_boxAnchor Min and Anchor Max 0.5, 1, Offset Min -440, -160, Offset Max 440, -40, Pivot 0.5, 1; Texture panel_brown.png, Border 8; Text the first line, Scale 0.625, Wrap, Rich and Visible Characters 0; fitter Padding 20, Min 0, 64, Anchor 0.5, 1; Key dialogue.line_1; Typewriter, Letters Per Second 30, Lines 3Pinned by its foot, grown upward to its line, typed out from nothing.
speakerAnchor Max 0, 0, Offset Min 20, -50, Offset Max 340, -6, Pivot 0, 1; Text The keeper, Scale 0.75, Outline 63, 38, 49, alpha 255, Outline Width 2, Effect Mode DistanceField, Shadow 0, 0, 0, alpha 153, Shadow Offset 3, 3, Shadow Softness 2; role Accent; Key dialogue.speakerA name with a soft shadow, drawn from the distance field.
nextA Menu Button: Anchor Min and Anchor Max 1, 0, Offset Min -320, -55, Offset Max -20, -6, Pivot 1, 1; the gallery buttons’ faces, Text Next, Action next; Shortcut Action ui_accept; Key dialogue.nextOver the box’s right corner, and Enter through the map’s ui_accept.
speedA Slider: Anchor Min and Anchor Max 1, 0, Offset Min -288, 120, Offset Max -48, 136, Pivot 1, 0; Action speed, Value 30, Min 10, Max 60, Step 5, Fill Texture slide_horizontal_color.png, Track Texture slide_horizontal_grey.png, Border 8Ten to sixty letters a second, in fives.
speed_labelAnchor Max 0, 0, Offset Min -176, -16, Offset Max -16, 32, Pivot 0, 0; Text Speed, Scale 0.75, Align Right; Key dialogue.speedThe slider’s word, at its left.

Project Settings’ Font tab has Build an SDF atlas ticked. Every other colour came from Apply theme: the line’s cream, the name’s gold, the slider’s Fill and Track.

What it costs. 9 draws, for the room’s sprites, the bars, the box, the buttons, the slider and the words. The name’s distance-field run is a draw of its own, which the player’s count leaves out, and with its outline and its soft shadow it costs what a plain name costs. Drawn as copies, its outline and shadow would be four to eight more copies of every letter.

A level select

What it is for. A world map, a list of stages, a chapter menu: levels in a row, some open with the stars they earned, some locked, and more of them than the screen holds at once. It joins a Scroll view that scrolls sideways, its Scrollbar across, a Grid of columns inside it, buttons that are off under a padlock, Focus links that go round from the last level to the first, a field that shows dots, and a list that is not open yet.

  1. Start the screen as every screen starts: the scene gallery_levels.ron, the root Levels, the canvas levels_canvas, and the name levels_name, its Text Choose a level and its Key levels.name.
  2. Select levels_canvas, and in its Focus Ring set Width to 3 and Outset to 4: a thicker ring, a little further out, round the big level buttons.
Width
A Focus Ring’s Width: how thick the ring is, in UI units. A new ring’s, and every other screen’s, is 2. The level select’s is 3, a thicker line round its big tiles, 160 square.
Outset
A Focus Ring’s Outset: how far outside the control’s edge the ring is drawn, in UI units. A new ring’s is 2. The level select’s is 4, so the ring stands clear of a tile’s raised edge.

The strip

  1. Select levels_canvas, add a Panel and rename it strip_plate. In its Rect, set Anchor Min and Anchor Max both to 0.5, 0, Offset Min to -284, 128, Offset Max to 284, 328 and Pivot to 0.5, 0. In its Panel, set Texture to panel_brown.png and Border to 8.
  2. Select levels_canvas, click + Add node and choose Scroll View. Rename it level_strip. In its Rect, set Anchor Min and Anchor Max both to 0.5, 0, Offset Min to -276, 136, Offset Max to 276, 320 and Pivot to 0.5, 0: 552 wide, 8 inside the plate, three levels in view.
  3. In its Scroll, set Content to 1104, 0 and Wheel to 184. Give it a FocusComponent with Mode None: the ring walks its levels, and the view brings the one it lands on into view.
  4. Select level_strip, add a Grid and rename it levels. Its Rect stays as it comes. In its Grid, set Columns to 6, Cell to 0, 160, Spacing to 24, 24 and Padding to 12. Fit stays Columns.

Six levels

  1. Select levels, click + Add node and choose Menu Button. Rename it level_1. Its Rect stays as it comes: the Grid places it. In its Panel, set Texture to buttonLong_beige.png and Border to 8. In its Text, set Text to 1, Scale to 1.25 and Align to Center. In its Button, set Action to level_1 and the three textures the gallery’s buttons have.
  2. In its Shortcut, leave Action empty, set Key to Digit1 and untick Show Label. Give it a FocusComponent with Down code and Left level_6.
  3. Select level_1, add a Stack and rename it level_1_stars. In its Rect, set Anchor Min to 0, 1, Offset Min to 0, -48, Offset Max to 0, -14 and Pivot to 0.5, 1: a band along the tile’s foot. Anchor Max stays 1, 1. In its Stack, set Axis to Horizontal, Fit to Fixed, Spacing to 4, Item to 32 and Align to Center.
  4. Select level_1_stars, add a Panel and rename it level_1_star_1. Its Rect stays as it comes. Set its Texture to star.png. Press Ctrl+D twice, and rename the copies level_1_star_2 and level_1_star_3.
  5. Create the script level_tile and replace everything in it with the script below. Give level_1 a LevelTile, Level 1, Secret unticked.
rgs level_tile.rgs
/// A level on the level select: a click on it, Enter or a pad's South while it
/// holds the focus, or its number's key chooses it, and the line under the
/// strip says so. A locked level is a disabled button, which none of those
/// press. The secret level opens once the secret code is typed.
component LevelTile {
    /// Which level it is: 1 to 6. Its button presses level_ and this number.
    level: f32 = 1.0,
    /// Whether the secret code opens it.
    secret: bool = false,

    fn loop() {
        // The code's field keeps secret_open as 1 once the code is typed.
        if secret && kept_number("secret_open", 0.0) == 1.0 {
            set_flag("ButtonComponent", "disabled", false);
        }
        if pressed("level_" + text(level)) {
            // What the line under the strip says: the level chosen.
            keep_number("levels_said", level);
        }
    }
}

pressed("level_" + text(level)) is true for one step after the level is clicked, after Enter or a pad’s South while it holds the focus, or after its number’s key, through its Shortcut. A level that is off presses nothing, by any of them.

  1. Select level_1 and press Ctrl+D five times. Rename the copies level_2 to level_6, and the stars under each to its own number, level_2_stars, level_2_star_1 and so on. Give each its own Text, Action, Key and Level from the table below.
  2. On level_2, set level_2_star_3’s Texture to star_outline.png: two stars earned. On level_3, set level_3_star_1, level_3_star_2 and level_3_star_3 to star_outline.png: none yet.
  3. On level_4 and level_5, delete the stars, and tick the Button’s Disabled. Add a Panel under each, named level_4_lock and level_5_lock: Anchor Min and Anchor Max both 0.5, 0, Offset Min -24, 16, Offset Max 24, 64, Pivot 0.5, 0, Texture lock.png, which came with the starter.
  4. On level_6, tick Disabled, set its three stars to star_outline.png, untick level_6_stars’ Visible, and add a padlock, level_6_lock, as on level_4. Tick its LevelTile’s Secret, set its Focus’s Right to level_1, and clear the Left it took from level_1.
  5. On level_2 to level_5, clear the Left the copy took from level_1, so their links left and right are worked out from where they stand.
LevelText, Action, Key, LevelUnder itIts Button and its links
level_11, level_1, Digit1, 1Three stars, star.pngLeft level_6, Down code
level_22, level_2, Digit2, 2Two star.png and one star_outline.pngDown code
level_33, level_3, Digit3, 3Three star_outline.pngDown code
level_44, level_4, Digit4, 4The padlockDisabled; Down code
level_55, level_5, Digit5, 5The padlockDisabled; Down code
level_66, level_6, Digit6, 6, Secret tickedThree star_outline.png, hidden, and the padlockDisabled; Right level_1, Down code
Left, Right
A Focus’s Left and Right: the control the ring moves to when the left or the right arrow is pressed, or a pad’s stick or D-pad is pushed that way, while this one holds the focus. Empty, as they come, the ring finds the nearest control that way. level_1’s Left is level_6 and level_6’s Right is level_1, so the row goes round. A link to a level that is off passes on, as the ring passes every control that is off: from level 3, right goes past the locked 4 and 5 to 6 once it is open, and stays on 3 until then.

The secret level’s stars and padlock swap when the code is typed. One script does both, told which way round.

  1. Create the script shown_by_secret and replace everything in it with the script below. Give level_6_stars a ShownBySecret with Once Open ticked, as it comes, and level_6_lock one with Once Open unticked.
rgs shown_by_secret.rgs
/// Shows a part of the secret level's button only before the secret code is
/// typed, as its padlock, or only after, as its stars.
component ShownBySecret {
    /// Whether it shows once the level is open, rather than until then.
    once_open: bool = true,

    fn loop() {
        // The code's field keeps secret_open as 1 once the code is typed.
        let open = kept_number("secret_open", 0.0) == 1.0;
        if open == once_open {
            show();
        } else {
            hide();
        }
    }
}

The bar under the strip is the settings’ slider turned into a scrollbar: its grey track is the bar, and its gold fill the thumb.

  1. Select levels_canvas, click + Add node and choose Scrollbar. Rename it level_bar. In its Rect, set Anchor Min and Anchor Max both to 0.5, 0, Offset Min to -276, 336, Offset Max to 276, 352 and Pivot to 0.5, 0: as wide as the strip, under it. In its Panel, set Texture to slide_horizontal_grey.png and Border to 8.
  2. In its Scrollbar, set View to level_strip, Axis to Horizontal, Min Thumb to 16 and Thumb to 255, 255, 255, alpha 255. Under Textures, set Thumb Texture to slide_horizontal_color.png and Border to 8.
  3. Give it a FocusComponent with Mode None: the bar is the pointer’s, and the keys scroll the strip by walking its levels.
level_bar selected, its Inspector narrowed with the search box, scrollbar, to its ScrollbarComponent: View level_strip, Axis Horizontal, Min Thumb 16.00, a white Thumb, and under Textures, Thumb Texture slide_horizo… and Border 8.00; the Viewport shows CHOOSE A LEVEL over the brown plate with levels 1, 2 and 3 in view, three, two and no gold stars, the bar under them outlined as the selection with the Move gizmo at the middle of its top edge, SECRET CODE beside a field reading TYPE THE CODE, DIFFICULTY beside a list reading NORMAL, and CHOOSE AN OPEN LEVEL, OR TYPE THE SECRET CODE. along the foot
The strip’s bar: Axis Horizontal.
Axis
A Scrollbar’s Axis: which way the bar runs, and so which way of its view it drives. Vertical, as it comes, for a list such as the journal; Horizontal for a strip. The level bar is Horizontal: its thumb runs across, as long as the share of the strip in view, half of it, and dragging it scrolls the strip sideways.

The code, the list and the line

  1. Select levels_canvas, click + Add node and choose Text Entry. Rename it code. In its Rect, set Anchor Min and Anchor Max both to 0.5, 0, Offset Min to 16, 384, Offset Max to 316, 433 and Pivot to 0, 0. In its Panel, set Texture to input_rectangle.png and Border to 8.
  2. In its Text Entry, set Action to code, Placeholder to Type the code, Max Length to 8, Mode to Password, Padding to 12, Scale to 0.75 and Caret Width to 3. Set its Focus’s Up to level_1.
  3. Add a LocalizedTextComponent with Placeholder levels.code_prompt.
  4. Select code, add a Localized Label and rename it code_label. In its Rect, set Anchor Max to 0, 0, Offset Min to -316, 0, Offset Max to -16, 49 and Pivot to 0, 0: hung to the field’s left. In its Text, set Text to Secret code, Scale to 0.75 and Align to Right, and its Key to levels.code.
  5. Create the script secret_code and replace everything in it with the script below. Give code a SecretCode, its Code delve, as the script gives it.
rgs secret_code.rgs
/// The level select's secret code: Enter in the field checks what was typed,
/// and the right code opens the secret level for as long as the game runs.
component SecretCode {
    /// The code that opens the secret level.
    code: String = "delve",

    fn loop() {
        // Enter in the field presses its action, code, and entered() reads
        // what it holds, which its Password mode shows as dots.
        if pressed("code") {
            if entered("code") == code {
                keep_number("secret_open", 1.0);
                // What the line under the strip says: 7 the level is open.
                keep_number("levels_said", 7.0);
            } else {
                // 8: that was not the code.
                keep_number("levels_said", 8.0);
            }
        }
    }
}

pressed("code") is true for one step after Enter is pressed in the field. entered("code") is what the field holds, as the step began: the letters typed, though the field draws each as a dot.

  1. Select levels_canvas, click + Add node and choose Dropdown. Rename it difficulty. In its Rect, set Anchor Min and Anchor Max both to 0.5, 0, Offset Min to 16, 449, Offset Max to 316, 489 and Pivot to 0, 0. In its Panel, set Texture to input_rectangle.png and Border to 8. In its Text, set Text to Difficulty, Scale to 0.75 and Align to Center.
  2. In its Dropdown, set Action to difficulty, add two Options, Normal and Hard, set Item Height to 40, and tick Disabled.
  3. Give it a TooltipComponent: Text Finish every level to choose Hard., Delay 0.3, Placement Right. Add a LocalizedTextComponent with Key levels.difficulty, Tooltip levels.hard_tip and two Options, levels.normal and levels.hard.
  4. Select difficulty, add a Panel and rename it difficulty_arrow: Anchor Min and Anchor Max 1, 0.5, Offset Min -46, -11, Offset Max -12, 11, Pivot 1, 0.5, Texture icon_arrow_down_dark.png, as the settings’ lists have. Add a Localized Label, difficulty_label: Anchor Max 0, 0, Offset Min -316, -4, Offset Max -16, 44, Pivot 0, 0, Text Difficulty, Scale 0.75, Align Right, Key levels.difficulty.
Disabled
A Dropdown’s Disabled: whether the list is off. An off list does not open, by a click, a key or a pad, is drawn greyed, and the ring passes it by; its closed face still shows its choice, Normal. A new Dropdown is on. The difficulty is off, as Hard is not the player’s yet, and its tooltip says what opens it. A script turns it on with set_flag("DropdownComponent", "disabled", false).
  1. Select levels_canvas, add a Localized Label and rename it levels_said. In its Rect, set Anchor Min and Anchor Max both to 0.5, 0, Offset Min to -500, 517, Offset Max to 500, 557 and Pivot to 0.5, 0. In its Text, set Text to Choose an open level, or type the secret code., Scale to 0.75 and Align to Center. Set its Key to levels.choose, with one Substitution, Name n, Value 1.
  2. Create the script level_said and replace everything in it with the script below. Give levels_said a LevelSaid.
  3. Last, drag back onto levels_canvas, select it, press Apply theme, and press Ctrl+S.
rgs level_said.rgs
/// The line under the level select: what to do, which level was chosen, or
/// what the secret code did, whichever happened last.
component LevelSaid {
    fn loop() {
        // A level keeps levels_said as its number, 1 to 6, and the code's
        // field as 7 when it opens the secret level and 8 when it does not.
        let said = kept_number("levels_said", 0.0);
        let words = "levels.choose";
        if said >= 1.0 && said <= 6.0 {
            words = "levels.chosen";
        } else if said == 7.0 {
            words = "levels.open";
        } else if said == 8.0 {
            words = "levels.wrong";
        }
        // The Localized Text beside this script writes the key's words, its
        // substitution named n filled with the level's number.
        set_text("LocalizedTextComponent", "key", words);
        set_text("LocalizedTextComponent", "substitutions.0.value", text(said));
    }
}

The line reads levels_said, which the levels and the code keep, and fills {n} with the level’s number in the game’s language.

The level select as it opens: Back, CHOOSE A LEVEL in gold, the brown plate with levels 1, 2 and 3 in view, 1 holding the focus on the grey face with three silver stars, 2 with two silver stars and one outlined, 3 with three outlined, the bar under them with its gold thumb along the left half, SECRET CODE beside a field showing TYPE THE CODE, DIFFICULTY beside a greyed list reading NORMAL, and CHOOSE AN OPEN LEVEL, OR TYPE THE SECRET CODE. along the foot
As it opens: the first level holds the focus, and three of six are in view.
The level select after the code was typed and entered and level 3 clicked: the field shows five dots, the line reads LEVEL 3: OFF YOU GO., and the strip is scrolled to its far end, levels 4 and 5 greyed under their padlocks and level 6 open, three outlined stars on it, ringed in gold, the bar’s thumb now along the right half
The code typed, as dots, and entered; level 3 clicked, and the right arrow pressed once: the ring passes the locked 4 and 5 to level 6, and the strip scrolls to show it. A second press goes round to level 1.

The level select, in one table

NodeValuesWhy this value
Levels, camera, levels_name, backEvery screen’s, with Text Choose a level and Key levels.nameThe gallery’s frame.
levels_canvasEvery screen’s, with Focus Ring Width 3 and Outset 4A ring that reads round a big tile.
strip_plateAnchor Min and Anchor Max 0.5, 0, Offset Min -284, 128, Offset Max 284, 328, Pivot 0.5, 0; Texture panel_brown.png, Border 8The plate behind the strip.
level_stripA Scroll View: Anchor Min and Anchor Max 0.5, 0, Offset Min -276, 136, Offset Max 276, 320, Pivot 0.5, 0; Content 1104, 0, Wheel 184; Focus Mode NoneThree levels in view of six; a level a notch.
levelsA Grid: Columns 6, Cell 0, 160, Spacing 24, 24, Padding 12Six columns across the content’s 1104.
level_1 to level_6A Menu Button each: Texture buttonLong_beige.png, Border 8; Text its number, Scale 1.25, Align Center; Action level_ and its number, the gallery buttons’ three textures; Shortcut Key Digit1 to Digit6, Show Label unticked; Focus Down code; LevelTile, Level its number; level_4 to level_6 Disabled, and the rest of each in the table aboveA big number on a beige tile, chosen by its own key.
level_1_stars, level_2_stars, level_3_stars, level_6_starsA Stack: Anchor Min 0, 1, Offset Min 0, -48, Offset Max 0, -14, Pivot 0.5, 1; Axis Horizontal, Fit Fixed, Spacing 4, Item 32, Align Center; level_6_stars hidden, with ShownBySecret, Once Open tickedThree stars in a row along the tile’s foot.
level_1_star_1 to level_6_star_3A Panel each: Texture star.png for a star earned, star_outline.png for one not yetStars at half their size, 32 wide in the Stack.
level_4_lock, level_5_lock, level_6_lockAnchor Min and Anchor Max 0.5, 0, Offset Min -24, 16, Offset Max 24, 64, Pivot 0.5, 0; Texture lock.png; level_6_lock with ShownBySecret, Once Open untickedThe padlock, 16 pixels at three times, over the number.
level_barA Scrollbar: Anchor Min and Anchor Max 0.5, 0, Offset Min -276, 336, Offset Max 276, 352, Pivot 0.5, 0; Texture slide_horizontal_grey.png, Border 8; View level_strip, Axis Horizontal, Min Thumb 16, Thumb 255, 255, 255, alpha 255, Thumb Texture slide_horizontal_color.png, Border 8; Focus Mode NoneThe slider’s art across, 16 high, under the strip.
codeA Text Entry: Anchor Min and Anchor Max 0.5, 0, Offset Min 16, 384, Offset Max 316, 433, Pivot 0, 0; Texture input_rectangle.png, Border 8; Action code, Placeholder Type the code, Max Length 8, Mode Password, Padding 12, Scale 0.75, Caret Width 3; Focus Up level_1; Localized Placeholder levels.code_prompt; SecretCode, Code delveDots for what is typed, and up goes back to the first level.
code_labelAnchor Max 0, 0, Offset Min -316, 0, Offset Max -16, 49, Pivot 0, 0; Text Secret code, Scale 0.75, Align Right; Key levels.codeThe field’s word, at its left.
difficultyA Dropdown: Anchor Min and Anchor Max 0.5, 0, Offset Min 16, 449, Offset Max 316, 489, Pivot 0, 0; Texture input_rectangle.png, Border 8; Text Difficulty, Scale 0.75, Align Center; Action difficulty, Options Normal and Hard, Disabled ticked, Item Height 40; Tooltip Finish every level to choose Hard., Delay 0.3, Placement Right; Key levels.difficulty, Tooltip levels.hard_tip, Options levels.normal and levels.hardA list that is not the player’s yet, and says why.
difficulty_arrowAnchor Min and Anchor Max 1, 0.5, Offset Min -46, -11, Offset Max -12, 11, Pivot 1, 0.5; Texture icon_arrow_down_dark.pngThe settings’ list arrow.
difficulty_labelAnchor Max 0, 0, Offset Min -316, -4, Offset Max -16, 44, Pivot 0, 0; Text Difficulty, Scale 0.75, Align Right; Key levels.difficultyThe list’s word, at its left.
levels_saidAnchor Min and Anchor Max 0.5, 0, Offset Min -500, 517, Offset Max 500, 557, Pivot 0.5, 0; Text Choose an open level, or type the secret code., Scale 0.75, Align Center; Key levels.choose, Substitution n 1; LevelSaidWhat happened last, across the foot.

Every other colour came from Apply theme, the ring’s gold among them.

What it costs. 13 draws as it opens, and 14 in the picture of the secret level, ringed, with other tiles in view. The tiles, the stars, the padlocks, the bar’s two pictures, the field, the list and the words break a run wherever a neighbour wears another picture.

Rings and meters

What it is for. A value drawn as a ring or a pie: health round a portrait, a shot charged while a button is held, an ability waiting to be used again. Three of them side by side: a health ring that two buttons empty and fill, a charge meter of a dozen chunky segments that fills both ways while it is held, and a cooldown pie that fills each time the charge is let go and empties again before the meter will charge. It joins the Radial Progress fields chapter 7 left as they came, a Button held down, a list whose choice a script writes into a choice field by name, a field that takes a number with a point, and a signal from one node to another.

  1. Start the screen as every screen starts: the scene gallery_rings.ron, the root Rings, the canvas rings_canvas, and the name rings_name, its Text Rings and meters and its Key rings.name.

The health ring

A ring is drawn twice: its track, full and dim, and the ring itself on it, filling with its value. Each is a plain node given a Rect and a Radial Progress, as the hotbar’s cooldown rings are.

  1. Select rings_canvas, click + Add node and choose Node. Rename it health_ring_track. Click Add component and choose RectComponent: Anchor Min and Anchor Max both 0.5, 0, Offset Min -460, 136, Offset Max -260, 336, Pivot 0.5, 0, a square of 200 left of the middle.
  2. Click Add component and choose RadialProgressComponent. Leave Value at 1, full. Set Start to 90, Direction to CounterClockwise, Inner Radius to 0.6 and Color to 63, 38, 49, alpha 153: the plum the words are outlined in, at three fifths.
  3. Select health_ring_track, add a Node and rename it health_ring. Give it a RectComponent, as it comes, filling the track, and a RadialProgressComponent with the same Start, Direction and Inner Radius, and Color 232, 69, 55, alpha 255, the palette’s danger red.
  4. Give health_ring a TextComponent: Text 10, Scale 1.5, Align Center. It is drawn in the ring’s hole.
  5. Select rings_canvas, add a Localized Label and rename it health_label: Anchor Min and Anchor Max 0.5, 0, Offset Min -530, 344, Offset Max -190, 376, Pivot 0.5, 0; Text Health, Scale 0.625, Align Center; Key rings.health.
  6. Add two Button nodes under rings_canvas, hurt and heal, each with the gallery buttons’ faces: Anchor Min and Anchor Max 0.5, 0, Pivot 0.5, 0; hurt at Offset Min -524, 388 and Offset Max -364, 437, Text Hurt, Action hurt, Key rings.hurt; heal at -356, 388 and -196, 437, Text Heal, Action heal, Key rings.heal. Each is 160 wide, for the French Blesser and Soigner.
  7. Create the script health_ring and replace everything in it with the script below. Give health_ring a HealthRing, Most 10.
rgs health_ring.rgs
/// The health ring: Hurt takes a point and Heal gives one back, from none to
/// the most, and the ring drains anticlockwise as health goes, its number in
/// its hole.
component HealthRing {
    /// The most health there is: a full ring.
    most: f32 = 10.0,
    /// The health now.
    #[hidden]
    health: f32 = 10.0,

    fn loop() {
        // A click on Hurt or Heal, or Enter or a pad's South while it holds
        // the focus, presses hurt or heal on the game's next step.
        if pressed("hurt") && health > 0.0 {
            health = health - 1.0;
        }
        if pressed("heal") && health < most {
            health = health + 1.0;
        }
        set_number("RadialProgressComponent", "value", health / most);
        set_text("TextComponent", "text", text(health));
    }
}

pressed("hurt") and pressed("heal") are true for one step after Hurt or Heal is clicked, or after Enter or a pad’s South while it holds the focus. The script writes the ring’s Value as a share of Most, and the number in its hole.

The charge meter

  1. Select rings_canvas, add a Node, charge_meter_track, with a RectComponent: Anchor Min and Anchor Max 0.5, 0, Offset Min -100, 136, Offset Max 100, 336, Pivot 0.5, 0, in the middle. Give it a RadialProgressComponent: Value 1, Start 90, Direction Both, Inner Radius 0.3, Segments 12 and Color 63, 38, 49, alpha 153.
  2. Select charge_meter_track, add a Node, charge_meter, with a RectComponent as it comes and a RadialProgressComponent: Value 0, Start 90, Direction Both, Inner Radius 0.3, Segments 12, Color 254, 174, 52, alpha 255, the accent gold.
charge_meter_track selected, its Inspector narrowed with the search box, radial, to its RadialProgressComponent: Value 1.000, Start 90.00 °, Direction Both, Inner Radius 0.30, Segments 12 and a plum Color over a checkerboard; the Viewport shows RINGS AND METERS, the full red health ring with 10 in its hole, HEALTH, HURT and HEAL, the dark twelve-sided charge track in the middle outlined as the selection with the Move gizmo at the middle of its top edge, CHARGE, HOLD TO CHARGE with C at its right edge, BOTH WAYS, SECONDS with 1.5, and at the right the dark cooldown disc, COOLDOWN and its three lines of words
The charge meter’s track: from six o’clock, both ways, in 12 segments.
Start
Where the fill begins, in degrees clockwise from three o’clock. A new ring’s is -90, twelve o’clock, as the hotbar’s rings and the cooldown pie keep it. The health ring and the charge meter take 90, six o’clock, so they fill from their foot.
Direction
Which way the fill grows from Start. Clockwise, as it comes; CounterClockwise, the other way; Both, half of it each way, so a ring half full covers the quarter on each side of Start. The health ring is CounterClockwise: from six o’clock it fills through three, twelve and nine, and as health goes its end falls back towards six. The meter is Both, rising up both sides at once like a cup filling.
Segments
How many straight edges a full turn is drawn with, from 3 to 512. A new ring’s 64 reads as round at any size The Delve draws. The meter’s 12 is chunky on purpose: a twelve-sided ring that fills a facet at a time, as a game’s charge gauge often does.
  1. Select rings_canvas, add a Localized Label, charge_label: Anchor Min and Anchor Max 0.5, 0, Offset Min -170, 344, Offset Max 170, 376, Pivot 0.5, 0; Text Charge, Scale 0.625, Align Center; Key rings.charge.
  2. Add a Menu Button under rings_canvas, charge, with the gallery buttons’ faces: Anchor Min and Anchor Max 0.5, 0, Offset Min -170, 380, Offset Max 170, 429, Pivot 0.5, 0; Text Hold to charge, Action charge; its Shortcut’s Action charge too, so it draws C; Key rings.hold.
  3. Add a Dropdown under rings_canvas, direction: Anchor Min and Anchor Max 0.5, 0, Offset Min -190, 444, Offset Max 190, 484, Pivot 0.5, 0; Texture input_rectangle.png, Border 8; Text Fills, Scale 0.75, Align Center; Action direction, Options Both ways, Clockwise and Anticlockwise, Item Height 40. Give it a LocalizedTextComponent with Key rings.fills and Options rings.both, rings.clockwise and rings.anticlockwise, and an arrow, direction_arrow, as the level select’s list has.
  4. Add a Text Entry under rings_canvas, charge_time: Anchor Min and Anchor Max 0.5, 0, Offset Min 8, 500, Offset Max 128, 549, Pivot 0, 0; Texture input_rectangle.png, Border 8; Action charge_time, Text 1.5, Placeholder empty, Max Length 4, Mode Decimal, Padding 12, Scale 0.75, Caret Width 3. Under it, a Localized Label, charge_time_label: Anchor Max 0, 0, Offset Min -176, 0, Offset Max -16, 49, Pivot 0, 0; Text Seconds, Scale 0.75, Align Right; Key rings.seconds.

The cooldown pie

  1. Select rings_canvas, add a Node, cooldown_pie_track, with a RectComponent: Anchor Min and Anchor Max 0.5, 0, Offset Min 260, 136, Offset Max 460, 336, Pivot 0.5, 0, at the right. Give it a RadialProgressComponent with Color 63, 38, 49, alpha 153, everything else as it comes: a full disc with no hole.
  2. Select cooldown_pie_track, add a Node, cooldown_pie, with a RectComponent as it comes and a RadialProgressComponent: Value 0, Color 254, 231, 97, alpha 255, the light gold.
  3. Add two Localized Label nodes under rings_canvas: cooldown_label, Anchor Min and Anchor Max 0.5, 0, Offset Min 190, 344, Offset Max 530, 376, Pivot 0.5, 0, Text Cooldown, Scale 0.625, Align Center, Key rings.cooldown; and cooldown_after, the same but Offset Min 190, 380 and Offset Max 530, 460, Text Fills when the charge is let go; the meter waits until it empties., Wrap ticked, Key rings.after: three lines in either language.
  4. Create the scripts cooldown_pie and charge_meter and replace everything in each with its script below. Give cooldown_pie a CooldownPie, Seconds 2, and charge_meter a ChargeMeter, Seconds 1.5 and Cooldown 2.
  5. Select charge_meter, click Add component and choose SignalComponent. Under Connections, add one: Signal released, To cooldown_pie.
  6. Last, drag back onto rings_canvas, select it, press Apply theme, and press Ctrl+S.
rgs charge_meter.rgs
/// The charge meter: holding the Charge button down, by the mouse or a finger,
/// or C, or a pad's right trigger, fills it over the seconds typed beside it,
/// and letting go empties it and starts its cooldown, which the cooldown pie
/// shows. It fills nothing while it cools down. The Fills list chooses which
/// way it fills.
component ChargeMeter {
    /// How long a full charge takes, until the Seconds field holds a time.
    #[suffix("s")]
    seconds: f32 = 1.5,
    /// How long after a release it fills nothing: the cooldown pie's time.
    #[suffix("s")]
    cooldown: f32 = 2.0,
    /// How full it is, 0 to 1.
    #[hidden]
    charge: f32 = 0.0,
    /// Seconds until it fills again.
    #[hidden]
    cooling: f32 = 0.0,

    fn loop(dt: f32) {
        // Counted here rather than waited out, as a hotbar slot counts its
        // cooldown: a hook that waits hears nothing. dt is how long a step
        // is, in seconds.
        if cooling > 0.0 {
            cooling -= dt;
        }
        // A field in Decimal mode answers the number typed in it, 0 while it
        // is empty.
        let time = seconds;
        if amount("charge_time") > 0.0 {
            time = amount("charge_time");
        }
        // held() is true on every step the Charge button, its key or the
        // trigger is held down; while it cools down, holding fills nothing.
        if held("charge") && cooling <= 0.0 {
            charge = min(1.0, charge + dt / time);
        } else if charge > 0.0 {
            // Let go: it empties and cools down. Heard by the cooldown pie
            // its Signal component wires it to, on the next step.
            charge = 0.0;
            cooling = cooldown;
            emit("released");
        }
        set_number("RadialProgressComponent", "value", charge);
        // Choosing a row of the Fills list presses direction, and amount()
        // answers the row, from 0. A choice field takes one of its choices by
        // name.
        if pressed("direction") {
            if amount("direction") == 1.0 {
                set_text("RadialProgressComponent", "direction", "Clockwise");
            } else if amount("direction") == 2.0 {
                set_text("RadialProgressComponent", "direction", "CounterClockwise");
            } else {
                set_text("RadialProgressComponent", "direction", "Both");
            }
        }
    }
}

held("charge") is true on every step the Charge button is held down, by the mouse or a finger, or C or a pad’s right trigger is held: from the press until the release, or until the pointer is dragged off the button. amount("charge_time") is the number in the Seconds field, 1.5 as it opens and 0 while it is empty. pressed("direction") is true on the step a real choice is made in the Fills list, and amount("direction") is that row, from 0.

The meter’s Direction is a choice field, and set_text writes it by its choice’s name: Clockwise, CounterClockwise or Both. A name it does not have is refused, with the Console naming the three.

rgs cooldown_pie.rgs
/// The cooldown pie: each time the charge is let go, it fills and then
/// empties over the meter's cooldown, as a hotbar slot's ring does after a
/// use, so the player sees how long it is until the meter fills again.
component CooldownPie {
    /// How long the pie takes to empty: the charge meter's cooldown.
    #[suffix("s")]
    seconds: f32 = 2.0,

    fn loop() {
        // The meter emits released when the charge is let go, and its Signal
        // component wires that to this pie, which hears it on the next step.
        if heard("released") {
            // Full first, so the anneal starts from there.
            set_number("RadialProgressComponent", "value", 1.0);
            anneal_number("RadialProgressComponent", "value", 0.0, seconds, .Linear);
        }
    }
}

The meter calls emit("released") as the charge is let go. Its Signal component wires released to the pie, which hears it with heard("released") on the next step, fills, and anneals back to empty over its Seconds, as a hotbar slot tells its ring in chapter 7. Neither script names the other node: the wire is the scene’s.

held
Whether the player is holding an action down, on every step until it is let go: a key, a pad’s control, or a Button held down by the mouse or a finger. A Button still fires its pressed on the release, so a press dragged off it is no click; a Button that is off holds nothing.
Rings and meters after Hurt was clicked three times, with C held: the health ring red from six o’clock round through three and twelve to nearly nine, the dark track showing on the left, 7 in its hole, HURT drawn grey; the twelve-sided charge meter filled gold across its lower half, up both sides from six o’clock; the cooldown pie a dark empty disc; HOLD TO CHARGE with C, BOTH WAYS, SECONDS with 1.5, and the three lines about the cooldown
Hurt three times: health 7 of 10. C held for most of a second: the meter fills both ways, a little over half of its 1.5 seconds.
The same screen after the charge was let go and C held again: the meter dark and empty, and the cooldown pie light gold from twelve o’clock clockwise round to seven, more than half of it still full
Let go, then held again: the pie fills and runs down over its 2 seconds, and while it runs the meter fills nothing.

Rings and meters, in one table

NodeValuesWhy this value
Rings, camera, rings_canvas, rings_name, backEvery screen’s, with Text Rings and meters and Key rings.nameThe gallery’s frame.
health_ring_trackA Node with a Rect, Anchor Min and Anchor Max 0.5, 0, Offset Min -460, 136, Offset Max -260, 336, Pivot 0.5, 0, and a Radial Progress, Start 90, Direction CounterClockwise, Inner Radius 0.6, Color 63, 38, 49, alpha 153The full ring, dim, behind the health.
health_ringA Rect as it comes; Radial Progress Start 90, Direction CounterClockwise, Inner Radius 0.6, Color 232, 69, 55, alpha 255; Text 10, Scale 1.5, Align Center; HealthRing, Most 10Red, from six o’clock, its number in a hole six tenths across.
health_labelAnchor Min and Anchor Max 0.5, 0, Offset Min -530, 344, Offset Max -190, 376, Pivot 0.5, 0; Text Health, Scale 0.625, Align Center; Key rings.healthUnder the ring.
hurt, healA Button each, the gallery buttons’ faces: Anchor Min and Anchor Max 0.5, 0, Offset Min -524, 388 and -356, 388, Offset Max -364, 437 and -196, 437, Pivot 0.5, 0; Text Hurt and Heal, Action hurt and heal; Key rings.hurt and rings.healSide by side under the ring.
charge_meter_trackA Node with a Rect, Anchor Min and Anchor Max 0.5, 0, Offset Min -100, 136, Offset Max 100, 336, Pivot 0.5, 0, and a Radial Progress, Start 90, Direction Both, Inner Radius 0.3, Segments 12, Color 63, 38, 49, alpha 153The meter’s track, twelve-sided.
charge_meterA Rect as it comes; Radial Progress Value 0, Start 90, Direction Both, Inner Radius 0.3, Segments 12, Color 254, 174, 52, alpha 255; Signal released to cooldown_pie; ChargeMeter, Seconds 1.5, Cooldown 2Empty until held; full in a second and a half.
charge_labelAnchor Min and Anchor Max 0.5, 0, Offset Min -170, 344, Offset Max 170, 376, Pivot 0.5, 0; Text Charge, Scale 0.625, Align Center; Key rings.chargeUnder the meter.
chargeA Menu Button, the gallery buttons’ faces: Anchor Min and Anchor Max 0.5, 0, Offset Min -170, 380, Offset Max 170, 429, Pivot 0.5, 0; Text Hold to charge, Action charge; Shortcut Action charge; Key rings.holdHeld by the pointer, or by C through its Shortcut, which draws the key.
directionA Dropdown: Anchor Min and Anchor Max 0.5, 0, Offset Min -190, 444, Offset Max 190, 484, Pivot 0.5, 0; Texture input_rectangle.png, Border 8; Text Fills, Scale 0.75, Align Center; Action direction, Options Both ways, Clockwise, Anticlockwise, Item Height 40; Key rings.fills, Options rings.both, rings.clockwise, rings.anticlockwiseWide enough for the French Des deux côtés.
direction_arrowAnchor Min and Anchor Max 1, 0.5, Offset Min -46, -11, Offset Max -12, 11, Pivot 1, 0.5; Texture icon_arrow_down_dark.pngThe list’s arrow.
charge_timeA Text Entry: Anchor Min and Anchor Max 0.5, 0, Offset Min 8, 500, Offset Max 128, 549, Pivot 0, 0; Texture input_rectangle.png, Border 8; Action charge_time, Text 1.5, Placeholder empty, Max Length 4, Mode Decimal, Padding 12, Scale 0.75, Caret Width 3A time with a point, four letters at most.
charge_time_labelAnchor Max 0, 0, Offset Min -176, 0, Offset Max -16, 49, Pivot 0, 0; Text Seconds, Scale 0.75, Align Right; Key rings.secondsThe field’s word, at its left.
cooldown_pie_trackA Node with a Rect, Anchor Min and Anchor Max 0.5, 0, Offset Min 260, 136, Offset Max 460, 336, Pivot 0.5, 0, and a Radial Progress, Color 63, 38, 49, alpha 153A full dim disc: no hole, from twelve o’clock.
cooldown_pieA Rect as it comes; Radial Progress Value 0, Color 254, 231, 97, alpha 255; CooldownPie, Seconds 2Empty until a release.
cooldown_label, cooldown_afterAnchor Min and Anchor Max 0.5, 0, Offset Min 190, 344 and 190, 380, Offset Max 530, 376 and 530, 460, Pivot 0.5, 0; Text Cooldown and the sentence, Scale 0.625, Align Center, the sentence with Wrap; Key rings.cooldown and rings.afterThe pie’s word, and what it does on three lines.

The rings keep their own colours: Apply theme writes none of a Radial Progress’s. Every other colour came from it.

What it costs. 7 draws, as few as the gallery’s menu. A Radial Progress is plain colour, with no picture of its own, and Segments is how many triangles a full turn takes: 64 as it comes, 12 on the meter.

Key hints

What it is for. The controls shown in a corner of the screen, which the player may hide once they know them. Four hints, each a control whose Shortcut draws its key at its right edge, from the input map, in the game’s language: each lights while its key is held. Beside them, tick boxes hide them one by one, and the hints close up. It joins a Stack that gives each child its own Layout Element’s height, a Shortcut’s label, a Focus that keeps both the ring and the pointer out of a subtree, and tick boxes, one of them off.

  1. Start the screen as every screen starts: the scene gallery_hints.ron, the root Hints, the canvas hints_canvas, and the name hints_name, its Text Key hints and its Key hints.name.
  2. Select hints_canvas, add a Panel and rename it hint_panel. In its Rect, set Anchor Max to 0, 0, Offset Min to 340, 136, Offset Max to 780, 408 and Pivot to 0, 0. In its Panel, set Texture to panel_brown.png and Border to 8.
  3. Select hint_panel, add a Stack and rename it hint_rows. Its Rect stays as it comes, filling the plate. In its Stack, set Fit to Elements, Spacing to 12, Padding to 20 and Item to 49, which Elements leaves unread.
  4. Give hint_rows a FocusComponent: Mode None, Subtree Focus Deny and Subtree Pointer Deny.
Subtree Pointer
Whether the pointer may reach the controls under this node: Inherit, as it comes, takes the answer from the nearest ancestor that says; Deny makes the subtree invisible to the pointer, so a click or a hover goes through it to whatever is behind; Allow lets it back in under a Deny. hint_rows takes Deny, as its Subtree Focus is Deny: the hints say which keys do what, and are not the way to do it, so neither the ring nor a click lands on them.
  1. Select hint_rows, click + Add node and choose Menu Button. Rename it hint_bag. Its Rect stays as it comes: the Stack places it. Give it the gallery buttons’ faces, Text Open the bag and Action bag, and set its Shortcut’s Action to bag too, the input map’s, on B. Its Show Label stays ticked.
  2. Add a LocalizedTextComponent with Key hints.bag, and a LayoutElement with Min 49 and Preferred 49. Flexible stays 0.
hint_bag selected, its Inspector narrowed with the search box, shortcut, to its LitWhileHeld, Action bag, and its ShortcutComponent: Action bag, Key empty, Show Label ticked; the Viewport shows KEY HINTS over the brown plate of four hints, OPEN THE BAG outlined as the selection, with no gizmo on it, READ THE JOURNAL, USE THE FIRST SLOT and GO BACK, each with its key at its right edge, B, J, 1 and ESC, and at the right SHOW over four ticked boxes, BAG, JOURNAL, SLOT 1 and BACK, and PRESS A KEY: ITS HINT LIGHTS UP. under them; the Viewport’s foot reads Placed by its Stack
A hint’s Shortcut: the action whose key it draws.
Preferred
A Layout Element’s Preferred: the length its node asks for along a Stack’s axis, in UI units, which a Stack with Fit Elements gives it when there is room. A new Layout Element’s is 48. The hints take 49, a button’s height, with Min 49 too, so none is ever squeezed. A hidden child asks for nothing, so the Stack closes the gap it leaves.
Key
A Shortcut’s Key: a key named by itself, such as Ctrl+KeyS, which presses the control and is never rebound. Empty, as it comes, the Shortcut uses its Action’s keys instead, from the input map, and a rebind moves them. The hints leave it empty and name actions. The level select’s tiles name Digit1 to Digit6 here: a level’s number key is not the player’s to rebind.
Show Label
Whether the control draws its key inside its right edge, at half the size of its words: the input map’s first keyboard key for its Action, named as a key. key of the string tables renames it, or its Key. Ticked, as it comes: the hints draw B, J, 1 and Esc, and Échap in French. The level tiles untick it, as their number is already their word.

A hint lights while its key is held. The script reads the action its Shortcut names, so a key rebound in the input map lights it too.

  1. Create the script lit_while_held and replace everything in it with the script below. Give hint_bag a LitWhileHeld: its Action bag, Lit and Plain, as the script gives them.
rgs lit_while_held.rgs
/// A key hint, lit while its key is held: the input map's action its Shortcut
/// names, so a rebound key lights it too.
component LitWhileHeld {
    /// The action its Shortcut names.
    action: String = "bag",
    /// The tint its face is drawn in while the key is down: the coin's light
    /// gold.
    #[color]
    lit: Vec4 = vec4(0.99, 0.8, 0.12, 1.0),
    /// The tint its face is drawn in otherwise: white, which leaves the face
    /// its own colours.
    #[color]
    plain: Vec4 = vec4(1.0, 1.0, 1.0, 1.0),

    fn loop() {
        // A button's face is drawn in its Normal tint, since no pointer
        // reaches a hint.
        if held(action) {
            set_colour("ButtonComponent", "normal", lit);
        } else {
            set_colour("ButtonComponent", "normal", plain);
        }
    }
}

held(action) is true on every step the action’s key is held down, B for the bag, and the hint’s face takes its Lit gold for as long. No pointer reaches the hints, so their faces are always drawn in their Normal tint, which the script sets.

  1. Create the script shown_by_tick and replace everything in it with the script below. Give hint_bag a ShownByTick, its Tick show_bag, as the script gives it.
rgs shown_by_tick.rgs
/// A key hint, shown while its tick box beside the hints is ticked. A hidden
/// hint takes no room in the hints' Stack, so the ones under it move up.
component ShownByTick {
    /// The tick box's action, which answers 1 while the box is ticked.
    tick: String = "show_bag",

    fn loop() {
        if amount(tick) == 1.0 {
            show();
        } else {
            hide();
        }
    }
}

amount(tick) is 1 while the box is ticked and 0 while it is not, from the step it flips by a click, or by Enter or a pad’s South while it holds the focus.

  1. Select hint_bag and press Ctrl+D three times. Rename the copies hint_journal, hint_use and hint_back, and give each its Text, Action, Shortcut Action, Key, LitWhileHeld Action and ShownByTick Tick from the table below.
HintTextIts action, on Button, Shortcut and LitWhileHeldKeyTick
hint_bagOpen the bagbaghints.bagshow_bag
hint_journalRead the journaljournalhints.journalshow_journal
hint_useUse the first slotuse_1hints.useshow_use
hint_backGo backui_cancelhints.backshow_back

The tick boxes

  1. Select hints_canvas, add a Panel and rename it tick_panel: Anchor Max 0, 0, Offset Min 800, 136, Offset Max 1060, 408, Pivot 0, 0; Texture panel_brown.png, Border 8.
  2. Select tick_panel, add a Stack and rename it tick_rows: Fit Fixed, Spacing 12, Padding 20, Item 49, so each row lines up with its hint.
  3. Select tick_rows, click + Add node and choose Node. Rename it show_bag_row: a plain node in the Stack, as the settings screen’s rows are.
  4. Select show_bag_row, add a Toggle and rename it show_bag. Remove its Text with the bin at the right of its TextComponent header. In its Rect, set Anchor Min and Anchor Max both to 0, 0.5, Offset Min to 16, -16, Offset Max to 48, 16 and Pivot to 0, 0.5: the box, 32 square. Set its Panel’s Texture to check_square_grey.png.
  5. In its Toggle, set Action to show_bag, tick On, and set Check Inset to 9. Leave On Texture empty: the mark is drawn inside the box.
  6. Select show_bag, add a Localized Label and rename it show_bag_label: Anchor Max 0, 0, Offset Min 44, -8, Offset Max 232, 40, Pivot 0, 0; Text Bag, Scale 0.75; Key hints.show_bag.
  7. Select show_bag_row and press Ctrl+D three times. Rename the copies show_journal_row, show_use_row and show_back_row, and their children to match: show_journal and show_journal_label, and so on. Give each box its Action, show_journal, show_use and show_back, and each label its Text, Journal, Slot 1 and Back, and its Key, hints.show_journal, hints.show_use and hints.show_back.
  8. Select show_back and tick its Toggle’s Disabled: the way back is always shown.
  9. Select hints_canvas, add a Localized Label and rename it hints_show: Anchor Max 0, 0, Offset Min 800, 104, Offset Max 1060, 132, Pivot 0, 0; Text Show, Scale 0.625, Align Center; Key hints.show. It goes over the boxes’ plate.
  10. Select hints_canvas, add a Localized Label and rename it hints_press: Anchor Max 0, 0, Offset Min 140, 440, Offset Max 1140, 472, Pivot 0, 0; Text Press a key: its hint lights up., Scale 0.625, Align Center; Key hints.press.
  11. Last, drag back onto hints_canvas, select it, press Apply theme, and press Ctrl+S.
Disabled
A Toggle’s Disabled: whether the box is off. An off box does not flip, by a click, a key or a pad, is drawn greyed, and the ring passes it by; it keeps its On. A new Toggle is on. show_back is off and ticked: the Back hint can never be hidden, and the box says so.
Check Inset
How far inside the box’s edge the mark of a Toggle with no On Texture is drawn, in UI units, in its Check colour. A new Toggle’s is 6. The boxes take 9: the box is 32 square, so the mark is a square of 14 in its middle, as a lamp is lit.
Key hints after the Journal box was unticked, with B held: three hints in the left plate, OPEN THE BAG lit gold with B at its right edge, USE THE FIRST SLOT with 1 and GO BACK with ESC, closed up one under another with the room for a fourth empty at the foot; at the right SHOW over the four boxes, BAG ticked, JOURNAL empty, SLOT 1 ticked, BACK ticked and greyed; PRESS A KEY: ITS HINT LIGHTS UP. along the foot
Journal unticked, and B held: the Journal hint is gone, the two under it have moved up, and Open the bag is lit.

The key hints, in one table

NodeValuesWhy this value
Hints, camera, hints_canvas, hints_name, backEvery screen’s, with Text Key hints and Key hints.nameThe gallery’s frame.
hint_panelAnchor Max 0, 0, Offset Min 340, 136, Offset Max 780, 408, Pivot 0, 0; Texture panel_brown.png, Border 8Four hints of 49, three gaps of 12 and 20 above and below.
hint_rowsA Stack: Fit Elements, Spacing 12, Padding 20, Item 49; Focus Mode None, Subtree Focus Deny, Subtree Pointer DenyEach hint its own height; neither the ring nor a click reaches them.
hint_bag, hint_journal, hint_use, hint_backA Menu Button each, the gallery buttons’ faces; Text, Action and Shortcut Action in the table above; Key its own; Layout Element Min 49, Preferred 49; LitWhileHeld, Action its own; ShownByTick, Tick its ownA button only for the key it draws.
hints_showAnchor Max 0, 0, Offset Min 800, 104, Offset Max 1060, 132, Pivot 0, 0; Text Show, Scale 0.625, Align Center; Key hints.showOver the boxes.
tick_panelAnchor Max 0, 0, Offset Min 800, 136, Offset Max 1060, 408, Pivot 0, 0; Texture panel_brown.png, Border 8Beside the hints, as tall, wide enough for Afficher and Journal.
tick_rowsA Stack: Fit Fixed, Spacing 12, Padding 20, Item 49Rows level with the hints.
show_bag_row, show_journal_row, show_use_row, show_back_rowA Node each, with nothing on itA row the Stack places, holding its box.
show_bag, show_journal, show_use, show_backA Toggle without its Text: Anchor Min and Anchor Max 0, 0.5, Offset Min 16, -16, Offset Max 48, 16, Pivot 0, 0.5; Texture check_square_grey.png; Action its own name, On ticked, Check Inset 9; show_back DisabledThe grey box at its own size, its mark drawn inset.
show_bag_label, show_journal_label, show_use_label, show_back_labelAnchor Max 0, 0, Offset Min 44, -8, Offset Max 232, 40, Pivot 0, 0; Text Bag, Journal, Slot 1 and Back, Scale 0.75; Key hints.show_ and its nameThe box’s word, at its right.
hints_pressAnchor Max 0, 0, Offset Min 140, 440, Offset Max 1140, 472, Pivot 0, 0; Text Press a key: its hint lights up., Scale 0.625, Align Center; Key hints.pressWhat to do, under it all.

Every colour came from Apply theme, the boxes’ plum mark among them.

What it costs. 8 draws: the plates, the hints’ faces, the boxes and the words, with the keys drawn as words.

Quitting

What it is for. Asking before the game is left with work unsaved: a level editor, a character sheet, a page of notes. A notes page with a Game menu of Save and Quit, and over it the question its Quit asks, with three answers: Save, Don’t save and Cancel. While it is up the page behind it is dimmed and takes no input, and Save is off until the notes hold something unsaved. It joins a Canvas Group that makes a subtree inert, another that ignores the groups above it, a Modal whose first answer is off, a Pop-up Menu with a wider rule, and a field that starts with words in it.

  1. Start the screen as every screen starts: the scene gallery_quit.ron, the root Quit, the canvas quit_canvas, and the name quit_name, its Text Quitting and its Key quit.name.
  2. Select quit_canvas, click + Add node and choose Fade Group. Rename it quit_page. Its Rect stays as it comes, filling the canvas. Drag quit_name onto it: the page holds everything on the screen.
  3. In its Canvas Group, set Alpha to 0.45 and untick Interactable: the page opens dimmed and inert, on the question.
Interactable
A Canvas Group’s Interactable: whether the controls under it answer at all. Unticked, nothing in the subtree answers a click or takes the ring, and the subtree is drawn as it is, so Alpha dims it to show it. A new group’s is ticked. quit_page unticks it as the question opens, at Alpha 0.45, and its script ticks it again, at 1, when the question is answered with Cancel.

The notes page

  1. Select quit_page, add a Panel and rename it notes_plate. In its Rect, set Anchor Min and Anchor Max both to 0.5, 0.5, Offset Min to -320, -200 and Offset Max to 320, 60. In its Panel, set Texture to panel_brown.png and Border to 8.
  2. Select notes_plate, add a Localized Label and rename it notes_title, the bag’s title again: Anchor Max 1, 0, Offset Min 16, 0, Offset Max -16, 36, Pivot 0.5, 0; Text Notes, Scale 0.75, Outline 63, 38, 49, alpha 255, Outline Width 1; role Accent; Key quit.notes.
  3. Select notes_plate, add a Button and rename it game_menu: Anchor Max 0, 0, Offset Min 40, 56, Offset Max 200, 105, Pivot 0, 0; the gallery buttons’ faces, Text Game, Action game_menu. Add a LocalizedTextComponent with Key quit.game and two Options, quit.save and quit.quit: the menu’s rows.
  4. Click Add component and choose PopupMenuComponent, as chapter 9’s slot menus. Under Items, add three rows: Label Save, Action game_save, Shortcut KeyS; a row with Separator ticked and Enabled unticked; and Label Quit, Action game_quit, Shortcut KeyQ. Enabled stays ticked on both.
  5. Set Opens to game_menu, so the Game button opens it, Width to 240, Item Height to 30, Separator Height to 16, Shortcut Width to 24 and Scale to 0.625. Set Texture to panel_beigeLight.png, Border to 8 and Padding to 8, 8. The colours come from Apply theme.
Separator Height
A Pop-up Menu’s Separator Height: how tall a separator row is, in UI units. A new menu’s is 9, as the bag’s menus keep it. The Game menu’s is 16, a wider gap between Save and Quit, so Quit stands apart from the row a player means to press.
  1. Select notes_plate, click + Add node and choose Text Entry. Rename it notes: Anchor Max 0, 0, Offset Min 40, 132, Offset Max 600, 181, Pivot 0, 0; Texture input_rectangle.png, Border 8.
  2. In its Text Entry, set Action to notes, Text to The shrine heals., Placeholder to Write a note, Padding to 12, Scale to 0.625 and Caret Width to 3. Add a LocalizedTextComponent with Placeholder quit.notes_prompt.
  3. Create the script notes and replace everything in it with the script below. Give notes a Notes.
Text
A Text Entry’s Text: the words in the field, which the field opens with, the player edits, and a script reads with entered or writes with set_text. Empty, as it comes, the field shows its Placeholder instead. The notes open on The shrine heals.; the shop’s quantity on 1; the rings’ Seconds on 1.5.
rgs notes.rgs
/// The notes field: it opens on the notes last saved, even empty ones, and
/// saves what it holds when the Game menu's Save or the quit question's Save
/// is pressed. It keeps notes_changed as 1 while it holds words it has not
/// saved, which the quit question reads.
component Notes {
    /// The words last saved, or, until the notes are first saved, the words
    /// the field is made with.
    #[hidden]
    last_saved: String = "",
    /// Whether last_saved holds them yet.
    #[hidden]
    known: bool = false,

    fn on_create() {
        // gallery_notes_saved is 1 once the notes have been saved, so notes
        // saved empty open empty. Until then the field keeps the words it
        // was made with.
        if saved_number("gallery_notes_saved", 0.0) == 1.0 {
            last_saved = saved_text("gallery_notes", "");
            known = true;
            set_text("TextEntryComponent", "text", last_saved);
        }
    }

    fn loop() {
        // entered() reads the field's words as the step began.
        let words = entered("notes");
        if !known {
            last_saved = words;
            known = true;
        }
        // The Game menu's Save, by a click on its row or S while the menu is
        // open, presses game_save, and the quit question's Save presses
        // save_quit, on the game's next step.
        if pressed("game_save") || pressed("save_quit") {
            save_text("gallery_notes", words);
            save_number("gallery_notes_saved", 1.0);
            last_saved = words;
        }
        if words == last_saved {
            keep_number("notes_changed", 0.0);
        } else {
            keep_number("notes_changed", 1.0);
        }
    }
}

entered("notes") is the field’s words as the step began. pressed("game_save") is true for one step after the Game menu’s Save is chosen, by a click on its row or S while the menu is open; pressed("save_quit") after the question’s Save is clicked, or chosen by the arrows and Enter. Each saves the notes with save_text.

The question

  1. Select quit_page, click + Add node and choose Modal. Rename it quit_question. In its Rect, set Anchor Min and Anchor Max both to 0.5, 0.5, Offset Min to -480, 50 and Offset Max to 480, 250: over the page’s foot. In its Panel, set Texture to panel_beige.png and Border to 8, the dialogs’ plate.
  2. In its Modal, set Title to Quit the game? and Body to Your notes have not changed. Under Buttons, make three: Label Save, Action save_quit, Disabled ticked; Label Don't save, Action quit_unsaved; and Label Cancel, Action cancel_quit. Set Cancel Action to cancel_quit, so Esc answers Cancel.
  3. Under Colors, set Scrim to 63, 38, 49, alpha 0: no scrim, as the page dims itself. Under Answers, set Button Texture to buttonLong_beige.png, Button Hovered Texture to buttonLong_grey.png, Button Pressed Texture to buttonLong_beige_pressed.png and Button Border to 8. Under Layout, set Padding to 20, Scale to 0.625, Title Height to 36, Button Height to 49 and Button Gap to 16, as chapter 9’s questions.
  4. Click Add component and choose CanvasGroupComponent, and tick its Ignore Parent Groups.
  5. Add a LocalizedTextComponent with Key quit.title, Body quit.unchanged and three Options, quit.save, quit.dont_save and quit.cancel: the title, the question and the answers. Add an AudioSourceComponent, Positional unticked, with the trigger back on audio/back_001.ogg.
  6. Create the scripts quit_question and quit_page and replace everything in each with its script below. Give quit_question a QuitQuestion, and quit_page a QuitPage, Dimmed 0.45.
  7. Last, drag back onto quit_page, not the canvas: Back is on the page, and dims with it. Select quit_canvas, press Apply theme, and press Ctrl+S.
quit_question selected, its Inspector narrowed with the search box, group, to its Node rows, Groups and an open Advanced fold, and its CanvasGroupComponent: Alpha 1.000, Interactable ticked, Blocks Input unticked and Ignore Parent… ticked; the Viewport shows QUITTING, the dimmed notes page with NOTES, GAME and the field reading THE SHRINE HEALS., and over its foot the question bright and whole, outlined as the selection with the Move gizmo at its middle: QUIT THE GAME?, YOUR NOTES HAVE NOT CHANGED., SAVE greyed, DON’T SAVE and CANCEL
The question’s own Canvas Group: Ignore Parent Groups ticked.
Ignore Parent Groups
Whether a Canvas Group ignores every group above it, their Alpha and their Interactable alike, and answers only to its own. A new group’s is unticked. The question ticks it: it is inside quit_page, which is at 0.45 and inert, and it is drawn whole and answers all the same.
Disabled
A Modal answer’s Disabled: whether that answer is off. An off answer is drawn with its word at half strength, and a click on it does nothing. Each answer has its own, under Buttons; the dialog has none of its own. Save starts off: nothing is changed as the screen opens.
rgs quit_question.rgs
/// The question asked before the game is left: Save keeps the notes and
/// leaves, Don't save leaves without them, and Cancel, or Escape, stays.
/// While the notes hold nothing that is not saved, Save is off and the
/// question says so. In the gallery, leaving goes back to the gallery.
component QuitQuestion {
    fn loop() {
        // The Game menu's Quit, by a click on its row or Q while the menu is
        // open, presses game_quit. The question is up as the screen opens,
        // with Save off, as the scene makes it: nothing is changed yet.
        if pressed("game_quit") {
            // The notes field keeps notes_changed as 1 while it holds words
            // it has not saved.
            let changed = kept_number("notes_changed", 0.0) == 1.0;
            // The answers count from 0, left to right: Save is the first.
            set_flag("ModalComponent", "buttons.0.disabled", !changed);
            // The Localized Text beside this script writes the question in
            // the game's language, from its body's key.
            if changed {
                set_text("LocalizedTextComponent", "body", "quit.body");
            } else {
                set_text("LocalizedTextComponent", "body", "quit.unchanged");
            }
            show();
        }
        // A click on an answer, or Enter on the one the arrows chose, presses
        // its action; Escape presses the Cancel Action, cancel_quit.
        if pressed("cancel_quit") {
            hide();
        }
        // The notes field saves the notes when save_quit is pressed.
        if pressed("save_quit") {
            play_sound("back");
            change_scene("gallery");
        }
        if pressed("quit_unsaved") {
            play_sound("back");
            change_scene("gallery");
        }
    }
}

pressed("game_quit") is true for one step after the Game menu’s Quit is chosen, by a click on its row or Q while the menu is open. pressed("cancel_quit") is true after Cancel is clicked or chosen, and after Esc or a pad’s East, through the Cancel Action; pressed("save_quit") and pressed("quit_unsaved") after Save and Don’t save.

rgs quit_page.rgs
/// The notes page behind the quit question: dimmed and inert while the
/// question is up, and lit and live again once Cancel answers it.
component QuitPage {
    /// How strongly the page is drawn while the question is up, 0 to 1.
    dimmed: f32 = 0.45,

    fn loop() {
        // Cancel, or Escape through the question's Cancel Action, presses
        // cancel_quit on the game's next step.
        if pressed("cancel_quit") {
            set_number("CanvasGroupComponent", "alpha", 1.0);
            set_flag("CanvasGroupComponent", "interactable", true);
        }
        // The Game menu's Quit, by a click on its row or Q while the menu is
        // open, presses game_quit.
        if pressed("game_quit") {
            set_number("CanvasGroupComponent", "alpha", dimmed);
            set_flag("CanvasGroupComponent", "interactable", false);
        }
    }
}

pressed("cancel_quit") and pressed("game_quit") are the question’s: the page lights up and comes alive when the question is cancelled, and dims and goes inert when the Game menu asks it again.

The quit question as the screen opens: Back, QUITTING and the notes page behind it all dimmed, NOTES, GAME and the field reading THE SHRINE HEALS. faded into the brown, and over the page’s foot the question on its beige plate at full strength, QUIT THE GAME?, YOUR NOTES HAVE NOT CHANGED., SAVE greyed, and DON’T SAVE and CANCEL live
As it opens: the page dimmed and inert, the question whole, and Save off, with the question saying why.

Press Cancel, or Esc: the page lights up and takes input again. Type into the notes, choose Game, then Quit, and the question comes back with Save live, saying Your notes are not saved yet.

The quit question asked again: the notes page dimmed behind it, its field now reading MIND THE SPIKES. THE SHRINE HEALS., and the question at full strength, QUIT THE GAME?, YOUR NOTES ARE NOT SAVED YET., and all three answers live, SAVE, DON’T SAVE and CANCEL
Esc, then words typed in front of the note, then the Game menu’s Quit: the notes changed, so Save is live and the question says they are not saved.

Save keeps the notes and goes back to the gallery, and the page opens on them the next time; Don’t save goes back keeping nothing.

Quitting, in one table

NodeValuesWhy this value
Quit, camera, quit_canvas, quit_name, backEvery screen’s, with Text Quitting and Key quit.name; quit_name and back under quit_pageThe gallery’s frame, on the page.
quit_pageA Fade Group: Alpha 0.45, Interactable unticked; QuitPage, Dimmed 0.45Opens dimmed and inert, under the question.
notes_plateAnchor Min and Anchor Max 0.5, 0.5, Offset Min -320, -200, Offset Max 320, 60; Texture panel_brown.png, Border 8The page’s plate, above the middle.
notes_titleAnchor Max 1, 0, Offset Min 16, 0, Offset Max -16, 36, Pivot 0.5, 0; Text Notes, Scale 0.75, Outline 63, 38, 49, alpha 255, Outline Width 1; role Accent; Key quit.notesThe bag’s title.
game_menuA Button: Anchor Max 0, 0, Offset Min 40, 56, Offset Max 200, 105, Pivot 0, 0; the gallery buttons’ faces, Text Game, Action game_menu; Key quit.game, Options quit.save, quit.quit; Pop-up Menu Items Save on game_save and KeyS, a separator, Quit on game_quit and KeyQ, Opens game_menu, Width 240, Item Height 30, Separator Height 16, Shortcut Width 24, Scale 0.625, Texture panel_beigeLight.png, Border 8, Padding 8, 8A menu the button opens, Quit set apart.
notesA Text Entry: Anchor Max 0, 0, Offset Min 40, 132, Offset Max 600, 181, Pivot 0, 0; Texture input_rectangle.png, Border 8; Action notes, Text The shrine heals., Placeholder Write a note, Padding 12, Scale 0.625, Caret Width 3; Localized Placeholder quit.notes_prompt; NotesA field that opens with a note in it.
quit_questionA Modal: Anchor Min and Anchor Max 0.5, 0.5, Offset Min -480, 50, Offset Max 480, 250; Texture panel_beige.png, Border 8; Title Quit the game?, Body Your notes have not changed., Buttons Save on save_quit and Disabled, Don’t save on quit_unsaved, Cancel on cancel_quit; Cancel Action cancel_quit; Scrim 63, 38, 49, alpha 0; the three answer textures, Button Border 8; Padding 20, Scale 0.625, Title Height 36, Button Height 49, Button Gap 16; Canvas Group Ignore Parent Groups; Key quit.title, Body quit.unchanged, Options quit.save, quit.dont_save, quit.cancel; Audio Source, trigger back; QuitQuestionShown as the screen opens, whole and live inside the dimmed page.

Every other colour came from Apply theme, the answers’ tints and the question’s words among them.

What it costs. 11 draws. The page under a group at 0.45 is still drawn, every picture of it, and the question over it: a dimmed page costs what a lit one does.

What each costs

Each recipe counted its own screen. Here they are together, as the player counts them for each picture: the draws a frame of the screen takes.

ScreenDrawsWhy
The gallery’s menu7A plate, the buttons’ beige and grey faces, and the words.
A shop18Many pictures side by side, and the window display in the room; 19 with a tooltip up.
A dialogue9The room’s sprites, the bars, the box, the buttons, the slider and the words; the name’s distance-field run is not counted.
A level select13Tiles, stars and padlocks next to each other, the bar’s two pictures, the field and the list; 14 with the ring round the secret level.
Rings and meters7The rings are plain colour, with no picture.
Key hints8Two plates, the hints’ faces, the boxes and the words.
Quitting11The dimmed page is drawn whole, and the question over it.

A draw is a run of interface pictures that share a texture, drawn at once. A screen that changes picture between neighbours, a plate, then a face, then a word, then a face again, draws more; one that keeps to a few pictures draws less, whatever the number of nodes. When a screen draws too much on a phone, look first at how many different pictures sit next to each other.

Check it works