Table of Contents
- Cookbook — UI and infrastructure
- R31 — Add a Start-menu entry
- R32 — Register a custom screen
- R33 — Persist mod state
- R34 — Define a mod option
- R35 — Export an inter-mod API
- R37 — Reshape Oak's intro speech
- R45 — Bag wrap, page-jump, hold-to-scroll
- R46 — Digits on the naming screen
- R47 — Wider survey zoom
- R48 — Super Game Boy border
- R54 - React to real-world steps
Cookbook — UI and infrastructure
The mod object surface: Reference: The Mod Object;
worked path: Tutorial 11.
R31 — Add a Start-menu entry
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)
Checkpoint: the row appears with the mod on, is gone with it off.
Anchor on stable labels; a missing anchor appends. The same pattern
serves ui.party.submenu, ui.options.rows, and ui.pc.items.
R32 — Register a custom screen
mod.content.screens:register("QuestLog", {
new = function(game)
local self = { isOpaque = true }
function self:update(dt)
if game.input:wasPressed("b") then game.stack:pop() end
end
function self:draw()
mod.ui.Font.drawBox(0, 0, 20, 18)
mod.ui.Font.draw("QUEST LOG", 16, 16)
end
return self
end,
})
Checkpoint: mod.ui.push(game, "QuestLog") opens it, and the
push_screen script command reaches it from any script row. A factory
that throws is logged against the mod, never a dead end.
R33 — Persist mod state
mod.save:set("visits", (mod.save:get("visits", 0)) + 1)
Checkpoint: the value survives save/load under save.modData[<id>] —
your namespace alone, quarantine-proof and removed cleanly when the mod
is gone (Save Model).
R34 — Define a mod option
mod.options:define({
{ key = "hard_mode", type = "toggle", label = "HARD MODE", default = false },
})
if mod.options:get("hard_mode") then
mod.content.moves:patch("TACKLE", { power = 50 })
end
Checkpoint: the toggle renders in the manager under your mod; changes
persist in options.lua and fire mod.options_changed. Row types:
toggle, choice, number, text.
R35 — Export an inter-mod API
-- in colorlib/main.lua
mod.exports.tint = function(hex) ... end
-- in a dependent mod (manifest: "dependencies": ["colorlib@^1.0"])
local color = mod.find("colorlib")
if color then color.exports.tint("#ff0044") end
Checkpoint: the dependent loads after colorlib and calls its export;
with colorlib absent, find returns nil and the branch skips.
Range-check color.version with require("src.mods.Semver") (a
supported require) before relying on a shape.
R37 — Reshape Oak's intro speech
mod.hooks:wrap("intro.oak_speech.build", function(next, steps, speech)
steps = next(steps, speech)
mod.ui.insertStepAfter(steps, "demo_mon", {
id = "show_mew",
kind = "say",
pic = { type = "pokemon", id = "MEW" },
reveal = "wipe",
cry = "MEW",
text = "This is MEW.\nKeep it quiet.",
})
mod.ui.insertStepBefore(steps, "ask_rival_name", {
id = "snack_pick",
kind = "choice",
pic = "oak",
saveKey = "snack",
text = "Pick a snack.",
choices = { "BERRIES", "LEFTOVERS", "OLD ROD" },
})
return steps
end)
mod.events:on("intro.oak_speech.answered", function(ev)
if ev.saveKey then mod.save:set(ev.saveKey, ev.value) end
end)
Checkpoint: NEW GAME shows the MEW beat and the snack menu; the pick
survives under save.modData[<id>].snack. Anchor on vanilla step ids
(oak_welcome, demo_mon, name_player, ... — see
Reference: Hooks). Pics take "oak" / "rival" /
"player", a species id, or { type = "image", path = mod.path .. "/art.png" }.
Worked example: mods/examples/example_silly_oak.
R45 — Bag wrap, page-jump, hold-to-scroll
mod.hooks:wrap("ui.list_menu", function(next, opts, ctx)
opts = next(opts, ctx) or opts
if ctx.kind == "bag" then
opts.wrap, opts.pageJump, opts.keyRepeat = true, true, true
end
return opts
end)
Checkpoint: the bag wraps top↔bottom, Left/Right jump a page, and
holding Up/Down scrolls. Other lists (kind unset) stay vanilla.
R46 — Digits on the naming screen
mod.hooks:wrap("ui.naming.grid", function(next, grid, ctx)
grid = next(grid, ctx)
local out = {}
for i, row in ipairs(grid) do out[i] = row end
out[4] = { "0", "1", "2", "3", "4", "5", "6", "7", "8" }
return out
end)
Checkpoint: NEW NAME can pick digits. Keep an "ED" cell and a
single-cell case-switch row so confirm / case-flip still work.
R47 — Wider survey zoom
mod.hooks:wrap("zoom.range", function(next, lo, hi, S)
lo, hi = next(lo, hi, S)
return lo - S, hi -- another full fit-scale of survey-out
end)
Checkpoint: hotkey 4 / the ZOOM row reach sub-1 scales so more of the
region fits on screen.
R48 — Super Game Boy border
local border
mod.events:on("game.ready", function()
border = love.graphics.newImage(mod.assets:path("assets/sgb_border.png"))
end)
mod.hooks:wrap("render.letterbox", function(next, ctx)
next(ctx)
if not border or ctx.worldActive then return end
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(border, 0, 0, 0,
ctx.ww / border:getWidth(), ctx.wh / border:getHeight())
end)
Checkpoint: menus / battles show your border in the letterbox; survey
zoom (worldActive) skips it so the world fill is undisturbed. For
post-process approaches see render_pipelines.
R54 - React to real-world steps
On iOS and Android the engine counts the player's real steps natively.
Declare the steps permission in manifest.json (the player sees it in
the mod manager), then probe, sync and poll:
mod.events:on("game.ready", function(ev)
if mod.steps:available() then
mod.steps:sync() -- async; the OS consent sheet appears on first use
end
end)
mod.events:on("map.entered", function()
local walk = mod.steps:poll() -- { steps, from, to } or nil
if walk then
local total = mod.save:get("walked", 0) + walk.steps
mod.save:set("walked", total)
mod.log:info("%d real steps banked", total)
end
end)
Checkpoint: on desktop available() is false and the mod stays
dormant; on a phone, walking between polls delivers { steps, from, to }
once, to every permissioned mod, never twice. Without the permission,
sync and poll raise an error naming it (src/mods/Steps.lua).
Start here
Concepts
Tutorials
- The ladder
- 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
Reference
Guides
- Art Pipeline
- Audio Authoring
- Preparing Your Mod for Gen 2
- Translations
- Total Conversions
- Link Compatibility
- Publishing a Mod
- Custom Carts
- Docs Style Guide
Playing
Engine 1.0.0 · mod API 2 · repository · Discord
This project ships no ROM data. All game content is decoded on the player's machine from their own verified Pokemon Red ROM; mods ship recipes and original assets, never extracted content.