Skip to content

Tutorials · Interfaces · Chapter 12

Save slots

80 minutes Uses: TextEntry, Modal, Button, Focus, entered, save_text, saved_text, forget, save_number, saved_number, pressed, set_text, set_flag, change_scene

By the end

  • The title’s New game is Play, which opens three save slots
  • A name typed into a slot’s field and entered starts a run saved under it at the shrine, given back by Continue
  • Delete asks first, then forgets the slot
On this page

Three slots

Until now the title’s button started a run at once. A game that saves has to ask which game first. This chapter builds the screen that asks: three save slots, each with a field to type a name into, a button that starts the game under that name, and once the game has a name, a button that carries it on and another that deletes it. The title’s button becomes Play, and opens this screen.

The save slots screen as Play opens it: the title SAVE SLOTS in gold, then three brown plates, each holding a white field that reads EMPTY SLOT in grey and a beige START button, the first field with its caret showing; under them a beige BACK button with ESC at its right edge
Play: three free slots.

The screen is a scene of its own, scenes/slots.ron, built as the title was. A slot is a plate, and this section builds one: the plate, its name field, its check, and its Start and Delete buttons. The field’s own fields and its script come next, then the scripts that save, and then the plate is copied twice.

  1. Copy the two pictures below into art, as before, and press Refresh in the bar of the Assets panel. They are a check mark and a cross, from the UI Pack: RPG Expansion.

The scene, its camera and its canvas

  1. Choose File → New scene. Click + Add node, choose Node and rename it Slots. Press Ctrl+S and save the scene in the scenes folder as slots.ron.
  2. Select Slots, click + Add node and choose Camera. Rename the node camera. Set Scaling to Letterbox, Extent to 180, Width to 320 and Background to 118, 59, 54, alpha 255: the title’s and the settings screen’s brown.
  3. Select Slots, click + Add node and choose Canvas. Rename the node slots_canvas and set its Scale Mode to ShrinkToFit.
  4. In its Theme, set Panel to 255, 255, 255, alpha 255, Panel Accent to 254, 231, 97, alpha 255, Panel Danger to 232, 69, 55, alpha 255, Text to 236, 227, 206, alpha 255, Text Accent to 254, 174, 52, alpha 255, Text Danger to 255, 228, 220, alpha 255, Button to 255, 255, 255, alpha 255, Button Text to 63, 38, 49, alpha 255, Hover Lift to 0.15 and Press Sink to 0.35: the title’s palette.
  5. Press Add component and choose FocusRingComponent, and press Add component again and choose TextSize, chapter 6’s script. Their colours and sizes come as they are, and Apply theme writes the ring’s: 254, 174, 52, alpha 255. The three sizes are the script’s own: 1536, 864, then 1280, 720, then 1066, 600.

Every colour on this screen, a Panel’s, a Text’s, a button’s and a field’s, comes from the Theme when you press Apply theme on slots_canvas once the screen is built. So you do not type them: the steps name the nodes, and the recipe at the end of the chapter lists what Apply theme writes.

The title and the plate

  1. Select slots_canvas, click + Add node and choose Label. Rename the node slots_name. In its Rect, set Anchor Min and Anchor Max both to 0.5, 0.5, Offset Min to -360, -222, Offset Max to 360, -142 and Pivot to 0.5, 1. In its Text, set Text to Save slots, Scale to 1.5, Align to Center, Outline to 63, 38, 49, alpha 255, Outline Width to 2, Shadow to 0, 0, 0, alpha 153, Shadow Offset to 3, 3 and Shadow Softness to 0. Add ThemeRoleComponent and set its Role to Accent.
  2. Select slots_canvas, click + Add node and choose Stack. Rename the node slot_plates. In its Rect, set Anchor Min and Anchor Max both to 0.5, 0.5, Offset Min to -312, -126, Offset Max to 312, 149 and Pivot to 0.5, 0. In its Stack, set Fit to Fixed, Spacing to 16 and Item to 81. The three plates will be 624 wide and 81 high, 16 apart.
  3. Select slot_plates, click + Add node and choose Panel. Rename the node slot_1. Set its Texture to panel_brown.png and Border to 8. The Stack places it, so its Rect stays as it comes.

