mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-28 09:54:43 +02:00
49a5408c2d
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>
79 lines
2.9 KiB
Lua
79 lines
2.9 KiB
Lua
-- A sandboxed mod can take a Pokémon's level off the screens that print it
|
|
-- (pokemon.level_visible): the readout goes, the layout does not, and a
|
|
-- build with no mod wrapping the hook prints exactly what it always did.
|
|
--
|
|
-- The predicate is tested rather than the pixels: every Gen 1 level readout
|
|
-- goes through LevelDisplay.visible, and the four call sites are the same
|
|
-- one-line guard. What matters here is the contract -- default true, false
|
|
-- suppresses, the surface is named, and no-mod costs nothing.
|
|
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
love = love or require("tests.love_stub")
|
|
|
|
local T = require("tests.modkit")
|
|
local LevelDisplay = require("src.ui.LevelDisplay")
|
|
|
|
local FIXTURE = {
|
|
["mods/level_probe/manifest.json"] = [[{
|
|
"id": "level_probe",
|
|
"name": "Level Probe",
|
|
"version": "1.0.0",
|
|
"entry": "main.lua",
|
|
"api": 2
|
|
}]],
|
|
["mods/level_probe/main.lua"] = [[
|
|
local mod = ...
|
|
mod.exports.answer = nil
|
|
mod.exports.seen = nil
|
|
mod.hooks:wrap("pokemon.level_visible", function(next, mon, ctx)
|
|
mod.exports.seen = { mon = mon, where = ctx and ctx.where,
|
|
game = ctx and ctx.game }
|
|
if mod.exports.answer == nil then return next(mon, ctx) end
|
|
return mod.exports.answer
|
|
end)
|
|
]],
|
|
}
|
|
|
|
local MON = { species = "RATTATA", level = 42, moves = {} }
|
|
local GAME = { save = {} }
|
|
|
|
-- ------- no mod: the level is always printed
|
|
|
|
local vanilla = T.sdk.loadNone({})
|
|
T.eq(LevelDisplay.visible(MON, "battle.enemy", GAME), true,
|
|
"no mod: the enemy healthbox prints a level")
|
|
T.eq(LevelDisplay.visible(MON, "party", GAME), true,
|
|
"no mod: the party rows print a level")
|
|
T.eq(LevelDisplay.visible(nil, "summary", nil), true,
|
|
"no mod: even a nil mon answers true rather than throwing")
|
|
vanilla.release()
|
|
|
|
-- ------- a mod hides it
|
|
|
|
local run = T.sdk.loadMods({ "mods/level_probe" }, { fs = T.sdk.memfs(FIXTURE) })
|
|
T.eq(#run.errors, 0, "the level probe loads clean (" .. tostring(run.errors[1]) .. ")")
|
|
local probe = run.loader.exports.level_probe
|
|
|
|
probe.answer = false
|
|
T.eq(LevelDisplay.visible(MON, "battle.enemy", GAME), false,
|
|
"hidden: the enemy healthbox prints no level")
|
|
T.eq(probe.seen and probe.seen.where, "battle.enemy",
|
|
"the hook is told which surface asked")
|
|
T.check(probe.seen and probe.seen.mon == MON, "and which Pokémon")
|
|
T.check(probe.seen and probe.seen.game == GAME, "and the game")
|
|
|
|
-- the surface is what lets a mode hide an opponent's level and keep its own
|
|
probe.answer = nil
|
|
T.eq(LevelDisplay.visible(MON, "party", GAME), true,
|
|
"falling through prints, as today")
|
|
T.eq(probe.seen and probe.seen.where, "party", "and still names the surface")
|
|
|
|
-- only an explicit false suppresses: a mod returning nothing must not blank
|
|
-- a screen by accident
|
|
probe.answer = true
|
|
T.eq(LevelDisplay.visible(MON, "summary", GAME), true,
|
|
"an explicit true prints")
|
|
|
|
run.release()
|
|
T.finish("pokemon level visible")
|