Skip to content

Tutorial 11 Custom UI Screen

bryanthaboi edited this page Jul 19, 2026 · 1 revision

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.

Clone this wiki locally