The plate’s parts

  1. Select slot_1, click + Add node and choose Text Entry. Rename the node slot_1_name. In its Rect, set Anchor Min and Anchor Max both to 0, 0, Offset Min to 16, 16, Offset Max to 336, 65 and Pivot to 0, 0: the preset centres its rect. In its Panel, set Texture to input_rectangle.png and Border to 8. The preset brought a Focus too: it stays.
  2. Select slot_1_name, click + Add node and choose Panel. Rename the node slot_1_done. In its Rect, set Anchor Min and Anchor Max both to 1, 0, Offset Min to -40, 9, Offset Max to -8, 39 and Pivot to 1, 0. Set its Panel’s Texture to iconCheck_bronze.png. It is the check, in the field’s top right corner, and it is a child of the field so that it moves with it.
  3. Select slot_1, click + Add node and choose Button. Rename the node slot_1_play. In its Rect, set Anchor Max to 0, 0, Offset Min to 348, 16, Offset Max to 548, 65 and Pivot to 0, 0. In its Panel, set Texture to buttonLong_beige.png and Border to 8. In its Text, set Text to Start, Scale to 0.75 and Align to Center. In its Button, set Action to slot_1_play, Hovered Texture to buttonLong_grey.png, Pressed Texture to buttonLong_beige_pressed.png and Disabled Texture to buttonLong_blue.png.
  4. Press Add component and choose AudioSourceComponent. Untick Positional, and add one entry to Triggers: Name click, Clip click_001.ogg. Its Mode stays Shuffle, and its Pitch and Volume stay 1 to 1. That is the sound a press of Start makes, as on the title. Press Add component again and choose FocusComponent: the links it holds are set later, in this chapter.
  5. Select slot_1, click + Add node and choose Button. Rename the node slot_1_delete. In its Rect, set Anchor Max to 0, 0, Offset Min to 560, 16, Offset Max to 608, 65 and Pivot to 0, 0. In its Panel, set Texture to buttonLong_beige.png and Border to 8. Press the bin beside the header of its Text to remove it: this button has a picture for a face, not a word. In its Button, set Action to slot_1_delete and its three textures as Start’s. Press Add component, choose ThemeRoleComponent and set its Role to Danger. Press Add component again and choose FocusComponent.
  6. Select slot_1_delete, click + Add node and choose Panel. Rename the node slot_1_cross. In its Rect, set Anchor Max to 0, 0, Offset Min to 8, 8, Offset Max to 40, 38 and Pivot to 0, 0. Set its Panel’s Texture to iconCross_brown.png.
  7. Select slot_1_delete again, click + Add node and choose Label. Rename the node slot_1_delete_label, and untick its Visible. In its Text, set Text to Delete, Scale to 0.75 and Align to Center.

A name field

A Text Entry is a single line of typing: a save name, a code, a search box. It shows a placeholder while it is empty, takes typing only while it has the focus, and reports its action when Enter is pressed in it. Text Entry lists its fields.

  1. Select slot_1_name. In its Text Entry, set Action to slot_1_name, Placeholder to Empty slot, Max Length to 12, Padding to 12, Scale to 0.75 and Caret Width to 3.
  2. Leave Text empty, Mode as Text and Disabled unticked: its script writes the first and the third, and chapter 16 defines all three. The six colours under Colors come from Apply theme.
  3. In Assets, press Create, choose New script, type slot_name and press Enter. In the Code tab, replace everything in it with the script below and press Ctrl+S.
  4. Select slot_1_name, press Add component and choose SlotName. Its Slot is 1, the script’s own: leave it. Its In Use is hidden.
slot_1_name selected, its Inspector narrowed with the search box, entry, to its Text Entry: Action slot_1_name, Text empty, Placeholder Empty slot, Max Length 12, Mode Text, Disabled unticked, Padding 12.00 px, Scale 0.75, Caret Width 3.00 px, and under Colors Normal white, Hovered white, Pressed light grey, Foreground dark plum, Hint a chequered translucent plum and Selection yellow
A slot’s name field: its Text Entry.
Action
A Text Entry’s name for a script. Enter in the field presses it, and entered reads the field’s text under it. A click on the field or a Shortcut presses nothing: the action means Enter. Every field has its own, slot_1_name here, so a script can tell the three apart.
Placeholder
What the Text Entry says while it is empty, drawn in its Hint colour and gone as soon as there is a letter. Empty slot, a new field’s Enter text reworded.
Max Length
How many characters the Text Entry takes; 0, as a new one has it, is no limit. 12 keeps a name short enough for the field’s box, which also holds the check at its right end. Typing past it does nothing.
Padding
How far in from the Text Entry’s edges the text starts, in UI units. 12. A new field’s is 6.
Scale
How big the Text Entry’s typed text is, as a Text’s Scale is: a share of the face’s 32 pixels. 0.75, the size of the plate’s buttons. A new field’s is 2.
Caret Width
How wide the Text Entry’s caret bar is, in UI units: 3. It is drawn in the Foreground colour and does not blink. A new field’s is 2.
Normal, Hovered, Pressed
The Text Entry’s tints, multiplied into its picture: at rest, under the pointer or with the focus, and while held. Apply theme writes them from the Theme’s Button: 255, 255, 255, alpha 255 at rest; the same lifted Hover Lift toward white, which is white again; and sunk Press Sink toward black, 211, 211, 211, alpha 255.
Foreground
The colour of what is typed, and of the caret. Apply theme writes the Theme’s Button Text: 63, 38, 49, alpha 255, a dark plum that reads on the white field.
Hint
The colour of the placeholder: the Foreground at half strength, 63, 38, 49, alpha 128, so that it reads as a prompt, not as a name somebody has to delete.
Selection
The colour behind selected text. Apply theme writes the Theme’s Panel Accent: 254, 231, 97, alpha 255, a yellow that shows against the field’s white.
The Code panel with slot_name.rgs open: a comment on a save slot’s name field, then component SlotName with its slot field and its hidden in_use field, each with a comment, and a loop that reads the slot’s saved name; the Scripts list at the left names the project’s scripts, and the Hierarchy at the far left shows the Slots scene: three slot plates under slot_plates, each with a name field, a check, Start, Delete and a cross, then back and delete_dialog
rgs slot_name.rgs
/// A save slot's name field. While the slot is in use it shows the name the
/// slot was started under, and takes no typing, so the name stays the slot's
/// until Delete frees it; then it empties for a new name.
component SlotName {
    /// The slot it names: 1, 2 or 3.
    slot: f32 = 1.0,
    /// Whether it showed the slot's name on the last step, so that it empties
    /// once, as the slot is freed, and not on every step a new name is typed.
    #[hidden]
    in_use: bool = false,

    fn loop() {
        let name = saved_text("slot" + text(slot) + ".name", "");
        if name != "" {
            set_text("TextEntryComponent", "text", name);
            set_flag("TextEntryComponent", "disabled", true);
            in_use = true;
        } else if in_use {
            set_text("TextEntryComponent", "text", "");
            set_flag("TextEntryComponent", "disabled", false);
            in_use = false;
        }
    }
}

