Tutorial 11: Custom UI screen
Make: Open a quest log from the Start menu or scientist and show the same quest progress that the dialogue saves.
Before you start: Advanced. Complete Tutorial 08 and Events and Hooks. Use its same tutorial_world mod and save. UI means user interface: the screens and controls a player uses.
Game: Red, local single-player. Use a practice save for experiments. If file creation is new to you, start with Tutorial 00; unfamiliar words are in the Glossary.
1. Set up the files
Tutorials 06, 07, 08, and 11 are versions of one mod, tutorial_world. Install exactly one snapshot. Begin with Tutorial 08's finished folder. Copy your current files somewhere outside the mods directory before replacing them. Keep the same mod ID and save slot as you progress.
For a fresh start, these complete starting files match this lesson's prerequisite: manifest.json, main.lua. Replace them with the finished files below as you complete the lesson.
This is a mod folder, not the engine root. Do not replace the engine's own main.lua. Preserve filenames and avoid an extra .txt extension.
Finished folder layout:
mods/tutorial_world/
main.lua
manifest.json
Each link opens a complete file you can save at the matching path. The JSON and Lua are also printed in full below; do not append them to another lesson's entry function.
| File in this mod | Complete example |
|---|---|
main.lua |
Download main.lua |
manifest.json |
Download manifest.json |
2. Describe the mod
Complete file: replace tutorial_world/manifest.json:
{
"id": "tutorial_world",
"name": "Tutorial World",
"version": "1.0.0",
"entry": "main.lua",
"api": 2,
"games": [
"red"
]
}
id is the stable internal name, name is the label in MODS, and entry tells the loader which Lua file to open. games limits these examples to Red. Keep commas between JSON fields and no comma after the last field.
3. Write the change
Complete file: replace tutorial_world/main.lua:
return function(mod)
local blocks = {}
for i = 1, 30 do blocks[i] = 10 end
blocks[8], blocks[9] = 11, 11
local pallet = mod.content.maps:get("PALLET_TOWN")
local palletBlocks = {}
for i, block in ipairs(pallet.blocks) do palletBlocks[i] = block end
palletBlocks[7 * pallet.width] = 10
mod.content.maps:register("TUTORIAL_ROUTE", {
id = "TUTORIAL_ROUTE", label = "TutorialRoute", index = 1000,
tileset = "OVERWORLD", width = 6, height = 5, blocks = blocks,
borderBlock = 10, connections = { west = { map = "PALLET_TOWN", offset = -2 } },
objects = {
{
index = 91, x = 4, y = 2, sprite = "SPRITE_POKE_BALL",
movement = "STAY", range = "NONE", item = "NUGGET",
text = "TEXT_TUTORIAL_NUGGET", name = "TUTORIAL_ROUTE_NUGGET",
},
},
})
mod.content.maps:patch("PALLET_TOWN", {
connections = { east = { map = "TUTORIAL_ROUTE", offset = 2 } },
blocks = palletBlocks,
objects = { __append = {
{
index = 90, x = 8, y = 7, sprite = "SPRITE_BEAUTY",
movement = "STAY", range = "NONE", text = "TEXT_TUTORIAL_WORLD",
name = "TUTORIAL_WORLD_NPC",
},
{
index = 92, x = 12, y = 8, sprite = "SPRITE_SCIENTIST",
movement = "STAY", range = "NONE", text = "TEXT_TUTORIAL_QUEST",
name = "TUTORIAL_QUEST_GIVER",
},
} },
})
mod.content.encounters:register("TUTORIAL_ROUTE", {
grass = { rate = 25, slots = {
{ species = "PIDGEY", level = 3 }, { species = "RATTATA", level = 3 },
{ species = "PIDGEY", level = 4 }, { species = "RATTATA", level = 4 },
{ species = "PIDGEY", level = 5 }, { species = "RATTATA", level = 5 },
{ species = "PIDGEY", level = 3 }, { species = "RATTATA", level = 3 },
{ species = "PIDGEY", level = 6 }, { species = "RATTATA", level = 6 },
} },
})
mod.content.map_scripts:register("PALLET_TOWN", {
talk = {
TEXT_TUTORIAL_WORLD = {
{ "face_player" }, { "show_text", "Welcome to the\nTutorial World!" },
},
TEXT_TUTORIAL_QUEST = {
{ "push_screen", "TutorialQuestLog" },
{ "check_flag", "mod:done" }, { "jump_if_true", "thanks" },
{ "check_flag", "mod:stage" }, { "jump_if_true", "pending" },
{ "set_field", "mod:stage", true },
{ "show_text", "Please bring me\na NUGGET." }, { "jump", "end" },
{ "label", "pending" }, { "check_item", "NUGGET" },
{ "jump_if_false", "wait" }, { "take_item", "NUGGET", 1 },
{ "set_field", "mod:done", true }, { "show_text", "Quest complete!" },
{ "jump", "end" }, { "label", "wait" },
{ "show_text", "The NUGGET is on\nTUTORIAL ROUTE." }, { "jump", "end" },
{ "label", "thanks" }, { "show_text", "Thanks again!" },
{ "label", "end" },
},
},
})
mod.content.screens:register("TutorialQuestLog", {
new = function(game)
local Font = mod.ui.Font
local state = { isOpaque = true }
function state:update()
if game.input:wasPressed("b") then game.stack:pop() end
end
function state:draw()
Font.drawBox(0, 0, 20, 18)
Font.draw("QUEST LOG", 16, 16)
local status
if mod.save:get("done", false) then
status = "NUGGET QUEST: DONE"
elseif mod.save:get("stage", false) then
status = "NUGGET QUEST: ACTIVE"
else
status = "NUGGET QUEST: NOT STARTED"
end
Font.draw(status, 16, 40)
Font.draw("PRESS B TO EXIT", 16, 128)
end
return state
end,
})
mod.hooks:wrap("ui.start_menu.items", function(next, game, items)
local out = next(game, items)
if type(out) ~= "table" then return out end
return mod.ui.insertBefore(out, "OPTION", {
label = "QUESTS",
onSelect = function() mod.ui.push(game, "TutorialQuestLog") end,
})
end)
end
Understand what you wrote
This complete file includes the whole quest from Tutorial 08. The new portion registers TutorialQuestLog, adds it to the Start menu, and opens it at the beginning of the scientist's existing conversation. Keeping one talk entry preserves the quest rather than replacing it with a competing script.
Create. new(game) builds one screen state each time it opens. The table stores isOpaque = true, telling the stack that this screen covers what is underneath. game provides input and the screen stack.
Draw. draw() draws a box, title, progress label, and exit hint. The box uses UI grid units; text positions use pixels. It reads mod.save for stage and done, the same keys written by Tutorial 08's mod:stage and mod:done commands. The stable manifest ID is what makes the dialogue and screen share ownership.
Handle input. update() checks whether the player pressed B. It pops this screen from the stack so the underlying menu or conversation can continue. A drawn exit hint alone would not make the screen closable.
Open. The menu hook first calls next(game, items) to let the existing menu build its entries. It inserts QUESTS before OPTION and returns the resulting list. The item's onSelect opens the registered screen. The scientist uses the script command push_screen to open that same screen before continuing the normal quest commands.
Read the order. On the first talk, the screen opens before the scientist accepts the quest. It should show NOT STARTED. Close it to hear the request, then reopen from the menu to see ACTIVE. Similarly, close the active screen before handing in the Nugget; reopen afterward to see DONE.
4. Load and check it
Save every file, restart the app, enable the mod for Red in the launcher's MODS panel, and launch. Resolve any load error before checking gameplay. Troubleshooting shows where errors appear. With default desktop bindings, A is Z/Enter/Space, B is X/Backspace, and Start is Escape/keypad Enter; use your configured controls if different.
Optional source-checkout validation
If you have Python 3, LuaJIT, and the engine source with Red imported, open a terminal in the engine root containing tools/. For a mod installed in that checkout's mods directory, run:
python3 tools/modkit.py validate tutorial_world --base imported
If you saved it elsewhere, replace the mod ID with the quoted full path to its folder. A validation error identifies a file or content rule to fix; a pass means you can continue to the in-game checks. It does not prove rendering, sound, or the entire playthrough works. Packaged-app users can use MODS errors and the checkpoints without setting up these command-line tools.
Checkpoint
- Replace the Tutorial 08 files with this snapshot, keeping the same tutorial_world ID. Launch its practice save and open the Start menu. Select QUESTS.
- Before accepting the request, expect NUGGET QUEST: NOT STARTED. Press B to close. After accepting but before completing, expect ACTIVE. After the hand-in, expect DONE.
- Talk to the scientist. The screen should open; close it with B. The expected request, hint, completion, or thank-you dialogue should still follow.
- Complete the quest, save, close, and reload the same slot. QUESTS should still show DONE. Verify that closing the screen returns control every time.
Common pitfalls
| What you see | What to check |
|---|---|
| Progress differs from the scientist's dialogue | Both must be in tutorial_world and use stage/done. Replace the entire file; do not enable a separate Tutorial 11 mod with another ID. |
| Scientist opens the screen but does not speak | Press B to close it; the screen intentionally pauses the conversation. |
| Screen opens but menu item is absent | Check that the wrapper returns the list from insertBefore and preserves the next call. |
For syntax, missing-file, and restart problems, use Troubleshooting. Compare the whole finished file before changing several things at once.
Undo and continue
Keep the practice save and its matching mod files together. Saves can contain this example's custom content or progress. To compare with vanilla Red, disable the mod, restart, and open a separate unmodified practice slot. Do not overwrite the only copy of a modded save while its content is disabled. Save recovery explains backups.
Mod Object documents UI helpers, while UI and Infrastructure covers more screen/menu recipes. Keep drawing, input, and saved progress separate when expanding this screen.
Return to the tutorial index to choose the next lesson.
Start here
Playing
Tutorials
- Choose a tutorial
- 01 Sprite and Text Tweak
- 02 Balance Patch
- 03 New Species
- 04 New Item and Ball
- 05 New Move
- 06 NPC and Dialogue
- 07 New Map
- 08 Quest
- 09 Custom Music
- 10 New Mechanic
- 11 Custom UI Screen
- 12 Mini Total Conversion
Creative guides
Concepts
Recipes and reference
Author and developer tools
Home · Start Playing · Glossary · Troubleshooting · Repository · Community
Game data is imported locally from a supported ROM supplied by the player. Shared mods contain original work or transform recipes, never extracted game content. Check each guide's target game before using its examples.