mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-28 18:06:09 +02:00
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>
This commit is contained in:
@@ -16,6 +16,7 @@ local Damage = require("src.battle.Damage")
|
||||
local EffectRegistry = require("src.battle.EffectRegistry")
|
||||
local Experience = require("src.battle.Experience")
|
||||
local Font = require("src.render.Font")
|
||||
local LevelDisplay = require("src.ui.LevelDisplay")
|
||||
local Logger = require("src.core.Logger")
|
||||
local MoveEffects = require("src.battle.MoveEffects")
|
||||
local Party = require("src.pokemon.Party")
|
||||
@@ -6298,7 +6299,7 @@ function BattleState:drawHUDs(slide)
|
||||
end
|
||||
if self.enemy.shownStatus then
|
||||
Font.draw(self:statusLabel({ status = self.enemy.shownStatus }), 40, 8)
|
||||
else
|
||||
elseif LevelDisplay.visible(self.enemy.mon, "battle.enemy", self.game) then
|
||||
hudTile(0x6E, 32, 8) -- <LV>
|
||||
Font.draw(tostring(self.enemy.mon.level), 40, 8)
|
||||
end
|
||||
@@ -6382,7 +6383,7 @@ function BattleState:drawHUDs(slide)
|
||||
Font.draw(self.player.name, nameX(10, self.player.name), 56)
|
||||
if self.player.shownStatus then
|
||||
Font.draw(self:statusLabel({ status = self.player.shownStatus }), 120, 64)
|
||||
else
|
||||
elseif LevelDisplay.visible(self.player.mon, "battle.player", self.game) then
|
||||
hudTile(0x6E, 112, 64) -- <LV>
|
||||
Font.draw(tostring(self.player.mon.level), 120, 64)
|
||||
end
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
-- 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
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
local Assets = require("src.render.Assets")
|
||||
local Font = require("src.render.Font")
|
||||
local LevelDisplay = require("src.ui.LevelDisplay")
|
||||
local Logger = require("src.core.Logger")
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
local Screens = require("src.ui.Screens")
|
||||
@@ -785,7 +786,11 @@ function PartyMenu:draw()
|
||||
-- level at column 13 (<LV> tile + digits, PrintLevel) AND the
|
||||
-- status/FNT text at column 17 (PrintStatusCondition), like the
|
||||
-- original rows -- statused mons keep their level display
|
||||
if mon.level < 100 then
|
||||
if not LevelDisplay.visible(mon, "party", self.game) then -- RFC 0019
|
||||
-- the level column is simply empty; the status/FNT column at 17 is a
|
||||
-- separate field and still prints, exactly as it does for a mon whose
|
||||
-- level is on screen
|
||||
elseif mon.level < 100 then
|
||||
HudTiles.tile(0x6E, 104, y) -- <LV>
|
||||
Font.draw(tostring(mon.level), 112, y)
|
||||
else
|
||||
|
||||
+10
-3
@@ -12,6 +12,7 @@ local Font = require("src.render.Font")
|
||||
-- TypeChart.displayName maps it back to "PSYCHIC", like HallOfFame and the
|
||||
-- battle move-type box already do (#214).
|
||||
local TypeChart = require("src.battle.TypeChart")
|
||||
local LevelDisplay = require("src.ui.LevelDisplay")
|
||||
local Strings = require("src.core.Strings")
|
||||
local Stats = require("src.pokemon.Stats")
|
||||
local Status = require("src.battle.Status")
|
||||
@@ -137,7 +138,9 @@ function SummaryMenu:draw()
|
||||
if self.page == 1 then
|
||||
-- level is page 1 only: StatusScreen2 opens with ClearScreenArea over
|
||||
-- (9,2) 5x10 (status_screen.asm:303-305). #280
|
||||
printLevel(14, 2, mon.level)
|
||||
if LevelDisplay.visible(mon, "summary", self.game) then -- RFC 0019
|
||||
printLevel(14, 2, mon.level)
|
||||
end
|
||||
drawLineBox(19, 1, 6, 10)
|
||||
-- engine/pokemon/status_screen.asm:120-125
|
||||
local PaletteFX = require("src.render.PaletteFX")
|
||||
@@ -196,8 +199,12 @@ function SummaryMenu:draw()
|
||||
local nextExp = mon.level < 100
|
||||
and (Growth.expForLevel(def.growthRate, mon.level + 1) - mon.exp) or 0
|
||||
Font.draw(("%7d"):format(math.max(0, nextExp)), 56, 48)
|
||||
HudTiles.statusTile(0x70, 112, 48) -- '<to>' at (14,6), was missing (#280)
|
||||
printLevel(16, 6, math.min(100, mon.level + 1))
|
||||
if LevelDisplay.visible(mon, "summary", self.game) then -- RFC 0019
|
||||
-- the '<to>' arrow is half a sentence without the level it points at,
|
||||
-- so the pair is hidden together
|
||||
HudTiles.statusTile(0x70, 112, 48) -- '<to>' at (14,6), was missing (#280)
|
||||
printLevel(16, 6, math.min(100, mon.level + 1))
|
||||
end
|
||||
Font.drawBox(0, 8, 20, 10)
|
||||
for i = 1, 4 do
|
||||
local mv = mon.moves[i]
|
||||
|
||||
Reference in New Issue
Block a user