The script reads the slot’s saved name, slot1.name for the first, with saved_text, which answers a text kept in the game’s save file, or its second argument when nothing is kept. A slot with a name is in use: the script writes the name into the field’s Text and ticks its Disabled, so the name stays the slot’s until Delete frees it. A slot without one is free: the field is empty, and in_use makes it empty exactly once, as the slot is freed, so a new name being typed is never wiped.

The save slots screen with the first field reading ZOÉ, its accent drawn, and a dark caret just after the É; the other two fields still read EMPTY SLOT in grey, and every START button is beige
A name typed: its accent drawn, the caret after it.

Click the field, or walk the ring to it, and type. Backspace and Delete take letters away, Left, Right, Home and End move the caret, Ctrl+A selects it all and Shift extends a selection; Ctrl moves and deletes by whole words. Up, Down and Tab still move the ring. A name typed in the editor’s Play or in the exported game is the same: both draw the caret and take the accents.

Saved under a slot

A game keeps its saves in one file, and a slot is a key prefix in it: slot1. for the first, slot2. and slot3. for the others. slot1.name is the first slot’s name; slot1.health, slot1.coins and the rest are what the shrine saves of its run. A slot with a name is in use. The scripts below read and write those keys with save_text, saved_text, forget and entered.

Start and Continue

  1. Make load_slot the same way: Create, New script, load_slot, Enter, paste the script below and press Ctrl+S. Select slot_1_play, press Add component and choose LoadSlot. Its Slot is 1, and its Start Health is the script’s own 10: leave it.
  2. Make shown_by_slot the same way. Select slot_1_done, add ShownBySlot, and tick its When Done. Select slot_1_delete and add ShownBySlot too: its Slot is 1, and When Done stays unticked.
rgs load_slot.rgs
/// A save slot's Start or Continue button, which opens the delve on the slot's
/// run. A slot in use carries on the run the shrine last saved in it. A free
/// slot starts a new game under the name typed in its field, by this button or
/// by Enter in the field, and starts nothing while the field is empty. The
/// game keeps each slot's saves under its own prefix: slot1. for the first,
/// slot2. and slot3. for the others.
component LoadSlot {
    /// The slot it opens: 1, 2 or 3.
    slot: f32 = 1.0,
    /// The health a new game starts with.
    start_health: f32 = 10.0,

    fn loop() {
        let saves = "slot" + text(slot) + ".";
        // Its slot's name field reports under slot_1_name, slot_2_name or
        // slot_3_name, and this button under slot_1_play and so on.
        let field = "slot_" + text(slot) + "_name";
        let name = saved_text(saves + "name", "");
        if name == "" {
            set_text("TextComponent", "text", "Start");
        } else {
            set_text("TextComponent", "text", "Continue");
        }

        // A click on this button, or Enter while it holds the focus,
        // presses its action, and Enter in the name field presses the
        // field's, each on the game's next step. A free slot starts only once
        // a name is typed in its field.
        let typed = entered(field);
        let asked = pressed("slot_" + text(slot) + "_play") || pressed(field);
        if asked && (name != "" || typed != "") {
            if name == "" {
                // A new game: the slot is the name's from now on.
                save_text(saves + "name", typed);
            }
            // The slot's run, kept where the delve reads it. What the shrine
            // never saved reads as a new game's, and a free slot holds no
            // saves at all, since Delete forgets them: so a new game, or a
            // run that never rested at the shrine, starts at full health with
            // nothing carried, nothing in the journal, every pickup back in
            // the room and the door shut.
            keep_number("health", saved_number(saves + "health", start_health));
            keep_number("potions", saved_number(saves + "potions", 0.0));
            keep_number("elixirs", saved_number(saves + "elixirs", 0.0));
            keep_number("keys", saved_number(saves + "keys", 0.0));
            keep_number("gems", saved_number(saves + "gems", 0.0));
            keep_number("coins", saved_number(saves + "coins", 0.0));
            keep_number("haste", saved_number(saves + "haste", 0.0));
            for i in 1..5 {
                let kept = "hotbar_" + text(i);
                keep_number(kept, saved_number(saves + kept, 0.0));
            }
            for i in 1..17 {
                let kept = "bag_" + text(i);
                keep_number(kept, saved_number(saves + kept, 0.0));
            }
            for i in 1..21 {
                let kept = "journal_" + text(i);
                keep_number(kept, saved_number(saves + kept, 0.0));
            }
            keep_number("journal_count", saved_number(saves + "journal_count", 0.0));
            // And the room as the run left it: each pickup taken, which stays
            // gone, and the door, which opens itself if it was opened.
            for i in 1..13 {
                let kept = "taken_" + text(i);
                keep_number(kept, saved_number(saves + kept, 0.0));
            }
            keep_number("door_open", saved_number(saves + "door_open", 0.0));
            // No rest taken at the shrine yet, and nothing being inspected.
            keep_number("rests", 0.0);
            keep_number("inspect", 0.0);
            // The slot the run is played in, where the shrine saves it and
            // the way out marks it done.
            keep_number("slot", slot);
            // Loaded here, before the delve opens, so everything in it starts
            // on the slot's run: the hero on its health, and the journal's
            // keeper taking what the journal holds as already written.
            play_sound("click");
            change_scene("delve");
        }
    }
}
The Code panel with load_slot.rgs open: a comment on a save slot’s Start or Continue button, then component LoadSlot with its slot and start_health fields and a loop that builds the slot’s key prefix, reads its saved name and sets the button’s word to Start or Continue; the Scripts list and the Hierarchy are as in the last picture

