1
Tutorial 11 Custom UI Screen
bryanthaboi edited this page 2026-07-19 16:19:15 -04:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Tutorial 11 — Custom UI screen

Goal. Add a Quest Log screen, open it from the Start menu, and open it from a script command.

Prerequisites. Tutorials 08 and 10; Reference: The Mod Object (the mod.ui toolkit).

Files created.

mods/tutorial_11_ui/
├── manifest.json
└── main.lua

Steps

  1. Register the screen. The screens registry maps an id to a factory (function or { new = fn }), resolved by src/ui/Screens.lua. A screen is a state-stack table: optional enter/exit/update/draw/isOpaque, input polled from game.input:

    return function(mod)
      mod.content.screens:register("QuestLog", {
        new = function(game)
          local Font = mod.ui.Font
          local self = { game = game, isOpaque = true }
    
          function self:update(dt)
            if game.input:wasPressed("b") or game.input:wasPressed("a") then
              game.stack:pop()
            end
          end
    
          function self:draw()
            Font.drawBox(0, 0, 20, 18)          -- full-screen GB frame
            Font.draw("QUEST LOG", 16, 16)
            local stage = mod.save:get("stage", 0)
            Font.draw(("NUGGET QUEST: %d"):format(stage), 16, 40)
            Font.draw("PRESS B TO EXIT", 16, 128)
          end
    
          return self
        end,
      })
    end
    

    isOpaque = true stops the overworld drawing underneath; the GB canvas is 160×144 with the frame drawn in 8px tiles. Outcome: the screen exists but nothing opens it yet.

  2. Add a Start-menu row. The menu-assembly hook hands you the item list; insert against a stable label with the mod.ui helpers:

    mod.hooks:wrap("ui.start_menu.items", function(next, game, items)
      mod.ui.insertBefore(items, "OPTION", {
        label = "QUESTS",
        onSelect = function() mod.ui.push(game, "QuestLog") end,
      })
      return next(game, items)
    end)
    

    Outcome: a QUESTS row sits above OPTION and opens the log. With the mod disabled the row is gone — the hook chain is empty.

  3. Open it from a script. The push_screen command instantiates through the same registry and blocks until the screen pops:

    mod.content.map_scripts:register("PALLET_TOWN", {
      talk = {
        TEXT_TUT8_GIVER = {
          { "show_text", "Let me show you\nyour progress." },
          { "push_screen", "QuestLog" },
          { "show_text", "Keep at it!" },
        },
      },
    })
    

    Outcome: the quest giver pops the log mid-conversation, and the dialogue resumes when the player closes it.

Checkpoint

QUESTS appears in the Start menu with the mod on and is absent with it off; the screen opens from both the menu and the script, draws in the GB aesthetic, and closes on B. A screen factory that throws degrades: the error is logged against the mod and, for an overridden builtin id, the builtin screen is used instead (src/ui/Screens.lua).

Common pitfalls

  • Returning a non-table from a ui.* hook. The engine keeps the vanilla list and logs the mistake; always return next(game, items) (or the items table).
  • Forgetting isOpaque. The overworld bleeds through your screen. Set it unless you want an overlay.
  • Hard-coding row positions. Menus are hook-assembled; anchor with insertBefore/insertAfter on stable labels, not indices.
  • Reading LÖVE input directly. Poll game.input:wasPressed(...) in update — it respects the bindings menu and controllers.

Next: Tutorial 12 — Mini Total Conversion.