Files
gen1recomp/src/ui/LevelDisplay.lua
T
DESKTOP-8SRFDDM\cam95 49a5408c2d engine: pokemon.level_visible, a level a mode can take off the screen (RFC 0019)
A Pokémon's level is printed on four Gen 1 surfaces -- both battle
healthboxes, the party rows, and both status pages -- and every one of them
prints it unconditionally. There is no seam, so a mode that wants the number
gone has two options today and both are bad: paint over the engine's own
pixels from render.hud (four rectangles, a background shade to match, and
the palette flashes and healthbox slide to survive), or monkey-patch the
render modules from inside the sandbox, which works and is exactly what
CONTRIBUTING-mods.md tells mods not to do.

The motivating case is a battle royale that scales every party to a shared
rung rising with its fog: the number is the same for everyone, it changes on
a clock, and it reads as a threat it is not -- a Lv37 opponent looks
dangerous to a player who has not worked out that their own team is Lv37
too. A randomizer keeping an encounter unreadable, a challenge run that
forbids level-checking and a blind Nuzlocke want the same switch.

New hook `pokemon.level_visible`, taking the shape the presentation
predicates on the battle screen already use -- battle.status_hud_visible,
battle.bottom_ui_visible, battle.caught_marker_visible: consulted behind
Runtime.wantsHook, default visible, only an explicit false suppresses. It is
not named battle.* because a level is not a battle-only readout, and it
carries the surface that asked (battle.enemy / battle.player / party /
summary) so a mode can hide an opponent's level and keep its own.

src/ui/LevelDisplay.lua holds the one definition of "visible", so the four
call sites are a one-line guard each rather than four copies of the same
five lines that can drift apart.

No layout moves. Each site keeps its own hand-rolled PrintLevel rule
(home/pokemon.asm:335-345), it just asks first. Two details are deliberate:
a status condition still replaces the level on a healthbox exactly as in the
cart, so hiding a level never hides PSN or BRN (the guard is an elseif on
the existing status branch); and on status page 2 the <to> arrow is hidden
with the level it points at, because an arrow with nothing after it is half
a sentence.

Gen 1 only. The Gen 2 screens and the Gen 1 PC box list -- where the level
is part of a row label rather than a drawn field -- keep their own readouts
and do not consult the hook. Both are stated as follow-ups in the RFC and
beside the hook in docs/modding.md, so a mod author reads the limit before
depending on it.

Verification: tests/modkit/cases/pokemon_level_visible.lua covers the
contract through the public mod API; gate_hooks picks the hook up on its own
because it walks the live catalog; gate_meta_coverage is satisfied by the
change that introduces the seam, so it never enters the DEBT ledger.
tests/run_modkit.lua 33/33, tests/run_engine.lua 327/331 -- the same four
audio/hostshell suites fail unchanged on dev without this branch.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-26 10:53:09 -05:00

34 lines
1.5 KiB
Lua

-- Whether a Pokémon's level is printed on a screen that would normally
-- print it (RFC 0019).
--
-- Every Gen 1 screen that shows a level does it with the same pokered rule
-- -- home/pokemon.asm:335-345 PrintLevel: the <LV> tile, then the digits
-- left-aligned after it, with a level of 100 writing its third digit back
-- over the tile -- and each screen hand-rolls that rule against its own
-- coordinates. This module does not touch any of that. It answers one
-- question, in one place, so that a mode which wants the number off does
-- not have to know four call sites and a glyph code.
--
-- Default is true, and `Runtime.wantsHook` is checked first, so a build
-- with no mod wrapping the hook prints exactly what it always did and pays
-- nothing for the seam on a per-frame draw path.
--
-- `where` names the surface rather than the widget, because the number
-- means different things on different screens: on the battle HUD an
-- opponent's level is information about them, on the party and status
-- screens your own level is information about you. A mode can hide one and
-- keep the other.
local Runtime = require("src.mods.Runtime")
local LevelDisplay = {}
-- where: "battle.enemy" | "battle.player" | "party" | "summary"
function LevelDisplay.visible(mon, where, game)
if not Runtime.wantsHook("pokemon.level_visible") then return true end
return Runtime.call("pokemon.level_visible", function() return true end,
mon, { where = where, game = game }) ~= false
end
return LevelDisplay