From 9984958193d996e20df13b331f5699507e0d9970 Mon Sep 17 00:00:00 2001 From: thibautbus <310327033+thibautbus@users.noreply.github.com> Date: Tue, 18 Aug 2026 17:59:34 +0200 Subject: [PATCH 1/2] Translate the status abbreviations shown outside battle 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. --- src/battle/BattleState.lua | 6 +----- src/battle/Status.lua | 25 ++++++++++++++++++++----- src/ui/PartyMenu.lua | 3 ++- src/ui/SummaryMenu.lua | 3 ++- 4 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/battle/BattleState.lua b/src/battle/BattleState.lua index ec26c48b..b976ffb4 100644 --- a/src/battle/BattleState.lua +++ b/src/battle/BattleState.lua @@ -2527,11 +2527,7 @@ end -- the HUD label drawn in place of the level for a statused mon function BattleState:statusLabel(mon) - local record = Status.recordFor(self.data.statuses, mon.status) - if record then - return record.hudLabel or record.label or mon.status - end - return mon.status + return Status.hudLabelFor(self.data.statuses, mon.status) end -- the one accuracy roll (MoveHitTest), hooked as battle.accuracy diff --git a/src/battle/Status.lua b/src/battle/Status.lua index 9a217b65..86e5565c 100644 --- a/src/battle/Status.lua +++ b/src/battle/Status.lua @@ -54,6 +54,13 @@ end -- freeze the English. They are already translatable through the -- statuses registry (mod.content.statuses:patch(id, { label = ... })). -- +-- Do not add a matching hudLabel = "..." below: Status.hudLabelFor reads +-- hudLabel before label, and Registry:patch only overrides the fields a +-- mod actually passes, so a label-only translation patch would be +-- shadowed by this hudLabel forever. Nothing in this codebase gives +-- hudLabel a value different from label -- setting it here only recreates +-- that trap for no observed benefit. +-- -- The five persistent conditions as records: the beforeMove gauntlet, the -- residual sweep, the inflict text/immunities (StatusRegistry.inflict), -- the catch/wobble bonuses (Catching.attempt), the HUD label, and the @@ -61,7 +68,7 @@ end -- read these fields, so a mod's sixth status plugs into every consumer. Status.RECORDS = { SLP = { - id = "SLP", label = "SLP", hudLabel = "SLP", + id = "SLP", label = "SLP", catchBonus = 25, shakeBonus = 10, beforeMovePriority = 40, beforeMove = function(battler, _, battle) @@ -82,7 +89,7 @@ Status.RECORDS = { end, }, FRZ = { - id = "FRZ", label = "FRZ", hudLabel = "FRZ", + id = "FRZ", label = "FRZ", catchBonus = 25, shakeBonus = 10, beforeMovePriority = 30, beforeMove = function(battler, _, battle) @@ -96,7 +103,7 @@ Status.RECORDS = { end, }, PSN = { - id = "PSN", label = "PSN", hudLabel = "PSN", + id = "PSN", label = "PSN", catchBonus = 12, shakeBonus = 5, residual = damageOverTime("_HurtByPoisonText", Strings.source("%s's\nhurt by poison!")), @@ -112,7 +119,7 @@ Status.RECORDS = { end, }, BRN = { - id = "BRN", label = "BRN", hudLabel = "BRN", + id = "BRN", label = "BRN", catchBonus = 12, shakeBonus = 5, statPenalty = { stat = "attack", div = 2 }, residual = damageOverTime("_HurtByBurnText", @@ -124,7 +131,7 @@ Status.RECORDS = { end, }, PAR = { - id = "PAR", label = "PAR", hudLabel = "PAR", + id = "PAR", label = "PAR", catchBonus = 12, shakeBonus = 5, statPenalty = { stat = "speed", div = 4 }, beforeMovePriority = 10, @@ -160,6 +167,14 @@ function Status.recordFor(statuses, id) return (statuses or Status.RECORDS)[id] end +-- the HUD label for a status id: a mod's patched hudLabel/label if the +-- merged registry has one, the raw id otherwise (BattleState.statusLabel, +-- SummaryMenu.draw and PartyMenu.draw all read this the same way) +function Status.hudLabelFor(statuses, id) + local record = Status.recordFor(statuses, id) + return record and (record.hudLabel or record.label) or id +end + local function battleStatuses(battle) return battle and battle.data and battle.data.statuses end diff --git a/src/ui/PartyMenu.lua b/src/ui/PartyMenu.lua index 833b5ffb..4e2b9535 100644 --- a/src/ui/PartyMenu.lua +++ b/src/ui/PartyMenu.lua @@ -18,6 +18,7 @@ local Theme = require("src.ui.Theme") local FieldDefaults = require("src.world.FieldDefaults") local Map = require("src.world.Map") local Strings = require("src.core.Strings") +local Status = require("src.battle.Status") local PartyMenu = {} PartyMenu.__index = PartyMenu @@ -821,7 +822,7 @@ function PartyMenu:draw() if mon.hp <= 0 then Font.draw(Strings("FNT"), 136, y) elseif mon.status then - Font.draw(mon.status, 136, y) + Font.draw(Status.hudLabelFor(self.game.data.statuses, mon.status), 136, y) end -- the tile HP bar (DrawHP2 + SetPartyMenuHPBarColor). grayFill: -- tinting the fill AND running it through the row's zone diff --git a/src/ui/SummaryMenu.lua b/src/ui/SummaryMenu.lua index 30d0de4f..789c3135 100644 --- a/src/ui/SummaryMenu.lua +++ b/src/ui/SummaryMenu.lua @@ -14,6 +14,7 @@ local Font = require("src.render.Font") local TypeChart = require("src.battle.TypeChart") local Strings = require("src.core.Strings") local Stats = require("src.pokemon.Stats") +local Status = require("src.battle.Status") local SummaryMenu = {} SummaryMenu.__index = SummaryMenu @@ -145,7 +146,7 @@ function SummaryMenu:draw() HudTiles.drawHPBar(data, 11, 3, mon, 1, barZoned) -- wHPBarType 1 Font.draw(("%3d/%3d"):format(mon.hp, mon.stats.hp), 96, 32) Font.draw(Strings("STATUS/"), 72, 48) - Font.draw(mon.status or "OK", 128, 48) + Font.draw(Status.hudLabelFor(data.statuses, mon.status) or "OK", 128, 48) -- stats box (0,8) 10x10: names rows 9/11/13/15, values indented Font.drawBox(0, 8, 10, 10) From 085180992d8996e59d3c38990d01c4e8b85d60d2 Mon Sep 17 00:00:00 2001 From: thibautbus <310327033+thibautbus@users.noreply.github.com> Date: Tue, 18 Aug 2026 17:59:34 +0200 Subject: [PATCH 2/2] Cover the status abbreviation translation fix with a targeted test Neither tests/parity_status_true_color.lua (SGB recolor rectangle) nor tests/parity_party_icon_mirror.lua (icon mirroring) check the drawn status text, so this fix had no coverage. Drive SummaryMenu:draw() and PartyMenu:draw() with a mod-patched statuses registry and check the patched label reaches Font.draw instead of the raw status id, plus a vanilla case confirming the no-mod fallback is unchanged. Also cover the hudLabel-shadowing bug directly through the real Registry:patch (not a hand-built table): a label-only patch, the exact shape a translation mod would send, must reach Status.hudLabelFor for all five vanilla ids. Confirmed both regressions: reverting src/ui/*.lua and src/battle/*.lua to dev's pre-fix content fails 2 of the draw-site checks; reverting only the vanilla hudLabel removal in Status.lua fails the 3 checks whose French label differs from English (FRZ/BRN/SLP). --- .../status_abbreviation_translation_test.lua | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100644 tests/engine/status_abbreviation_translation_test.lua diff --git a/tests/engine/status_abbreviation_translation_test.lua b/tests/engine/status_abbreviation_translation_test.lua new file mode 100644 index 00000000..7c888a6a --- /dev/null +++ b/tests/engine/status_abbreviation_translation_test.lua @@ -0,0 +1,166 @@ +-- SummaryMenu.lua:148 and PartyMenu.lua:824 used to draw mon.status as a +-- bare literal ("PSN", "PAR", "BRN", "FRZ", "SLP"), invisible to any +-- translation a mod supplies. Unlike the strings catalog, a mod translates +-- status abbreviations through the statuses content registry +-- (mod.content.statuses:patch(id, { label = value }), label only), the +-- same registry src/battle/BattleState.lua:statusLabel already reads in +-- battle. This test drives both screens' status draw with a mod-patched +-- registry and checks the patched label reaches Font.draw, not the raw +-- status id. +-- +-- It also guards a second bug found alongside the first: Status.RECORDS' +-- five vanilla entries used to set hudLabel to the same literal as label +-- ("FRZ", hudLabel = "FRZ", ...). Since Status.hudLabelFor (and +-- BattleState:statusLabel before it) reads "hudLabel or label", and +-- Registry:patch only overrides the fields a mod actually passes, a +-- real label-only patch left the untouched vanilla hudLabel shadowing it +-- forever -- the translation was stored but never displayed anywhere, +-- in or out of battle. +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") + +love = love or {} +love.graphics = { + setColor = function() end, + rectangle = function() end, + draw = function() end, + push = function() end, pop = function() end, + translate = function() end, scale = function() end, +} + +package.loaded["src.render.Font"] = { + draw = function() end, + drawCode = function() end, + drawBox = function() end, +} +package.loaded["src.render.HudTiles"] = { + statusTile = function() end, + tile = function() end, + drawHPBar = function() end, +} +package.loaded["src.render.PaletteFX"] = { + shader = function() return nil end, + pal = function() return nil end, + markTrueColor = function() end, +} +package.loaded["src.ui.Theme"] = { cursor = 0, cursorHollow = 0 } +package.loaded["src.render.Assets"] = {} +package.loaded["src.world.FieldDefaults"] = {} +package.loaded["src.world.Map"] = {} +package.loaded["src.mods.Runtime"] = { wantsHook = function() return false end } +package.loaded["src.ui.Screens"] = {} +package.loaded["src.core.Logger"] = { warn = function() end } + +local Status = require("src.battle.Status") + +-- a mod's registered status translation, same shape mod.content.statuses: +-- patch(id, { label = ..., hudLabel = ... }) merges into Data.statuses +local function moddedStatuses() + local statuses = {} + for id, record in pairs(Status.RECORDS) do statuses[id] = record end + statuses.PSN = { id = "PSN", label = "PSN", hudLabel = "TOX" } + return statuses +end + +local Font = package.loaded["src.render.Font"] +local drawn +local origDraw = Font.draw +Font.draw = function(text, x, y) + drawn[#drawn + 1] = { text = text, x = x, y = y } + return origDraw(text, x, y) +end + +local function mkDef() + return { name = "BULBASAUR", dex = 1, types = { "GRASS" } } +end + +local function mkMon(status) + return { + nickname = "SAUR", species = "BULBASAUR", level = 5, + hp = 10, stats = { hp = 10, attack = 5, defense = 5, speed = 5, special = 5 }, + status = status, + } +end + +-- ---- SummaryMenu: page 1's STATUS/ line (~line 148-151) ---- +do + local SummaryMenu = assert(loadfile("src/ui/SummaryMenu.lua"))() + local game = { + data = { pokemon = { BULBASAUR = mkDef() }, statuses = moddedStatuses() }, + save = { player = { id = 1, name = "RED" } }, + } + local menu = setmetatable( + { game = game, mon = mkMon("PSN"), page = 1 }, SummaryMenu) + drawn = {} + menu:draw() + local statusDraw + for _, d in ipairs(drawn) do + if d.x == 128 and d.y == 48 then statusDraw = d end + end + T.check(statusDraw ~= nil, "SummaryMenu draws a status label at (128,48)") + T.eq(statusDraw.text, "TOX", + "SummaryMenu draws the mod-patched hudLabel, not the raw status id") +end + +-- vanilla (no mod): falls back to the plain id, same as before the fix +do + local SummaryMenu = assert(loadfile("src/ui/SummaryMenu.lua"))() + local game = { + data = { pokemon = { BULBASAUR = mkDef() }, statuses = nil }, + save = { player = { id = 1, name = "RED" } }, + } + local menu = setmetatable( + { game = game, mon = mkMon("PSN"), page = 1 }, SummaryMenu) + drawn = {} + menu:draw() + local statusDraw + for _, d in ipairs(drawn) do + if d.x == 128 and d.y == 48 then statusDraw = d end + end + T.eq(statusDraw.text, "PSN", + "SummaryMenu still shows the vanilla PSN label with no mod loaded") +end + +-- ---- PartyMenu: the roster row's status column (~line 824-827) ---- +do + local PartyMenu = assert(loadfile("src/ui/PartyMenu.lua"))() + PartyMenu.drawIcon = function() end + local game = { + data = { pokemon = { BULBASAUR = mkDef() }, statuses = moddedStatuses(), + text = {} }, + save = { party = { mkMon("PSN") } }, + } + local list = setmetatable({ game = game, index = 1 }, PartyMenu) + drawn = {} + list:draw() + local statusDraw + for _, d in ipairs(drawn) do + if d.x == 136 then statusDraw = d end + end + T.check(statusDraw ~= nil, "PartyMenu draws a status label at x=136") + T.eq(statusDraw.text, "TOX", + "PartyMenu draws the mod-patched hudLabel, not the raw status id") +end + +-- ---- real Registry:patch, not a hand-built table: a label-only patch (the +-- shape a translation mod would send for every one of the 5 vanilla +-- statuses) must reach the HUD despite the vanilla record already +-- defining hudLabel ---- +do + local Registry = require("src.mods.Registry") + local reg = Registry.new("statuses", { semantics = "record", target = "statuses" }) + reg.base = function() return Status.RECORDS end + local LABEL_ONLY_PATCH = { SLP = "SOM", FRZ = "GEL", PSN = "PSN", BRN = "BRU", PAR = "PAR" } + for id, translated in pairs(LABEL_ONLY_PATCH) do + reg:patch(id, { label = translated }, "mod") + end + local merged = {} + for id in pairs(Status.RECORDS) do merged[id] = reg:get(id) end + for id, translated in pairs(LABEL_ONLY_PATCH) do + T.eq(Status.hudLabelFor(merged, id), translated, + "a label-only mod patch on " .. id .. " reaches the HUD label") + end +end + +T.finish("status_abbreviation_translation_test")