The button’s word is the slot’s state: Start while the slot has no name, Continue once it has one. A free slot starts only once a name is typed, by this button or by Enter in the field, and the button does nothing while the field is empty. A new game saves the typed name, slot1.name, and then loads the run the same way a slot in use does: every kept number from what the slot saved, or, where nothing was ever saved, what a new game starts with. That is why one path serves both, and why the slot’s health starts at 10: Start Health is the health a new game opens with.

The script uses three of the game’s answers, for the first slot. pressed("slot_1_play") is true on the step a click on Start is let go, or Enter goes down while it has the focus. pressed("slot_1_name") is true on the step Enter goes down in the field. entered("slot_1_name") is the field’s text, read every frame, whether or not anything was pressed. The delve then reads what the script kept, before it opens.

rgs shown_by_slot.rgs
/// Shows this node only while its save slot has something to show: the Delete
/// button while the slot is in use, and the check once the slot's run has
/// reached the way out.
component ShownBySlot {
    /// The slot it belongs to: 1, 2 or 3.
    slot: f32 = 1.0,
    /// Whether it waits for the run to be done, as the check does, rather than
    /// only for the slot to be in use, as the Delete button does.
    when_done: bool = false,

    fn loop() {
        let saves = "slot" + text(slot) + ".";
        // A slot is in use while it has a name, and its run is done once the
        // way out has saved done as 1.
        let shown = saved_text(saves + "name", "") != "";
        if when_done {
            shown = saved_number(saves + "done", 0.0) == 1.0;
        }
        if shown {
            show();
        } else {
            hide();
        }
    }
}

ShownBySlot shows its node only while its slot has something to show. A slot is in use while it has a name, and its run is done once the stairs have saved slot1.done as 1. So the Delete button shows while the slot is in use, and the check shows once the run is done: When Done says which of the two waits for what.

The other two slots

  1. Select slot_1 and press Ctrl+D. The copy comes selected, named slot_1 copy, with everything under it. Rename the copy slot_2, and then each node under it: slot_2_name, slot_2_done, slot_2_play, slot_2_delete, slot_2_cross and slot_2_delete_label.
  2. Select slot_1 again, press Ctrl+D, and rename the new copy and the nodes under it for slot_3 in the same way.
  3. Set what is each slot’s own, from the table below, on the nodes of slot_2 and slot_3.
SlotName fieldStartDelete
slot_1Action slot_1_name, Slot 1Action slot_1_play, Slot 1Action slot_1_delete, Slot 1
slot_2Action slot_2_name, Slot 2Action slot_2_play, Slot 2Action slot_2_delete, Slot 2
slot_3Action slot_3_name, Slot 3Action slot_3_play, Slot 3Action slot_3_delete, Slot 3

Each slot’s check, slot_2_done and slot_3_done, has its own Slot too, 2 and 3, with When Done ticked. Every other field is as the first slot’s.

Back, and the question

  1. Select slots_canvas, click + Add node and choose Menu Button. Rename the node back. In its Rect, set Anchor Min and Anchor Max both to 0.5, 0.5, Offset Min to -110, 173, Offset Max to 110, 222 and Pivot to 0.5, 0. In its Panel, set Texture to buttonLong_beige.png and Border to 8. In its Text, set Text to Back, Scale to 0.75 and Align to Center. In its Button, set Action to back and its three textures as Start’s. In its Shortcut, set Action to ui_cancel: Esc presses Back, and the button draws ESC at its right edge. Press Add component and choose FocusComponent.
  2. Add an AudioSourceComponent to back: untick Positional and add one Triggers entry, Name back, Clip back_001.ogg.
  3. Make slots_menu the same way, with the script below, and add SlotsMenu to back.
rgs slots_menu.rgs
/// The save slots' way back: Back goes to the title. Each slot starts, carries
/// on and deletes its own game.
component SlotsMenu {
    fn loop() {
        // A click on Back, Enter while it holds the focus, or Escape through
        // its Shortcut presses back on the game's next step. Escape answers
        // the open question 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("title");
        }
    }
}

