Files
2026-07-19 16:18:18 -04:00

3.0 KiB

DexNav Example

A START-menu overlay listing every species in the merged dex with its seen/owned state, sortable, and publishing a small API other mods can call.

Persona: the Tool Builder. Consume the merged view, never require a private module, expose a stable inter-mod surface. This example is the reference for all three.

Try it

python3 tools/modkit.py validate mods/examples/example_dexnav --base imported
luajit mods/examples/example_dexnav/tests/example_dexnav_test.lua

Enable it (example_dexnav = true under mods in options.lua, or the F10 manager), then press START → DEXNAV. Its two options live in the manager's per-mod options pane.

What it demonstrates

Seam Where
content.screens:register main.lua — a factory the engine instantiates by id
hooks:wrap("ui.start_menu.items") main.lua — decorate, do not replace
mod.ui.insertBefore main.lua — anchor on a label, not a row index
mod.options:define / :get main.lua — auto-rendered rows in the manager
mod.exports main.lua — the inter-mod API
content.pokemon:each main.lua — the whole world, engine records included

Reading, not reaching

Every fact this mod displays comes from two public sources:

  • mod.content.pokemon:each() — the merged species view. A tool that hard-codes 151 breaks the moment another mod registers a species; this one just gets longer.
  • game.save.pokedex — the seen/owned tables, handed in by the engine.

No require("src.pokemon.…"), no permission declared, nothing that a later refactor of an engine module can break.

Anchoring a menu row

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, "SAVE", { label = "DEXNAV", onSelect = ... })
end)

Two rules, both load-bearing:

  1. Call next first, then decorate what comes back. Build a fresh list instead and every other mod's row disappears.
  2. Anchor on a stable label, not an index. insertBefore appends when the anchor is missing, so the row is always reachable even in a total conversion that renamed SAVE.

Exporting an API

mod.exports.countSeen = function(game) ... end

Another mod reads it as:

local nav = mod.find("example_dexnav")
if nav then print(nav.exports.countSeen(game)) end

mod.find returns nil when the other mod is absent, disabled, failed, or has not run yet — so a dependent degrades instead of crashing. Declare it in optional_dependencies if you can live without it and dependencies if you cannot.

Empty state

SHOW UNSEEN off on a fresh save means an empty list. ListMenu draws Nothing here. and B still exits — never a blank frame with no way out. The title still reports DEXNAV 0/0, so the screen explains itself.

Credits

  • pret/pokered — the START-menu layout the row is anchored into.