mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-28 18:06:09 +02:00
Merge pull request #1835 from campavao/rfc-0019-level-visible
engine: pokemon.level_visible, a level a mode can take off the screen (RFC 0019)
This commit is contained in:
@@ -1111,6 +1111,37 @@ own state, preserving selective control outside a battle; a wrapper that only
|
|||||||
owns battle presentation should return `false` only for its active battle or
|
owns battle presentation should return `false` only for its active battle or
|
||||||
text-box state.
|
text-box state.
|
||||||
|
|
||||||
|
`pokemon.level_visible` (RFC 0019) takes a Pokémon's level off the screens
|
||||||
|
that print it, without moving anything else on them:
|
||||||
|
|
||||||
|
```lua
|
||||||
|
mod.hooks:wrap("pokemon.level_visible", function(next, mon, ctx)
|
||||||
|
-- ctx = { where = "battle.enemy" | "battle.player" | "party" | "summary",
|
||||||
|
-- game = <Game> }
|
||||||
|
if myMode.active and ctx.where ~= "party" then return false end
|
||||||
|
return next(mon, ctx)
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
It receives `(next, mon, ctx)` and defaults to `true`, so vanilla rendering
|
||||||
|
is unchanged. Only an explicit `false` suppresses; `nil` and anything else
|
||||||
|
print, so a wrapper that forgets a branch cannot blank a screen by accident.
|
||||||
|
The `<LV>` glyph goes with the digits, and on status page 2 so does the
|
||||||
|
`<to>` arrow that points at the next level, because an arrow with nothing
|
||||||
|
after it is half a sentence. A status condition still replaces the level on
|
||||||
|
a battle healthbox exactly as it does in the cart, so hiding a level never
|
||||||
|
hides `PSN` or `BRN`.
|
||||||
|
|
||||||
|
`ctx.where` names the surface rather than the widget, because the number
|
||||||
|
means different things on different screens: on a 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.
|
||||||
|
|
||||||
|
**Gen 1 only for now.** The Gen 2 screens keep their own level readouts and
|
||||||
|
do not consult this hook, and neither does the Gen 1 PC box list, where the
|
||||||
|
level is part of a row label rather than a drawn field. Both are noted in
|
||||||
|
RFC 0019 as follow-ups.
|
||||||
|
|
||||||
`core.logic_speed` receives `(next, game)` once per `Game:logicSpeed()` call
|
`core.logic_speed` receives `(next, game)` once per `Game:logicSpeed()` call
|
||||||
(once per frame). Vanilla behavior resolves the per-category GAME SPEED
|
(once per frame). Vanilla behavior resolves the per-category GAME SPEED
|
||||||
option (`GameSpeed.CATEGORIES`: overworld/battle/menu) for whichever
|
option (`GameSpeed.CATEGORIES`: overworld/battle/menu) for whichever
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
# RFC 0019: `pokemon.level_visible` — a level a mode can take off the screen
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Proposed.
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
A Pokémon's level is printed on four Gen 1 surfaces — both battle
|
||||||
|
healthboxes, the party rows, and page 1 and page 2 of the status screen —
|
||||||
|
and every one of them prints it unconditionally. There is no seam. A mode
|
||||||
|
that wants the number gone has exactly two options today, and both are bad.
|
||||||
|
|
||||||
|
It can paint over the text from `render.hud`, which means knowing four pixel
|
||||||
|
rectangles, matching the background shade, and surviving the palette flashes
|
||||||
|
and the healthbox slide-in. Or it can monkey-patch the drawing modules from
|
||||||
|
inside its sandbox, which works — `require` hands a mod the engine's own
|
||||||
|
table — and is precisely what `CONTRIBUTING-mods.md` tells mods not to do.
|
||||||
|
A mod that took the second road already deleted its patching layer once, for
|
||||||
|
the reasons that document gives.
|
||||||
|
|
||||||
|
The motivating case is a battle royale where every party is scaled to a
|
||||||
|
shared rung that rises with the fog. The number on the healthbox is
|
||||||
|
therefore never news — it is the same for everyone, it changes on a clock,
|
||||||
|
and a player reading `:L37` on an opponent learns nothing except that they
|
||||||
|
are playing the same match. Worse, 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. The mode wants the level off the HUD and out of the party
|
||||||
|
list, and the announcement it replaces it with is one line about everyone
|
||||||
|
getting stronger at once.
|
||||||
|
|
||||||
|
Nothing about that is specific to a battle royale. A randomizer that hides
|
||||||
|
levels to keep an encounter unreadable, a challenge run that forbids
|
||||||
|
level-checking, a hard mode that withholds an opponent's level, and a
|
||||||
|
"blind" Nuzlocke all want the same switch, and none of them should have to
|
||||||
|
learn where the `<LV>` glyph lives.
|
||||||
|
|
||||||
|
## The decision it extends
|
||||||
|
|
||||||
|
This extends the **additive, guarded seam convention** Route B in
|
||||||
|
`CONTRIBUTING-mods.md` documents, and is gated by the parity guarantee
|
||||||
|
`tests/engine/gate_meta_coverage.lua` enforces.
|
||||||
|
|
||||||
|
It sits with the presentation predicates already on the battle screen —
|
||||||
|
`battle.status_hud_visible`, `battle.bottom_ui_visible` and
|
||||||
|
`battle.caught_marker_visible` — and takes the same shape: a hook consulted
|
||||||
|
behind `Runtime.wantsHook`, defaulting to visible, where only an explicit
|
||||||
|
`false` suppresses. The difference is that a level is not a battle-only
|
||||||
|
readout, so this one is not named `battle.*` and carries the surface that
|
||||||
|
asked.
|
||||||
|
|
||||||
|
There is no in-repo D-number registry to amend.
|
||||||
|
|
||||||
|
## Exact API delta
|
||||||
|
|
||||||
|
### New hook: `pokemon.level_visible`
|
||||||
|
|
||||||
|
```lua
|
||||||
|
mod.hooks:wrap("pokemon.level_visible", function(next, mon, ctx)
|
||||||
|
-- ctx = { where = "battle.enemy" | "battle.player" | "party" | "summary",
|
||||||
|
-- game = <Game> }
|
||||||
|
if myMode.active and ctx.where ~= "party" then
|
||||||
|
return false -- the level is not printed on that surface
|
||||||
|
end
|
||||||
|
return next(mon, ctx) -- true: printed, as today
|
||||||
|
end)
|
||||||
|
```
|
||||||
|
|
||||||
|
Default `true`. Only an explicit `false` suppresses; `nil` and every other
|
||||||
|
value print, so a wrapper that forgets a branch cannot blank a screen by
|
||||||
|
accident.
|
||||||
|
|
||||||
|
`ctx.where` names the surface rather than the widget, because the number
|
||||||
|
means different things on different screens: on a 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 that wants to hide the first and
|
||||||
|
keep the second can, and the motivating mode does exactly that.
|
||||||
|
|
||||||
|
### New module: `src/ui/LevelDisplay.lua`
|
||||||
|
|
||||||
|
One function, `LevelDisplay.visible(mon, where, game)`, wrapping the
|
||||||
|
`wantsHook`/`call` pair. It exists so the four call sites are a one-line
|
||||||
|
guard each instead of four copies of the same five lines, and so the hook
|
||||||
|
has one definition of "visible" rather than four that can drift.
|
||||||
|
|
||||||
|
### Call sites
|
||||||
|
|
||||||
|
| Surface | File | `where` |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Enemy healthbox | `src/battle/BattleState.lua` | `battle.enemy` |
|
||||||
|
| Player healthbox | `src/battle/BattleState.lua` | `battle.player` |
|
||||||
|
| Party rows | `src/ui/PartyMenu.lua` | `party` |
|
||||||
|
| Status page 1 and 2 | `src/ui/SummaryMenu.lua` | `summary` |
|
||||||
|
|
||||||
|
No layout moves. Each site keeps its own hand-rolled PrintLevel rule
|
||||||
|
(`home/pokemon.asm:335-345` — the `<LV>` tile, then the digits, with a level
|
||||||
|
of 100 writing its third digit back over the tile); it just asks first.
|
||||||
|
|
||||||
|
Two details are deliberate:
|
||||||
|
|
||||||
|
- **A status condition still replaces the level on a healthbox**, exactly as
|
||||||
|
it does in the cart, so hiding the level never hides `PSN` or `BRN`. The
|
||||||
|
guard is an `elseif` on the existing status branch, not a wrapper around
|
||||||
|
it.
|
||||||
|
- **On status page 2 the `<to>` arrow is hidden with the level it points
|
||||||
|
at.** The arrow introduces the next level; on its own it is half a
|
||||||
|
sentence. The EXP-to-next-level figure beside it is a different field and
|
||||||
|
still prints.
|
||||||
|
|
||||||
|
### No other surface changes
|
||||||
|
|
||||||
|
No event, no registry, no save field, no manifest key.
|
||||||
|
|
||||||
|
## Migration
|
||||||
|
|
||||||
|
None. The hook is additive and defaults to current behaviour.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `tests/modkit/cases/pokemon_level_visible.lua` — the contract through the
|
||||||
|
public mod API: default true with no mod, `false` suppresses, the surface
|
||||||
|
and the mon reach the hook, falling through prints, and a `nil` mon
|
||||||
|
answers rather than throwing.
|
||||||
|
- `tests/engine/gate_hooks.lua` — the structural parity gate picks the hook
|
||||||
|
up automatically, because it walks the live catalog rather than a list.
|
||||||
|
- `tests/engine/gate_meta_coverage.lua` — the coverage ratchet; the seam is
|
||||||
|
covered by name from the change that introduces it, so it never enters the
|
||||||
|
DEBT ledger.
|
||||||
|
|
||||||
|
## Backward compatibility
|
||||||
|
|
||||||
|
A build with no mod wrapping the hook never reaches `Runtime.call`:
|
||||||
|
`wantsHook` is checked first, which matters because two of the four sites
|
||||||
|
are on a per-frame draw path. Pixels are unchanged, and the parity gates
|
||||||
|
assert it.
|
||||||
|
|
||||||
|
## Scope, and what is deliberately not in this change
|
||||||
|
|
||||||
|
**Gen 1 only.** The Gen 2 screens keep their own level readouts
|
||||||
|
(`src/ui/gen2/PartyMenu.lua`, `SummaryMenu.lua`, `BoxMenu.lua`,
|
||||||
|
`HallOfFame.lua`) and do not consult the hook. So does the Gen 1 PC box list
|
||||||
|
(`src/ui/BoxMenu.lua`), where the level is baked into a row label string
|
||||||
|
rather than drawn as a field, and the box-to-PNG print path beside it.
|
||||||
|
|
||||||
|
Those are mechanical follow-ups, held back so this change stays reviewable
|
||||||
|
against screens that can actually be exercised here. The limitation is
|
||||||
|
stated in `docs/modding.md` beside the hook, so a mod author reads it before
|
||||||
|
depending on it rather than after.
|
||||||
|
|
||||||
|
## Compatibility seam for older engines
|
||||||
|
|
||||||
|
There is none, and none is possible without patching: the call sites are
|
||||||
|
mid-draw, so a mod on a stock engine cannot reach them. That is the argument
|
||||||
|
for the seam rather than a gap around it — the alternatives available to a
|
||||||
|
mod today are painting over the engine's own pixels or reaching into its
|
||||||
|
render modules, and the second is the thing the mod contract exists to
|
||||||
|
prevent.
|
||||||
@@ -16,6 +16,7 @@ local Damage = require("src.battle.Damage")
|
|||||||
local EffectRegistry = require("src.battle.EffectRegistry")
|
local EffectRegistry = require("src.battle.EffectRegistry")
|
||||||
local Experience = require("src.battle.Experience")
|
local Experience = require("src.battle.Experience")
|
||||||
local Font = require("src.render.Font")
|
local Font = require("src.render.Font")
|
||||||
|
local LevelDisplay = require("src.ui.LevelDisplay")
|
||||||
local Logger = require("src.core.Logger")
|
local Logger = require("src.core.Logger")
|
||||||
local MoveEffects = require("src.battle.MoveEffects")
|
local MoveEffects = require("src.battle.MoveEffects")
|
||||||
local Party = require("src.pokemon.Party")
|
local Party = require("src.pokemon.Party")
|
||||||
@@ -6337,7 +6338,7 @@ function BattleState:drawHUDs(slide)
|
|||||||
end
|
end
|
||||||
if self.enemy.shownStatus then
|
if self.enemy.shownStatus then
|
||||||
Font.draw(self:statusLabel({ status = self.enemy.shownStatus }), 40, 8)
|
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>
|
hudTile(0x6E, 32, 8) -- <LV>
|
||||||
Font.draw(tostring(self.enemy.mon.level), 40, 8)
|
Font.draw(tostring(self.enemy.mon.level), 40, 8)
|
||||||
end
|
end
|
||||||
@@ -6421,7 +6422,7 @@ function BattleState:drawHUDs(slide)
|
|||||||
Font.draw(self.player.name, nameX(10, self.player.name), 56)
|
Font.draw(self.player.name, nameX(10, self.player.name), 56)
|
||||||
if self.player.shownStatus then
|
if self.player.shownStatus then
|
||||||
Font.draw(self:statusLabel({ status = self.player.shownStatus }), 120, 64)
|
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>
|
hudTile(0x6E, 112, 64) -- <LV>
|
||||||
Font.draw(tostring(self.player.mon.level), 120, 64)
|
Font.draw(tostring(self.player.mon.level), 120, 64)
|
||||||
end
|
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 Assets = require("src.render.Assets")
|
||||||
local Font = require("src.render.Font")
|
local Font = require("src.render.Font")
|
||||||
|
local LevelDisplay = require("src.ui.LevelDisplay")
|
||||||
local Logger = require("src.core.Logger")
|
local Logger = require("src.core.Logger")
|
||||||
local Runtime = require("src.mods.Runtime")
|
local Runtime = require("src.mods.Runtime")
|
||||||
local Screens = require("src.ui.Screens")
|
local Screens = require("src.ui.Screens")
|
||||||
@@ -794,7 +795,11 @@ function PartyMenu:draw()
|
|||||||
-- level at column 13 (<LV> tile + digits, PrintLevel) AND the
|
-- level at column 13 (<LV> tile + digits, PrintLevel) AND the
|
||||||
-- status/FNT text at column 17 (PrintStatusCondition), like the
|
-- status/FNT text at column 17 (PrintStatusCondition), like the
|
||||||
-- original rows -- statused mons keep their level display
|
-- 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>
|
HudTiles.tile(0x6E, 104, y) -- <LV>
|
||||||
Font.draw(tostring(mon.level), 112, y)
|
Font.draw(tostring(mon.level), 112, y)
|
||||||
else
|
else
|
||||||
|
|||||||
+10
-3
@@ -12,6 +12,7 @@ local Font = require("src.render.Font")
|
|||||||
-- TypeChart.displayName maps it back to "PSYCHIC", like HallOfFame and the
|
-- TypeChart.displayName maps it back to "PSYCHIC", like HallOfFame and the
|
||||||
-- battle move-type box already do (#214).
|
-- battle move-type box already do (#214).
|
||||||
local TypeChart = require("src.battle.TypeChart")
|
local TypeChart = require("src.battle.TypeChart")
|
||||||
|
local LevelDisplay = require("src.ui.LevelDisplay")
|
||||||
local Strings = require("src.core.Strings")
|
local Strings = require("src.core.Strings")
|
||||||
local Stats = require("src.pokemon.Stats")
|
local Stats = require("src.pokemon.Stats")
|
||||||
local Status = require("src.battle.Status")
|
local Status = require("src.battle.Status")
|
||||||
@@ -137,7 +138,9 @@ function SummaryMenu:draw()
|
|||||||
if self.page == 1 then
|
if self.page == 1 then
|
||||||
-- level is page 1 only: StatusScreen2 opens with ClearScreenArea over
|
-- level is page 1 only: StatusScreen2 opens with ClearScreenArea over
|
||||||
-- (9,2) 5x10 (status_screen.asm:303-305). #280
|
-- (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)
|
drawLineBox(19, 1, 6, 10)
|
||||||
-- engine/pokemon/status_screen.asm:120-125
|
-- engine/pokemon/status_screen.asm:120-125
|
||||||
local PaletteFX = require("src.render.PaletteFX")
|
local PaletteFX = require("src.render.PaletteFX")
|
||||||
@@ -196,8 +199,12 @@ function SummaryMenu:draw()
|
|||||||
local nextExp = mon.level < 100
|
local nextExp = mon.level < 100
|
||||||
and (Growth.expForLevel(def.growthRate, mon.level + 1) - mon.exp) or 0
|
and (Growth.expForLevel(def.growthRate, mon.level + 1) - mon.exp) or 0
|
||||||
Font.draw(("%7d"):format(math.max(0, nextExp)), 56, 48)
|
Font.draw(("%7d"):format(math.max(0, nextExp)), 56, 48)
|
||||||
HudTiles.statusTile(0x70, 112, 48) -- '<to>' at (14,6), was missing (#280)
|
if LevelDisplay.visible(mon, "summary", self.game) then -- RFC 0019
|
||||||
printLevel(16, 6, math.min(100, mon.level + 1))
|
-- 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)
|
Font.drawBox(0, 8, 20, 10)
|
||||||
for i = 1, 4 do
|
for i = 1, 4 do
|
||||||
local mv = mon.moves[i]
|
local mv = mon.moves[i]
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
-- 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")
|
||||||
Reference in New Issue
Block a user