pressed("back") is true on the step a click on Back is let go, Enter goes down while it has the focus, or Esc goes down through its Shortcut. A question that is open answers Esc first, as the next section shows, and Back goes on the next press.

  1. Select slots_canvas, click + Add node and choose Modal. Rename the node delete_dialog and untick its Visible. In its Rect, set Anchor Min and Anchor Max both to 0.5, 0.5, Offset Min to -320, -100 and Offset Max to 320, 100. In its Panel, set Texture to panel_beige.png and Border to 8.
  2. In its Modal, set Title to Delete this save? and Body to Its name and progress are gone for good.. Set Buttons to two answers: the first with Label Keep and Action keep_slot, the second with Label Delete and Action delete_slot, both with Disabled unticked. Set Cancel Action to keep_slot, Scrim to 63, 38, 49, alpha 115, Button Texture to buttonLong_beige.png, Button Hovered Texture to buttonLong_grey.png, Button Pressed Texture to buttonLong_beige_pressed.png, Button Border to 8, Padding to 20, Scale to 0.625, Title Height to 36, Button Height to 49 and Button Gap to 16.
  3. Make delete_dialog the same way, with the script below, and add DeleteDialog to the dialog. Its Asking is hidden, and 0 while nobody asks.
rgs delete_dialog.rgs
/// The question a save slot's Delete asks: Delete forgets everything the slot
/// keeps, its name and its run, and Keep, or Escape, keeps it.
component DeleteDialog {
    /// The slot whose Delete asked: 1, 2 or 3, or 0 while nobody asks.
    #[hidden]
    asking: f32 = 0.0,

    fn loop() {
        // A click on a slot's Delete, or Enter while it holds the focus,
        // presses slot_1_delete, slot_2_delete or slot_3_delete on the game's
        // next step.
        for i in 1..4 {
            if pressed("slot_" + text(i) + "_delete") {
                asking = i;
                show();
            }
        }
        // A click on an answer, or Enter on the one the arrows chose, presses
        // its action; Escape presses the Cancel Action, keep_slot.
        if pressed("keep_slot") {
            asking = 0.0;
            hide();
        }
        if pressed("delete_slot") {
            // Every key the slot keeps, forgotten: the name, which frees the
            // slot, what the way out marked, and what the shrine saved of the
            // run and the room.
            let saves = "slot" + text(asking) + ".";
            forget(saves + "name");
            forget(saves + "done");
            forget(saves + "health");
            forget(saves + "potions");
            forget(saves + "elixirs");
            forget(saves + "keys");
            forget(saves + "gems");
            forget(saves + "coins");
            forget(saves + "haste");
            for i in 1..5 {
                forget(saves + "hotbar_" + text(i));
            }
            for i in 1..17 {
                forget(saves + "bag_" + text(i));
            }
            for i in 1..21 {
                forget(saves + "journal_" + text(i));
            }
            forget(saves + "journal_count");
            for i in 1..13 {
                forget(saves + "taken_" + text(i));
            }
            forget(saves + "door_open");
            asking = 0.0;
            hide();
        }
    }
}
The save slots screen after a run: the first slot’s field reads ANA, greyed, with a bronze check mark at its right end; its button reads CONTINUE and is ringed in gold, with a red square DELETE button holding a cross beside it; the other two slots still read EMPTY SLOT and START, and BACK is under them
A run started as Ana and ended on the stairs: Continue, and the check.
The same screen dimmed behind a beige dialog headed DELETE THIS SAVE?, asking ITS NAME AND PROGRESS ARE GONE FOR GOOD., with two answers, KEEP and DELETE, side by side
Delete: a question first.

pressed("slot_1_delete") is true on the step a click on the slot’s Delete is let go, or Enter goes down while it has the focus: the script remembers which slot asked and shows the question. pressed("keep_slot") and pressed("delete_slot") are true on the step a click on an answer is let go, or Enter goes down on the one the arrows chose, and Esc presses the Cancel Action, keep_slot, so the safe answer is also the way out. Delete forgets every key the slot keeps, its name and its run, and the slot is free again.

A forget takes one key. There is no “forget everything that starts with slot1.”, so the script lists them: the name, the mark the way out left, the health and the carried numbers, and, in loops, the hotbar’s four slots, the bag’s sixteen and the journal’s twenty lines, then the twelve pickups the room has lost and the door.

Walking between the slots

  1. Set the Up and Down of the Focus on the three controls of each slot, and on Back, from the table below. They are names, so every node they name must exist first.
  2. Select slots_canvas and press Apply theme. It writes the Theme’s colours into every control on the screen, by role: the Panels, the words, the buttons’ three tints, the fields’ six colours, the dialog’s and the ring’s.
NodeUpDown
slot_1_name, slot_1_play, slot_1_deletebackslot_2_play
slot_2_name, slot_2_play, slot_2_deleteslot_1_playslot_3_play
slot_3_name, slot_3_play, slot_3_deleteslot_2_playback
backslot_3_playslot_1_play

The ring goes from plate to plate through Start or Continue, the one control every slot always offers. A free slot has no Delete, and a slot in use has a locked field that refuses the focus, so a link to either would point at a control that is hidden or will not take it. Left and right along a plate are right by their places, and a field keeps them for its caret anyway.

