TypeChart.displayName(typeId, data) already resolved a merged type_chart
translation for the Gen 2 Summary screen, but the Gen 1/RBY SummaryMenu and
HallOfFame call sites never passed their data table, so opening either
screen before the first battle fell back to the untranslated TypeChart.TYPES
table even with a translation mod loaded.
WideBattle.lua's and this file's own in-battle move-details panels get the
same explicit data argument for the same call convention, though it is a
no-op there today: battle.data/self.data is game.data by reference, the
same table TypeChart.load(game.data) already caches from at battle start,
so both paths already agreed before this change.
The RBY OPTION screen's ZOOM, MAX FPS and the three GameSpeed rows also drew
their dynamic value labels straight from Zoom.offsetLabel/FrameCap.label/
GameSpeed.levelLabel, bypassing Strings() entirely, unlike the Gen 2 OPTION
screen's equivalent rows added earlier in this branch.
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>
src/ui/SummaryMenu.lua:148 and src/ui/PartyMenu.lua:824 drew mon.status
(PSN/PAR/BRN/FRZ/SLP) as a bare literal, bypassing translation. Unlike
plain text, a mod translates status labels through the statuses content
registry (mod.content.statuses:patch(id, { label = value }), the same
registry src/battle/BattleState.lua:statusLabel already reads in battle.
Route both screens through the same lookup, extracted as
Status.hudLabelFor(statuses, id) and shared with BattleState:statusLabel
so the hudLabel-or-label fallback rule lives in one place, with the raw
status id kept as the fallback when no record overrides it.
Found along the way: Status.RECORDS' five vanilla entries duplicated
hudLabel = label ("FRZ", hudLabel = "FRZ", ...) for no functional
reason. Since hudLabelFor reads hudLabel before label, and
Registry:patch only overrides fields a mod actually passes, a
translation mod's label-only patch (the natural shape for a status
catalog carrying one string per id, with no separate hudLabel data to
patch) was silently shadowed by the untouched vanilla hudLabel -- the
translation was stored but never displayed, in or out of battle. This
affected BattleState:statusLabel too, before this change and
independently of it. Dropped the redundant hudLabel field from all
five vanilla records: it's declared optional in the schema, and
nothing in this codebase ever gives it a value different from label --
setting it here only recreated the shadowing trap for no observed
benefit. Left a comment above Status.RECORDS warning against
re-adding it.
The SummaryMenu (pause menu stats screen and in-battle stats screen)
displayed the base PP from the move definition as the max value,
ignoring PP Up bonuses. After using a PP UP, the screen would show
e.g. 6/5 instead of 6/6.
Fix: calculate maxPP with the PP Up bonus (basePP + ppUps * basePP/5),
matching the formula used everywhere else -- battle fight menus,
ETHER restore, Pokemon Center heal, link protocol, and save editor.
Fixes#641
The ATTACK/DEFENSE/SPEED/SPECIAL labels on the status screen were drawn
literally; wrap them like the other UI so a language mod can translate them.
Behavior-neutral for the base game (Strings identity without a catalog).