mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-20 20:50:21 +02:00
Cover the romText fixes with targeted regression tests
Every existing test around these callsites only ever runs with an
empty/fixture Data.text, so none of them could tell a properly-wired
romText/t._X call apart from a literal that never looked at the
catalog at all -- every assertion passed either way, the same coverage
gap the museum ticket clerk and status abbreviation fixes hit earlier.
Seven new tests fake the real label for each fix and assert the pushed
or queued message uses the translated value, plus a vanilla case per
fix confirming the no-catalog fallback still matches the original
English literal exactly:
- box_release_confirmation_romtext.lua (BoxMenu _OnceReleasedText, via
a TextBox.new spy so real pagination/choice behavior stays intact)
- slot_machine_lined_up_romtext.lua (SlotMachine _LinedUpText, symbol
concatenated in front of the translated suffix)
- battle_fainted_message_romtext.lua (BattleState onFaint, both
_PlayerMonFaintedText and _EnemyMonFaintedText, confirming the raw
name reaches each without a duplicated "Enemy")
- battle_catch_messages_romtext.lua (BattleState storeCaughtMon,
_ItemUseBallText06 plus both _ItemUseBallText07/08 branches on
EVENT_MET_BILL)
- battle_ball_dodge_romtext.lua (BattleState throwBall,
_ItemUseBallText00's \f-merge collapsing to exactly one queued
message)
- overworld_field_faint_heal_romtext.lua (OverworldController
applyFieldPoison's _PokemonFaintedText, and
useSoftboiledFieldMove's _ItemUseNoEffectText/_PotionText including
the recovered-amount slot the old literal never showed)
- overworld_hidden_item_romtext.lua (OverworldController
tryHiddenObject's _FoundHiddenItemText, both the {PLAYER} token and
the item name landing in the right slots)
Confirmed several of these fail against the pre-fix code and pass
against the current code, not just reasoned about it. Not every one of
the 15 fixed callsites has its own dedicated test -- the ShopMenu,
LinkBattle, trainer-withdraw/sent-out and the normal (non-hidden)
found-item sites share the same romText mechanism already proven
correct by the seven tests above, and building the heavier fixtures
each would need (a full mart flow, a link session, a trainer AI
switch, an object_event NPC) wasn't judged worth it for what would be
the same assertion shape again.
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
-- BattleState:throwBall()'s can't-be-caught path used to queue two
|
||||
-- separate Strings() literals ("It dodged the\nthrown BALL!" then "This
|
||||
-- POKéMON\ncan't be caught!"). The real ROM label _ItemUseBallText00
|
||||
-- combines both as one \f-paged string. TextBox.new() would split \f
|
||||
-- itself, but throwBall() queues through self:sayNext(), which goes
|
||||
-- through the battle queue's own BattleState:startMessage() -- and that
|
||||
-- one only splits on \n/\v, not \f (confirmed live: the \f landed
|
||||
-- mid-line and the second sentence overflowed off the box instead of
|
||||
-- starting a fresh page). The fix resolves the label once, then splits
|
||||
-- it the same way TextBox.lua does and queues one sayNext per page. This
|
||||
-- test fakes the label and checks the two pages reach the queue as two
|
||||
-- separate messages, in order, not merged into one with a raw \f still
|
||||
-- inside it.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local Data = T.fixtures.fresh()
|
||||
require("src.render.Font").load(Data)
|
||||
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
|
||||
local function mkbattle()
|
||||
local save = SaveData.newGame()
|
||||
save.party = { Pokemon.new(Data, "FIXMON_A", 10) }
|
||||
local game = { data = Data, save = save,
|
||||
stack = { top = function() return nil end, push = function() end } }
|
||||
local battle = BattleState.newWild(game, "FIXMON_C", 8)
|
||||
battle.ghost = true -- forces the can't-be-caught path
|
||||
return battle
|
||||
end
|
||||
|
||||
-- throwBall defers its message-queuing work into self.queue via one
|
||||
-- top-level self:act(fn); run only that one function to reach the
|
||||
-- say() calls the fix touches. It's the last entry throwBall itself
|
||||
-- appends (after the immediate sayAuto), and it further queues its own
|
||||
-- self:act(function() self:executeAction(...) end) for the actual enemy
|
||||
-- turn -- deliberately NOT run here (out of scope, and running the queue
|
||||
-- generically after mutation risks looping into a real turn simulation)
|
||||
local function runThrowBallAct(battle)
|
||||
for i = #battle.queue, 1, -1 do
|
||||
if battle.queue[i].fn then
|
||||
battle.queue[i].fn()
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function textEntries(battle)
|
||||
local out = {}
|
||||
for _, entry in ipairs(battle.queue) do
|
||||
if entry.text then out[#out + 1] = entry.text end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
-- translated: the faked label's two \f-separated pages reach the queue
|
||||
-- as two separate messages, in order, and neither one still contains a
|
||||
-- raw \f (which would mean the battle queue's own renderer has to deal
|
||||
-- with it, and it can't)
|
||||
do
|
||||
local battle = mkbattle()
|
||||
Data.text._ItemUseBallText00 = "FAKE-DODGE!\fFAKE-CANTCATCH!"
|
||||
battle:throwBall("FIX_BALL")
|
||||
runThrowBallAct(battle)
|
||||
local texts = textEntries(battle)
|
||||
T.eq(texts[1], "FAKE-DODGE!", "page 1 reaches the queue on its own")
|
||||
T.eq(texts[2], "FAKE-CANTCATCH!", "page 2 follows right after, still in order")
|
||||
for _, t in ipairs(texts) do
|
||||
T.check(not t:find("\f", 1, true), "no queued message still carries a raw \\f")
|
||||
end
|
||||
Data.text._ItemUseBallText00 = nil
|
||||
end
|
||||
|
||||
-- vanilla: no catalog entry still falls back to the two English pages,
|
||||
-- split the same way
|
||||
do
|
||||
local battle = mkbattle()
|
||||
battle:throwBall("FIX_BALL")
|
||||
runThrowBallAct(battle)
|
||||
local texts = textEntries(battle)
|
||||
T.eq(texts[1], "It dodged the\nthrown BALL!", "vanilla page 1")
|
||||
T.eq(texts[2], "This POKéMON\ncan't be caught!", "vanilla page 2")
|
||||
end
|
||||
|
||||
T.finish("battle_ball_dodge_romtext")
|
||||
@@ -0,0 +1,77 @@
|
||||
-- BattleState:storeCaughtMon() queues up to two plain-Lua-literal
|
||||
-- messages: the new-Pokedex-data line (_ItemUseBallText06) and, when the
|
||||
-- party is full, the box-transfer line (_ItemUseBallText07/08, keyed on
|
||||
-- EVENT_MET_BILL -- two full, independently-translated ROM strings, not
|
||||
-- one template with a substituted PC name). This test fakes all three
|
||||
-- labels and checks the queued messages use them, not the English
|
||||
-- literals.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local Data = T.fixtures.fresh()
|
||||
require("src.render.Font").load(Data)
|
||||
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
|
||||
local function findText(battle, needle)
|
||||
for _, entry in ipairs(battle.queue) do
|
||||
if entry.text and entry.text:find(needle, 1, true) then return entry.text end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function mkbattle(partySize, metBill)
|
||||
local save = SaveData.newGame()
|
||||
save.party = {}
|
||||
for i = 1, partySize do
|
||||
save.party[i] = Pokemon.new(Data, "FIXMON_A", 5)
|
||||
end
|
||||
save.flags = save.flags or {}
|
||||
save.flags.EVENT_MET_BILL = metBill
|
||||
local game = { data = Data, save = save,
|
||||
stack = { top = function() return nil end, push = function() end } }
|
||||
return BattleState.newWild(game, "FIXMON_C", 8)
|
||||
end
|
||||
|
||||
-- empty party: Party.add succeeds, only the new-Pokedex-data message fires
|
||||
do
|
||||
local battle = mkbattle(0, false)
|
||||
Data.text._ItemUseBallText06 = "FAKE-DEX {RAM:wEnemyMonNick} FAKE!"
|
||||
battle:storeCaughtMon()
|
||||
T.eq(findText(battle, "FAKE-DEX"), "FAKE-DEX " .. battle.enemy.name .. " FAKE!",
|
||||
"a translated _ItemUseBallText06 reaches the new-Pokedex-data message")
|
||||
Data.text._ItemUseBallText06 = nil
|
||||
end
|
||||
|
||||
-- full party, EVENT_MET_BILL true: box transfer via _ItemUseBallText07
|
||||
do
|
||||
local battle = mkbattle(6, true)
|
||||
Data.text._ItemUseBallText07 = "FAKE-BILL {RAM:wBoxMonNicks} FAKE!"
|
||||
battle:storeCaughtMon()
|
||||
T.eq(findText(battle, "FAKE-BILL"), "FAKE-BILL " .. battle.enemy.name .. " FAKE!",
|
||||
"EVENT_MET_BILL true routes through the translated _ItemUseBallText07")
|
||||
Data.text._ItemUseBallText07 = nil
|
||||
end
|
||||
|
||||
-- full party, EVENT_MET_BILL false: box transfer via _ItemUseBallText08
|
||||
do
|
||||
local battle = mkbattle(6, false)
|
||||
Data.text._ItemUseBallText08 = "FAKE-SOMEONE {RAM:wBoxMonNicks} FAKE!"
|
||||
battle:storeCaughtMon()
|
||||
T.eq(findText(battle, "FAKE-SOMEONE"), "FAKE-SOMEONE " .. battle.enemy.name .. " FAKE!",
|
||||
"EVENT_MET_BILL false routes through the translated _ItemUseBallText08")
|
||||
Data.text._ItemUseBallText08 = nil
|
||||
end
|
||||
|
||||
-- vanilla, full party, EVENT_MET_BILL true: English literal, BILL's PC
|
||||
do
|
||||
local battle = mkbattle(6, true)
|
||||
battle:storeCaughtMon()
|
||||
T.eq(findText(battle, "transferred"),
|
||||
battle.enemy.name .. " was\ntransferred to\nBILL's PC!",
|
||||
"no catalog entry falls back to the English BILL's-PC literal")
|
||||
end
|
||||
|
||||
T.finish("battle_catch_messages_romtext")
|
||||
@@ -0,0 +1,64 @@
|
||||
-- BattleState:onFaint's "%s\nfainted!" collapsed two distinct ROM
|
||||
-- strings (_EnemyMonFaintedText already carries its own "Enemy" wording;
|
||||
-- _PlayerMonFaintedText does not) into one literal, substituted with
|
||||
-- displayName(battler) -- which itself runs the enemy name through a
|
||||
-- SEPARATE Strings("Enemy %s", ...) call. The fix passes the raw
|
||||
-- battler.name and lets each label supply its own wording. This test
|
||||
-- fakes both labels and checks the raw name reaches the right one, with
|
||||
-- no "Enemy" ever duplicated.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local Data = T.fixtures.fresh()
|
||||
require("src.render.Font").load(Data)
|
||||
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
|
||||
local function mkbattle()
|
||||
local save = SaveData.newGame()
|
||||
save.party = { Pokemon.new(Data, "FIXMON_A", 10) }
|
||||
local game = { data = Data, save = save,
|
||||
stack = { top = function() return nil end, push = function() end } }
|
||||
return BattleState.newWild(game, "FIXMON_C", 8)
|
||||
end
|
||||
|
||||
-- finds the queued say-message text (onFaint queues several entries: a
|
||||
-- wait, then the say)
|
||||
local function findText(battle)
|
||||
for _, entry in ipairs(battle.queue) do
|
||||
if entry.text then return entry.text end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- enemy: translated _EnemyMonFaintedText reaches onFaint, raw name only
|
||||
do
|
||||
local battle = mkbattle()
|
||||
Data.text._EnemyMonFaintedText = "FAKE-ENEMY {RAM:wEnemyMonNick} FAKE!"
|
||||
battle:onFaint(battle.enemy)
|
||||
T.eq(findText(battle), "FAKE-ENEMY " .. battle.enemy.name .. " FAKE!",
|
||||
"a translated _EnemyMonFaintedText reaches the enemy faint message")
|
||||
Data.text._EnemyMonFaintedText = nil
|
||||
end
|
||||
|
||||
-- enemy vanilla: the English literal already carries "Enemy " itself
|
||||
do
|
||||
local battle = mkbattle()
|
||||
battle:onFaint(battle.enemy)
|
||||
T.eq(findText(battle), "Enemy " .. battle.enemy.name .. "\nfainted!",
|
||||
"no catalog entry falls back to the English literal, Enemy included")
|
||||
end
|
||||
|
||||
-- player: translated _PlayerMonFaintedText reaches onFaint
|
||||
do
|
||||
local battle = mkbattle()
|
||||
Data.text._PlayerMonFaintedText = "FAKE-PLAYER {RAM:wBattleMonNick} FAKE!"
|
||||
battle:onFaint(battle.player)
|
||||
T.eq(findText(battle), "FAKE-PLAYER " .. battle.player.name .. " FAKE!",
|
||||
"a translated _PlayerMonFaintedText reaches the player faint message")
|
||||
Data.text._PlayerMonFaintedText = nil
|
||||
end
|
||||
|
||||
T.finish("battle_fainted_message_romtext")
|
||||
@@ -0,0 +1,129 @@
|
||||
-- BoxMenu's RELEASE confirmation ("Once released,\n%s is\ngone forever.
|
||||
-- OK?") used to be a bare Lua literal. tests/engine/pc_release.lua only
|
||||
-- ever runs with an empty Data.text, so it can't tell a properly-wired
|
||||
-- t._OnceReleasedText or "..." fallback apart from a literal that never
|
||||
-- looked at t at all -- every assertion there passes either way. This
|
||||
-- test drives the same interactive release flow with a faked
|
||||
-- Data.text._OnceReleasedText and checks the pushed TextBox's raw text
|
||||
-- (captured via a TextBox.new spy, so real pagination/choice behavior is
|
||||
-- untouched) uses the translated value, not the English literal.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local Data = T.fixtures.fresh()
|
||||
local ids = T.fixtures.ids
|
||||
require("src.render.Font").load(Data)
|
||||
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local Boxes = require("src.pokemon.Boxes")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local BoxMenu = require("src.ui.BoxMenu")
|
||||
local ListMenu = require("src.ui.ListMenu")
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local Sound = require("src.core.Sound")
|
||||
|
||||
local realCry, realPlay = Sound.playCry, Sound.play
|
||||
Sound.playCry = function() end
|
||||
Sound.play = function() end
|
||||
|
||||
-- spy: records every raw text TextBox.new receives, without disturbing
|
||||
-- pagination/choice re-push, so the interactive flow behaves exactly like
|
||||
-- pc_release.lua's
|
||||
local captured
|
||||
local realNew = TextBox.new
|
||||
TextBox.new = function(game, text, onDone, opts)
|
||||
captured[#captured + 1] = text
|
||||
return realNew(game, text, onDone, opts)
|
||||
end
|
||||
|
||||
local stack = { states = {} }
|
||||
function stack:push(s) self.states[#self.states + 1] = s end
|
||||
function stack:pop()
|
||||
local t = self.states[#self.states]
|
||||
self.states[#self.states] = nil
|
||||
return t
|
||||
end
|
||||
function stack:top() return self.states[#self.states] end
|
||||
function stack:update(dt)
|
||||
local t = self:top()
|
||||
if t and t.update then t:update(dt) end
|
||||
end
|
||||
|
||||
local pressed = {}
|
||||
local function press(btn)
|
||||
pressed = { [btn] = true }
|
||||
stack:update(1 / 60)
|
||||
pressed = {}
|
||||
end
|
||||
|
||||
local function topMt() return getmetatable(stack:top()) end
|
||||
local function mash(btn, cond, n)
|
||||
for _ = 1, (n or 400) do
|
||||
if cond() then return true end
|
||||
press(btn)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function mkGame()
|
||||
stack.states = {}
|
||||
captured = {}
|
||||
local game = {
|
||||
data = Data,
|
||||
save = SaveData.newGame(),
|
||||
stack = stack,
|
||||
input = {
|
||||
wasPressed = function(_, key) return pressed[key] or false end,
|
||||
isDown = function() return false end,
|
||||
},
|
||||
}
|
||||
game.save.options = game.save.options or {}
|
||||
game.save.options.textSpeed = 1
|
||||
local box = Boxes.active(game.save)
|
||||
box[1] = Pokemon.new(Data, ids.species[1], 5)
|
||||
return game, box
|
||||
end
|
||||
|
||||
local function releaseFirstMon(game)
|
||||
stack:push(BoxMenu.new(game))
|
||||
press("down"); press("down"); press("a") -- open RELEASE list
|
||||
T.check(topMt() == ListMenu, "RELEASE opens the box list")
|
||||
press("a") -- choose the first (only) mon
|
||||
T.check(mash("a", function() return topMt() == ChoiceBox end),
|
||||
"confirm choice opens")
|
||||
-- the confirmation TextBox is captured[1] the moment it was pushed,
|
||||
-- before this mash even ran
|
||||
return captured[1]
|
||||
end
|
||||
|
||||
-- monName (BoxMenu.lua): mon.nickname or def.name
|
||||
local function monName(box)
|
||||
local mon = box[1]
|
||||
local def = Data.pokemon[mon.species]
|
||||
return mon.nickname or def.name
|
||||
end
|
||||
|
||||
-- translated: the fake value must reach the pushed TextBox
|
||||
do
|
||||
local game, box = mkGame()
|
||||
local name = monName(box)
|
||||
Data.text._OnceReleasedText = "FAKE {RAM:wStringBuffer} released!"
|
||||
local text = releaseFirstMon(game)
|
||||
T.eq(text, "FAKE " .. name .. " released!",
|
||||
"a translated _OnceReleasedText reaches the release confirmation")
|
||||
Data.text._OnceReleasedText = nil
|
||||
end
|
||||
|
||||
-- vanilla: with no catalog entry, the English literal still substitutes
|
||||
do
|
||||
local game, box = mkGame()
|
||||
local name = monName(box)
|
||||
local text = releaseFirstMon(game)
|
||||
T.eq(text, "Once released,\n" .. name .. " is\ngone forever. OK?",
|
||||
"no catalog entry still falls back to the English literal")
|
||||
end
|
||||
|
||||
TextBox.new = realNew
|
||||
Sound.playCry, Sound.play = realCry, realPlay
|
||||
T.finish("box_release_confirmation_romtext")
|
||||
@@ -0,0 +1,139 @@
|
||||
-- Two OverworldController.lua messages used to be bare Lua literals:
|
||||
-- applyFieldPoison()'s "%s\nfainted!" (the third of three collapsed
|
||||
-- fainted-message ROM strings, _PokemonFaintedText) and
|
||||
-- useSoftboiledFieldMove()'s "It won't have\nany effect."/"%s's HP\nwas
|
||||
-- restored!" (the same _ItemUseNoEffectText/_PotionText labels
|
||||
-- ItemEffects.lua's real potion message already uses -- _PotionText's
|
||||
-- second slot is the actual amount healed, which the old literal never
|
||||
-- showed at all).
|
||||
--
|
||||
-- Uses the debug.setupvalue technique already established in
|
||||
-- oaks_pc_flow.lua to fake the module-level Game/TextBox upvalues
|
||||
-- ROM-free, without going through the heavy OverworldState:enter().
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local Data = T.fixtures.fresh()
|
||||
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local OW = require("src.world.OverworldController")
|
||||
|
||||
local function setUpvalue(fn, name, val)
|
||||
local i = 1
|
||||
while true do
|
||||
local n = debug.getupvalue(fn, i)
|
||||
if not n then return false end
|
||||
if n == name then debug.setupvalue(fn, i, val); return true end
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
||||
local pushed = {}
|
||||
local textBoxStub = {
|
||||
new = function(_, text, onDone, opts)
|
||||
return { text = text, onDone = onDone, opts = opts }
|
||||
end,
|
||||
}
|
||||
local realSound = package.loaded["src.core.Sound"]
|
||||
package.loaded["src.core.Sound"] = { play = function() end, playCry = function() end }
|
||||
|
||||
local function mkGame()
|
||||
local save = SaveData.newGame()
|
||||
save.party = { Pokemon.new(Data, "FIXMON_A", 20) }
|
||||
pushed = {}
|
||||
return {
|
||||
data = Data, save = save,
|
||||
stack = { push = function(_, item) pushed[#pushed + 1] = item end },
|
||||
}
|
||||
end
|
||||
|
||||
for _, name in ipairs({ "applyFieldPoison", "useSoftboiledFieldMove" }) do
|
||||
T.check(setUpvalue(OW[name], "Game", mkGame()), ("Game upvalue on %s"):format(name))
|
||||
T.check(setUpvalue(OW[name], "TextBox", textBoxStub), ("TextBox upvalue on %s"):format(name))
|
||||
end
|
||||
|
||||
local fakeSelf = setmetatable({}, { __index = OW })
|
||||
|
||||
-- ---- applyFieldPoison: _PokemonFaintedText ----
|
||||
do
|
||||
local game = mkGame()
|
||||
setUpvalue(OW.applyFieldPoison, "Game", game)
|
||||
local mon = game.save.party[1]
|
||||
mon.status = "PSN"
|
||||
mon.hp = 1 -- one poison tick (1 dmg by default) faints it
|
||||
game.save.poisonSteps = 3 -- (3+1) % 4 == 0: this step ticks poison
|
||||
|
||||
Data.text._PokemonFaintedText = "FAKE {RAM:wNameBuffer} FAKE!"
|
||||
fakeSelf:applyFieldPoison()
|
||||
T.eq(pushed[1] and pushed[1].text, "FAKE " .. (mon.nickname or "FIXMON A") .. " FAKE!",
|
||||
"a translated _PokemonFaintedText reaches the field-poison faint message")
|
||||
Data.text._PokemonFaintedText = nil
|
||||
end
|
||||
|
||||
-- vanilla: no catalog entry, English literal
|
||||
do
|
||||
local game = mkGame()
|
||||
setUpvalue(OW.applyFieldPoison, "Game", game)
|
||||
local mon = game.save.party[1]
|
||||
mon.status = "PSN"
|
||||
mon.hp = 1
|
||||
game.save.poisonSteps = 3
|
||||
|
||||
fakeSelf:applyFieldPoison()
|
||||
T.eq(pushed[1] and pushed[1].text,
|
||||
(mon.nickname or "FIXMON A") .. "\nfainted!",
|
||||
"no catalog entry falls back to the English fainted literal")
|
||||
end
|
||||
|
||||
-- ---- useSoftboiledFieldMove: _ItemUseNoEffectText / _PotionText ----
|
||||
do
|
||||
local game = mkGame()
|
||||
setUpvalue(OW.useSoftboiledFieldMove, "Game", game)
|
||||
local user = Pokemon.new(Data, "FIXMON_A", 20)
|
||||
local target = Pokemon.new(Data, "FIXMON_B", 20)
|
||||
target.hp = target.stats.hp -- already full: no effect
|
||||
|
||||
Data.text._ItemUseNoEffectText = "FAKE-NOEFFECT!"
|
||||
local ok = fakeSelf:useSoftboiledFieldMove(user, target)
|
||||
T.check(ok == false, "a full-HP target reports no effect")
|
||||
T.eq(pushed[1] and pushed[1].text, "FAKE-NOEFFECT!",
|
||||
"a translated _ItemUseNoEffectText reaches the no-effect message")
|
||||
Data.text._ItemUseNoEffectText = nil
|
||||
end
|
||||
|
||||
do
|
||||
local game = mkGame()
|
||||
setUpvalue(OW.useSoftboiledFieldMove, "Game", game)
|
||||
local user = Pokemon.new(Data, "FIXMON_A", 20)
|
||||
local target = Pokemon.new(Data, "FIXMON_B", 20)
|
||||
target.hp = target.stats.hp - 10 -- missing exactly 10 HP
|
||||
|
||||
Data.text._PotionText = "FAKE {RAM:wNameBuffer} healed {NUM:wHPBarHPDifference, 2, 3}!"
|
||||
local ok = fakeSelf:useSoftboiledFieldMove(user, target)
|
||||
T.check(ok == true, "a damaged target heals successfully")
|
||||
T.eq(pushed[1] and pushed[1].text,
|
||||
"FAKE " .. (target.nickname or "FIXMON B") .. " healed 10!",
|
||||
"a translated _PotionText reaches the heal message, amount included")
|
||||
Data.text._PotionText = nil
|
||||
end
|
||||
|
||||
-- vanilla: no catalog entry, the fallback's single %s slot still fills
|
||||
-- correctly (the amount is silently dropped by design, same as
|
||||
-- ItemEffects.lua's own _PotionText fallback -- not a regression, this
|
||||
-- matches the pre-fix literal's behavior exactly)
|
||||
do
|
||||
local game = mkGame()
|
||||
setUpvalue(OW.useSoftboiledFieldMove, "Game", game)
|
||||
local user = Pokemon.new(Data, "FIXMON_A", 20)
|
||||
local target = Pokemon.new(Data, "FIXMON_B", 20)
|
||||
target.hp = target.stats.hp - 10
|
||||
local ok = fakeSelf:useSoftboiledFieldMove(user, target)
|
||||
T.check(ok == true, "a damaged target heals successfully (vanilla)")
|
||||
T.eq(pushed[1] and pushed[1].text,
|
||||
(target.nickname or "FIXMON B") .. "'s HP\nwas restored!",
|
||||
"no catalog entry falls back to the English literal (no amount shown)")
|
||||
end
|
||||
|
||||
package.loaded["src.core.Sound"] = realSound
|
||||
T.finish("overworld_field_faint_heal_romtext")
|
||||
@@ -0,0 +1,85 @@
|
||||
-- OverworldState:tryHiddenObject()'s "%s found\n%s!" message used to
|
||||
-- substitute both the player name and the item name into one bare Lua
|
||||
-- literal. The real _FoundHiddenItemText label leads with a {PLAYER}
|
||||
-- named token, which romText auto-fills from a 2-arg call in the same
|
||||
-- order the literal already used -- this test checks both slots land
|
||||
-- correctly (an accidental argument swap is the easy mistake this shape
|
||||
-- invites).
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local Data = T.fixtures.fresh()
|
||||
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local OW = require("src.world.OverworldController")
|
||||
|
||||
local function setUpvalue(fn, name, val)
|
||||
local i = 1
|
||||
while true do
|
||||
local n = debug.getupvalue(fn, i)
|
||||
if not n then return false end
|
||||
if n == name then debug.setupvalue(fn, i, val); return true end
|
||||
i = i + 1
|
||||
end
|
||||
end
|
||||
|
||||
local pushed = {}
|
||||
local textBoxStub = {
|
||||
new = function(_, text, onDone, opts)
|
||||
return { text = text, onDone = onDone, opts = opts }
|
||||
end,
|
||||
soundOpts = function() return {} end,
|
||||
}
|
||||
|
||||
local MAP_ID = "FIX_TOWN"
|
||||
Data.field.hiddenItems[MAP_ID] = { { x = 3, y = 3, item = "FIX_BALL" } }
|
||||
|
||||
local function mkGame()
|
||||
local save = SaveData.newGame()
|
||||
save.player.name = "FAKEPLAYER"
|
||||
pushed = {}
|
||||
return {
|
||||
data = Data, save = save,
|
||||
stack = { push = function(_, item) pushed[#pushed + 1] = item end },
|
||||
}
|
||||
end
|
||||
|
||||
T.check(setUpvalue(OW.tryHiddenObject, "Game", mkGame()), "Game upvalue on tryHiddenObject")
|
||||
T.check(setUpvalue(OW.tryHiddenObject, "TextBox", textBoxStub), "TextBox upvalue on tryHiddenObject")
|
||||
|
||||
local fakeSelf = setmetatable({ map = { id = MAP_ID } }, { __index = OW })
|
||||
|
||||
-- translated: player name and item name both land in the right slots
|
||||
do
|
||||
local game = mkGame()
|
||||
setUpvalue(OW.tryHiddenObject, "Game", game)
|
||||
Data.text._FoundHiddenItemText = "FAKE {PLAYER} found FAKE {RAM:wNameBuffer} FAKE!"
|
||||
local found = fakeSelf:tryHiddenObject(3, 3)
|
||||
T.check(found == true, "the hidden item at (3,3) is found")
|
||||
T.eq(pushed[1] and pushed[1].text,
|
||||
"FAKE FAKEPLAYER found FAKE FIX BALL FAKE!",
|
||||
"a translated _FoundHiddenItemText fills both {PLAYER} and the item name")
|
||||
Data.text._FoundHiddenItemText = nil
|
||||
end
|
||||
|
||||
-- vanilla: no catalog entry, so romText falls back to plain
|
||||
-- Strings(fallback, ...) -- the fallback literal is "%s found\n%s!" (both
|
||||
-- slots plain %s, matching the pre-fix literal's own shape), not the
|
||||
-- {PLAYER} token the real label uses, since Strings() never does
|
||||
-- {TOKEN} substitution on its own. (A {PLAYER}-token fallback would
|
||||
-- still render correctly too, since the real TextBox.new always runs
|
||||
-- TextBox.substitute over whatever text it's given -- but this test
|
||||
-- stubs TextBox without that call, and the fallback shouldn't lean on a
|
||||
-- substitution pass happening downstream regardless.)
|
||||
do
|
||||
local game = mkGame()
|
||||
setUpvalue(OW.tryHiddenObject, "Game", game)
|
||||
game.save.hiddenTaken = {} -- fresh spot
|
||||
local found = fakeSelf:tryHiddenObject(3, 3)
|
||||
T.check(found == true, "the hidden item is found again in a fresh game")
|
||||
T.eq(pushed[1] and pushed[1].text, "FAKEPLAYER found\nFIX BALL!",
|
||||
"with no catalog entry, the fallback still fills both the player "
|
||||
.. "and item name via plain %s substitution")
|
||||
end
|
||||
|
||||
T.finish("overworld_hidden_item_romtext")
|
||||
@@ -0,0 +1,39 @@
|
||||
-- SlotMachine:resolveWin's "%s lined up!\nScored %d coins!" message used
|
||||
-- to interpolate the symbol id AND the payout into one bare Lua literal.
|
||||
-- The real _LinedUpText label has no slot for the symbol at all (the
|
||||
-- original ROM drew it separately) -- only for the coin count. This test
|
||||
-- fakes _LinedUpText and checks the symbol is concatenated in front of the
|
||||
-- translated suffix, with the payout correctly substituted into it.
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
local Data = T.fixtures.fresh()
|
||||
|
||||
local SlotMachine = require("src.ui.SlotMachine")
|
||||
|
||||
local function mkSelf()
|
||||
local game = { data = Data, save = { coins = 0 } }
|
||||
return setmetatable({ game = game, allowMatchesCounter = 0 }, SlotMachine)
|
||||
end
|
||||
|
||||
-- translated: the fake suffix reaches self.message, with the symbol
|
||||
-- concatenated in front and the payout substituted into the fake text
|
||||
do
|
||||
local self = mkSelf()
|
||||
Data.text._LinedUpText = " FAKE-SUFFIX {RAM:wStringBuffer}!"
|
||||
self:resolveWin({ symbol = "CHERRY", payout = 8 })
|
||||
T.eq(self.message, "CHERRY FAKE-SUFFIX 8!",
|
||||
"a translated _LinedUpText reaches the lined-up message")
|
||||
Data.text._LinedUpText = nil
|
||||
end
|
||||
|
||||
-- vanilla: with no catalog entry, the English literal still substitutes,
|
||||
-- with its own leading space (the original had no slot for the symbol)
|
||||
do
|
||||
local self = mkSelf()
|
||||
self:resolveWin({ symbol = "CHERRY", payout = 8 })
|
||||
T.eq(self.message, "CHERRY lined up!\nScored 8 coins!",
|
||||
"no catalog entry still falls back to the English literal")
|
||||
end
|
||||
|
||||
T.finish("slot_machine_lined_up_romtext")
|
||||
Reference in New Issue
Block a user