diff --git a/docs/modding.md b/docs/modding.md index d7189ca9..04f7cac3 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -343,6 +343,13 @@ lists fall back to their title; PC lists use stable, localization-independent identifiers: `pc_box_withdraw`, `pc_box_deposit`, `pc_box_release`, `pc_box_change`, `pc_item_withdraw`, `pc_item_deposit`, and `pc_item_toss`. +`battle.bottom_ui_visible` and `battle.status_hud_visible` independently +control the battle text/menu layer and the HP/status panels. Both receive +`(next, state)` and default to `true`, so vanilla rendering is unchanged. +Pushed text boxes also pass through `battle.bottom_ui_visible`; a wrapper that +only owns battle presentation should return `false` only for its active battle +or text-box state. + `core.logic_speed` receives `(next, game)` once per `Game:logicSpeed()` call (once per frame). Vanilla behavior resolves the per-category GAME SPEED option (`GameSpeed.CATEGORIES`: overworld/battle/menu) for whichever diff --git a/src/battle/BattleState.lua b/src/battle/BattleState.lua index 15805293..f0a5403f 100644 --- a/src/battle/BattleState.lua +++ b/src/battle/BattleState.lua @@ -133,6 +133,18 @@ function BattleState:sgbPalettes() return nil end +function BattleState:bottomUIVisible() + if not Runtime.wantsHook("battle.bottom_ui_visible") then return true end + return Runtime.call("battle.bottom_ui_visible", function() return true end, + self) ~= false +end + +function BattleState:statusHUDVisible() + if not Runtime.wantsHook("battle.status_hud_visible") then return true end + return Runtime.call("battle.status_hud_visible", function() return true end, + self) ~= false +end + local Rulesets = { gen1_faithful = require("src.battle.rulesets.gen1_faithful"), modern_clean = require("src.battle.rulesets.modern_clean"), @@ -5358,7 +5370,7 @@ function BattleState:drawPicsLayer(slide, sx, sy, onlySide, skipMenuClip) -- .mimicmenu) wipes rows 7+. The port draws pics above the menu -- layer in the colorized pipeline, so clip them to the visible rows. local g = love.graphics - local clipY = not skipMenuClip + local clipY = not skipMenuClip and self:bottomUIVisible() and (self.phase == "mimicSelect" and 56 or self.phase == "moveSelect" and 64) or nil @@ -5470,6 +5482,7 @@ function BattleState:drawHUDs(slide) -- per-pixel tint (grayFill) -- otherwise GREENBAR's red-channel-0 fill -- double-applies and the zone shade shader maps the whole bar to black (#229). local grayFill = self:colorMode() + local showStatus = self:statusHUDVisible() local barData = self.data local fx = self.fx local hudShake = (fx and fx.hudShakeX) or 0 @@ -5479,7 +5492,8 @@ function BattleState:drawHUDs(slide) -- DrawEnemyHUDAndHPBar is called from _InitBattleCommon (core.asm:6763) -- AFTER PrintBeginningBattleText returns, so "Wild X appeared!" shows the -- player's ball row with no enemy HUD beside it (#317) - if self.enemy and not self.showEnemyTrainer and not self.enemySendingOut + if showStatus and self.enemy and not self.showEnemyTrainer + and not self.enemySendingOut and not self:growInScale(self.enemy) and slide == 0 and not self.introBalls and not self.enemy.fainted then -- enemy HUD (DrawEnemyHUDAndHPBar): name row 0, +level (4,1), @@ -5566,7 +5580,7 @@ function BattleState:drawHUDs(slide) self:drawBallRow(self.playerParty or self.game.save.party, 88, 80, 8) end local hidePlayer = self.safari or self.demo - if self.player and not hidePlayer and not self.showPlayerBack + if showStatus and self.player and not hidePlayer and not self.showPlayerBack and slide == 0 then -- player HUD (DrawPlayerHUDAndHPBar): name (10,7), +level -- (14,8), HP bar (10,9), HP numbers row 10, underline row 11 with @@ -5591,6 +5605,7 @@ function BattleState:drawHUDs(slide) end function BattleState:drawTextArea() + if not self:bottomUIVisible() then return end Font.drawBox(0, 12, 20, 6) love.graphics.setColor(0, 0, 0, 1) if self.phase == "messages" diff --git a/src/battle/WideBattle.lua b/src/battle/WideBattle.lua index 72b122c7..87e43bd2 100644 --- a/src/battle/WideBattle.lua +++ b/src/battle/WideBattle.lua @@ -128,7 +128,8 @@ local function drawIntroBalls(battle) end local function drawHUDs(battle, slide) - if battle.enemy and not battle.showEnemyTrainer + local showStatus = battle:statusHUDVisible() + if showStatus and battle.enemy and not battle.showEnemyTrainer and not battle.enemySendingOut and not battle:growInScale(battle.enemy) and slide == 0 and not battle.introBalls and not battle.enemy.fainted then drawStatusPanel(battle, battle.enemy, 0, 0, false) @@ -139,7 +140,7 @@ local function drawHUDs(battle, slide) -- item, not a HUD element (DisplayBattleMenu prints wNumSafariBalls inside -- the battle menu box, engine/battle/core.asm:2074-2079), so it rides in -- drawCommandMenu below like the classic layout's (#540). - if not battle.safari and battle.player and not battle.demo + if showStatus and not battle.safari and battle.player and not battle.demo and not battle.showPlayerBack and slide == 0 then drawStatusPanel(battle, battle.player, 184, 56, true) end @@ -255,6 +256,7 @@ local function drawMoveMenu(battle) end local function drawTextArea(battle) + if not battle:bottomUIVisible() then return end if battle.phase == "messages" and (battle.current or battle.animPlaying) then drawMessageBox(battle) elseif battle.phase == "menu" then diff --git a/src/render/TextBox.lua b/src/render/TextBox.lua index 43696871..290c4ecc 100644 --- a/src/render/TextBox.lua +++ b/src/render/TextBox.lua @@ -7,11 +7,13 @@ -- the text is exhausted and A is pressed, then calls onDone. local Font = require("src.render.Font") +local Runtime = require("src.mods.Runtime") local Theme = require("src.ui.Theme") local Timing = require("src.core.Timing") local TextBox = {} TextBox.__index = TextBox +TextBox.isTextBox = true -- theme-free fallbacks; geometry resolves against Theme.textBox at -- construction time, so an unthemed boot stays byte-identical @@ -344,6 +346,11 @@ function TextBox:update(dt) end function TextBox:draw() + if Runtime.wantsHook("battle.bottom_ui_visible") + and Runtime.call("battle.bottom_ui_visible", function() return true end, + self) == false then + return + end -- The dialogue box belongs against the bottom of the screen, not floating -- in the middle of a zoomed-out letterbox. Declared per frame; the -- renderer blits this region to the screen edge and the rest of the UI diff --git a/tests/engine/wide_battle_shake_bug562.lua b/tests/engine/wide_battle_shake_bug562.lua index 4f367a12..b445748c 100644 --- a/tests/engine/wide_battle_shake_bug562.lua +++ b/tests/engine/wide_battle_shake_bug562.lua @@ -74,6 +74,9 @@ local function battleWith(fx, sprites) growInScale = function() return nil end, drawBallRow = function() end, statusLabel = function() return "" end, + statusHUDVisible = function() return true end, + bottomUIVisible = function() return true end, + caughtMarkerVisible = function() return false end, } end diff --git a/tests/mod_qol_hooks_tests.lua b/tests/mod_qol_hooks_tests.lua index 25a8b9ff..6fd63347 100644 --- a/tests/mod_qol_hooks_tests.lua +++ b/tests/mod_qol_hooks_tests.lua @@ -11,6 +11,7 @@ local Stats = require("src.pokemon.Stats") local Zoom = require("src.render.Zoom") local ListMenu = require("src.ui.ListMenu") local NamingScreen = require("src.ui.NamingScreen") +local TextBox = require("src.render.TextBox") local Player = require("src.world.Player") local Music = require("src.core.Music") @@ -161,6 +162,36 @@ do unsub() end +-- ------- battle UI visibility (companion / alternate renderers) + +do + local BattleState = require("src.battle.BattleState") + check(BattleState.bottomUIVisible({ phase = "menu" }), + "battle bottom UI is visible without a mod") + local seen + local unsub = wrap("battle.bottom_ui_visible", function(_, state) + seen = state + return false + end) + check(not BattleState.bottomUIVisible({ phase = "messages" }), + "a mod can hide the battle text and menu layer") + local text = setmetatable({}, TextBox) + text:draw() + check(seen == text, "pushed text boxes use the same visibility hook") + unsub() + check(BattleState.bottomUIVisible({ phase = "moveSelect" }), + "battle bottom UI returns when the hook is removed") + + check(BattleState.statusHUDVisible({}), + "battle status HUD is visible without a mod") + unsub = wrap("battle.status_hud_visible", function() return false end) + check(not BattleState.statusHUDVisible({}), + "a mod can hide the battle status HUD") + unsub() + check(BattleState.statusHUDVisible({}), + "battle status HUD returns when the hook is removed") +end + -- ------- music.volume (distance / indoor muffling) do