The title’s Play

  1. Open scenes/title.ron. Select new_game and rename it play. In its Text, set Text to Play. In its Button, set Action to play.
  2. Select settings. In its Focus, set Up and Previous to play. Select quit and set its Down and Next to play. play keeps its own links, to quit and settings.
  3. In the Code tab’s Scripts list, click title_menu.rgs, replace everything in it with the script below and press Ctrl+S. Press Ctrl+S for the scene too.
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, 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("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 menu no longer starts a run. Play opens the slots, and the slot’s Start or Continue starts it, so the eight lines that emptied the journal and put the numbers back, and the field that held the starting health, have moved into load_slot.rgs. TitleMenu keeps no fields.

The room keeps what it lost

A slot saves the hero’s numbers, and a game carried on has to give the room back as it was, with each pickup the hero took still gone and the door still open. So each pickup is told which one it is, and the door, the way out, the shrine and the pickups themselves change a little.

  1. Open scenes/delve.ron. In pickups, select each pickup and set the Index of its Pickup from the table below.
PickupIndex
potion_11
potion_22
elixir3
key4
gem_green5
gem_red6
gem_blue7
coin_18
coin_29
coin_310
coin_411
coin_512
  1. In the Scripts list, replace pickup.rgs, door.rgs, exit.rgs and shrine.rgs with the scripts below, and the comment above component QuitDialog { in quit_dialog.rgs with the four lines after them, pressing Ctrl+S for each.
rgs pickup.rgs
/// Something lying in the room that the hero takes by walking into it. It adds
/// `amount` to the number kept under `kind`, and is gone. A potion, the elixir
/// and the key each need a slot to go in, so one stays on the floor while the
/// satchel has no room for it. Once taken it stays gone for the rest of the
/// run, and from a slot's Continue too, once the shrine has saved it.
component Pickup : Area {
    /// The kept number it counts into: potions, elixirs, keys, gems or coins.
    kind: String = "gems",
    /// How much it adds to that number.
    amount: f32 = 1.0,
    /// The item a slot shows for it: 1 a potion, 2 the elixir, 3 a key, or 0
    /// for what is only counted, as gems and coins are.
    item: f32 = 0.0,
    /// Which of the room's pickups it is, from 1 to 12, each its own: the
    /// game keeps it taken as taken_1 to taken_12.
    index: f32 = 0.0,

    fn on_create() {
        // Taken already, earlier in this run or in the run a slot gave back:
        // it is not in the room.
        if kept_number("taken_" + text(index), 0.0) == 1.0 {
            destroy_self();
        }
    }

    fn on_enter(other: NodeId) {
        // The hero's node is in the group "hero".
        if in_group(other, "hero") {
            // The room the satchel keeps: a key goes only in the bag, a potion
            // or the elixir in the hotbar or the bag.
            let room = kept_number("room_bag", 0.0);
            if item == 1.0 || item == 2.0 {
                room += kept_number("room_hotbar", 0.0);
            }
            if item == 0.0 || room >= amount {
                add_number(kind, amount);
                // Kept as taken, 1, where the shrine saves it.
                keep_number("taken_" + text(index), 1.0);
                destroy_self();
            }
        }
    }
}
rgs door.rgs
/// A locked door. The hero opens it by walking into it with a key, which the
/// door keeps: it shows itself open, no longer stands in the way, and keeps
/// door_open, so that the journal can say it opened and the shrine can save
/// it. A door opened earlier in the run, or in the run a slot gave back, is
/// open as the room opens.
component Door : Area {
    /// The piece of the sheet it shows once open.
    open_region: String = "door_open",
    /// Whether it has been opened.
    #[hidden]
    open: bool = false,

    fn on_create() {
        // Opened already: open as the key left it, and it takes no key again.
        if kept_number("door_open", 0.0) == 1.0 {
            set_text("SpriteComponent", "region_name", open_region);
            set_flag("ColliderComponent", "solid", false);
            open = true;
        }
    }

    fn on_enter(other: NodeId) {
        if in_group(other, "hero") && !open && kept_number("keys", 0.0) >= 1.0 {
            add_number("keys", -1.0);
            set_text("SpriteComponent", "region_name", open_region);
            set_flag("ColliderComponent", "solid", false);
            open = true;
            // Kept as the counts are, where the journal's keeper reads it: 1
            // once the door is open.
            keep_number("door_open", 1.0);
        }
    }
}
rgs exit.rgs
/// The way out, down the stairs. The hero who steps onto them leaves the delve
/// for the title, and the slot the run is played in is marked done.
component Exit : Area {
    fn on_enter(other: NodeId) {
        if in_group(other, "hero") {
            // 0 for a delve opened on its own, which no slot holds.
            let slot = kept_number("slot", 0.0);
            if slot > 0.0 {
                // The save slots show a check on a slot whose run is done.
                save_number("slot" + text(slot) + ".done", 1.0);
            }
            change_scene("title");
        }
    }
}
rgs shrine.rgs
/// The shrine, a gargoyle over a stone basin. The hero who steps up to it
/// rests there, the journal says so, and the run is saved in the slot it is
/// played in, for the slot's Continue to give back.
component Shrine : Area {
    fn on_enter(other: NodeId) {
        if in_group(other, "hero") {
            // Counted where the journal's keeper reads it: one more rest each
            // time the hero steps up to the shrine.
            add_number("rests", 1.0);
            // The slot the run is played in, which its Start or Continue
            // keeps: 0 for a delve opened on its own, which no slot holds,
            // so nothing is saved.
            let slot = kept_number("slot", 0.0);
            if slot > 0.0 {
                // The keeper writes the rest down on a step soon after this
                // one, so the save waits a moment to hold that line too.
                await wait(0.25);
                let saves = "slot" + text(slot) + ".";
                save_number(saves + "health", kept_number("health", 0.0));
                save_number(saves + "potions", kept_number("potions", 0.0));
                save_number(saves + "elixirs", kept_number("elixirs", 0.0));
                save_number(saves + "keys", kept_number("keys", 0.0));
                save_number(saves + "gems", kept_number("gems", 0.0));
                save_number(saves + "coins", kept_number("coins", 0.0));
                save_number(saves + "haste", kept_number("haste", 0.0));
                for i in 1..5 {
                    let kept = "hotbar_" + text(i);
                    save_number(saves + kept, kept_number(kept, 0.0));
                }
                for i in 1..17 {
                    let kept = "bag_" + text(i);
                    save_number(saves + kept, kept_number(kept, 0.0));
                }
                for i in 1..21 {
                    let kept = "journal_" + text(i);
                    save_number(saves + kept, kept_number(kept, 0.0));
                }
                save_number(saves + "journal_count", kept_number("journal_count", 0.0));
                // And what the room has lost: each pickup taken, and the door
                // if it is open.
                for i in 1..13 {
                    let kept = "taken_" + text(i);
                    save_number(saves + kept, kept_number(kept, 0.0));
                }
                save_number(saves + "door_open", kept_number("door_open", 0.0));
            }
        }
    }
}
rgs part of quit_dialog.rgs
/// The question Quit to title asks before the delve is left: Leave goes back
/// to the title, and Stay, or Escape, stays in the paused game. The run is
/// saved only at the shrine, so leaving loses what happened since the hero
/// last rested there.

Each pickup keeps itself taken, as taken_ and its Index, in the numbers the game keeps, and is gone as the room opens if it was taken already. The door keeps door_open, and opens itself as the room opens if it is 1. The stairs mark the slot the run is played in as done, with slot1.done, which is what shows the check. quit_dialog.rgs changes only its comment: leaving now loses what happened since the hero last rested at the shrine, not the whole run, and its old last sentence was no longer true.

The shrine is where a run is saved. It rests the hero, as the journal says, and then saves every number the run holds under the slot’s prefix: health, the carried items, the hotbar, the bag, the journal and what the room has lost. A delve opened on its own, with no slot, saves nothing.

The slots screen, the field and the scripts’ fields

The slots screen

NodeComponentFieldValue
cameraCameraScalingLetterbox
Extent180
Width320
Background118, 59, 54, alpha 255
slots_canvasCanvasScale ModeShrinkToFit
ThemePanel255, 255, 255, alpha 255
Panel Accent254, 231, 97, alpha 255
Panel Danger232, 69, 55, alpha 255
Text236, 227, 206, alpha 255
Text Accent254, 174, 52, alpha 255
Text Danger255, 228, 220, alpha 255
Button255, 255, 255, alpha 255
Button Text63, 38, 49, alpha 255
Hover Lift0.15
Press Sink0.35
Focus RingColor254, 174, 52, alpha 255, written by Apply theme
TextSizeSmall1536, 864
Normal1280, 720
Large1066, 600
slots_nameRectAnchor Min0.5, 0.5
Anchor Max0.5, 0.5
Offset Min-360, -222
Offset Max360, -142
Pivot0.5, 1
TextTextSave slots
Scale1.5
Color254, 174, 52, alpha 255, written by Apply theme
AlignCenter
Outline63, 38, 49, alpha 255
Outline Width2
Shadow0, 0, 0, alpha 153
Shadow Offset3, 3
Shadow Softness0
Theme RoleRoleAccent
slot_platesRectAnchor Min0.5, 0.5
Anchor Max0.5, 0.5
Offset Min-312, -126
Offset Max312, 149
Pivot0.5, 0
StackFitFixed
Spacing16
Item81
slot_1, slot_2 and slot_3PanelColor255, 255, 255, alpha 255, written by Apply theme
Texturepanel_brown.png
Border8
backRectAnchor Min0.5, 0.5
Anchor Max0.5, 0.5
Offset Min-110, 173
Offset Max110, 222
Pivot0.5, 0
PanelColor255, 255, 255, alpha 255, written by Apply theme
TexturebuttonLong_beige.png
Border8
TextTextBack
Scale0.75
Color63, 38, 49, alpha 255, written by Apply theme
AlignCenter
ButtonActionback
Normal255, 255, 255, alpha 255, written by Apply theme
Hovered255, 255, 255, alpha 255, written by Apply theme
Pressed211, 211, 211, alpha 255, written by Apply theme
Hovered TexturebuttonLong_grey.png
Pressed TexturebuttonLong_beige_pressed.png
Disabled TexturebuttonLong_blue.png
ShortcutActionui_cancel
Audio SourcePositionalunticked
Triggersone entry: Name back, Clip back_001.ogg, Mode Shuffle, Pitch 1 to 1, Volume 1 to 1
FocusUpslot_3_play
Downslot_1_play
delete_dialogNodeVisibleunticked
RectAnchor Min0.5, 0.5
Anchor Max0.5, 0.5
Offset Min-320, -100
Offset Max320, 100
PanelColor255, 255, 255, alpha 255, written by Apply theme
Texturepanel_beige.png
Border8
ModalTitleDelete this save?
BodyIts name and progress are gone for good.
Buttonstwo answers: Label Keep, Action keep_slot; Label Delete, Action delete_slot; Disabled unticked on both
Cancel Actionkeep_slot
Scrim63, 38, 49, alpha 115
Normal255, 255, 255, alpha 255, written by Apply theme
Hovered255, 255, 255, alpha 255, written by Apply theme
Pressed211, 211, 211, alpha 255, written by Apply theme
Foreground63, 38, 49, alpha 255, written by Apply theme
Button TexturebuttonLong_beige.png
Button Hovered TexturebuttonLong_grey.png
Button Pressed TexturebuttonLong_beige_pressed.png
Button Border8
Padding20
Scale0.625
Title Height36
Button Height49
Button Gap16

A name field

NodeComponentFieldValue
slot_1_name, slot_2_name and slot_3_nameRectAnchor Min0, 0
Anchor Max0, 0
Offset Min16, 16
Offset Max336, 65
Pivot0, 0
PanelColor255, 255, 255, alpha 255, written by Apply theme
Textureinput_rectangle.png
Border8
Text EntryActionslot_1_name, slot_2_name and slot_3_name
PlaceholderEmpty slot
Max Length12
Padding12
Scale0.75
Caret Width3
Normal255, 255, 255, alpha 255, written by Apply theme
Hovered255, 255, 255, alpha 255, written by Apply theme
Pressed211, 211, 211, alpha 255, written by Apply theme
Foreground63, 38, 49, alpha 255, written by Apply theme
Hint63, 38, 49, alpha 128, written by Apply theme
Selection254, 231, 97, alpha 255, written by Apply theme
slot_1_done, slot_2_done and slot_3_doneRectAnchor Min1, 0
Anchor Max1, 0
Offset Min-40, 9
Offset Max-8, 39
Pivot1, 0
PanelColor255, 255, 255, alpha 255, written by Apply theme
TextureiconCheck_bronze.png

The Start and Delete buttons

NodeComponentFieldValue
slot_1_play, slot_2_play and slot_3_playRectAnchor Max0, 0
Offset Min348, 16
Offset Max548, 65
Pivot0, 0
PanelColor255, 255, 255, alpha 255, written by Apply theme
TexturebuttonLong_beige.png
Border8
TextTextStart
Scale0.75
Color63, 38, 49, alpha 255, written by Apply theme
AlignCenter
ButtonActionslot_1_play, slot_2_play and slot_3_play
Normal255, 255, 255, alpha 255, written by Apply theme
Hovered255, 255, 255, alpha 255, written by Apply theme
Pressed211, 211, 211, alpha 255, written by Apply theme
Hovered TexturebuttonLong_grey.png
Pressed TexturebuttonLong_beige_pressed.png
Disabled TexturebuttonLong_blue.png
Audio SourcePositionalunticked
Triggersone entry: Name click, Clip click_001.ogg, Mode Shuffle, Pitch 1 to 1, Volume 1 to 1
slot_1_delete, slot_2_delete and slot_3_deleteRectAnchor Max0, 0
Offset Min560, 16
Offset Max608, 65
Pivot0, 0
PanelColor232, 69, 55, alpha 255, written by Apply theme
TexturebuttonLong_beige.png
Border8
ButtonActionslot_1_delete, slot_2_delete and slot_3_delete
Normal232, 69, 55, alpha 255, written by Apply theme
Hovered236, 124, 118, alpha 255, written by Apply theme
Pressed192, 55, 44, alpha 255, written by Apply theme
Hovered TexturebuttonLong_grey.png
Pressed TexturebuttonLong_beige_pressed.png
Disabled TexturebuttonLong_blue.png
Theme RoleRoleDanger
ShownBySlotWhen Doneunticked
slot_1_cross, slot_2_cross and slot_3_crossRectAnchor Max0, 0
Offset Min8, 8
Offset Max40, 38
Pivot0, 0
PanelColor255, 255, 255, alpha 255, written by Apply theme
TextureiconCross_brown.png
slot_1_delete_label, slot_2_delete_label and slot_3_delete_labelNodeVisibleunticked
TextTextDelete
Scale0.75
Color236, 227, 206, alpha 255, written by Apply theme
AlignCenter

The slots’ scripts’ fields

NodeComponentFieldValue
slot_1_name, slot_2_name and slot_3_nameSlotNameSlot1, 2 and 3
slot_1_play, slot_2_play and slot_3_playLoadSlotSlot1, 2 and 3
Start Health10
slot_1_done, slot_2_done and slot_3_doneShownBySlotSlot1, 2 and 3
When Doneticked
slot_1_delete, slot_2_delete and slot_3_deleteShownBySlotSlot1, 2 and 3
When Doneunticked
delete_dialogDeleteDialogAsking0, hidden
potion_1PickupIndex1
potion_2PickupIndex2
elixirPickupIndex3
keyPickupIndex4
gem_greenPickupIndex5
gem_redPickupIndex6
gem_bluePickupIndex7
coin_1PickupIndex8
coin_2PickupIndex9
coin_3PickupIndex10
coin_4PickupIndex11
coin_5PickupIndex12

The title’s Play

NodeComponentFieldValue
new_gameNodeNameplay
TextTextPlay
ButtonActionplay
settingsFocusUpplay
Previousplay
quitFocusDownplay
Nextplay

The three slots’ other nodes are the first slot’s, with their own numbers. Every other field of every node is as it comes.

Check it works