Merge pull request #611 from ShaneMcGovernIE/fix/oak-last-ball-601

This commit is contained in:
bryanthaboi
2026-08-01 16:00:52 -04:00
committed by GitHub
3 changed files with 255 additions and 5 deletions
+14 -5
View File
@@ -6,6 +6,8 @@
-- takes it ("I'll take this one, then!") and both balls disappear.
-- Source: scripts/OaksLab.asm OaksLabCharmanderPokeBallText /
-- OaksLabRivalTakePokeBallScript.
-- * Leftover ball (after the pick): Oak turns and reads the last-mon
-- line instead of re-offering the starter (OaksLabLastMonScript, #601).
-- * Rival (object 1): before starter -> "go ahead and choose" once Oak
-- has walked you in, else "gramps isn't around" (#218); with
-- starter -> taunt + battle OPP_RIVAL1 with the counter-pick party
@@ -23,7 +25,7 @@ local function starterBall(askText, species, choseFlag, ownBall,
{ "jump_if_true", 20 }, -- 2
-- no picking until Oak has walked you in (OaksLabScript gating)
{ "check_flag", "EVENT_FOLLOWED_OAK_INTO_LAB" }, -- 3
{ "jump_if_false", 20 }, -- 4
{ "jump_if_false", 22 }, -- 4
-- the Pokédex "new species" entry shows before the ask (predef
-- StarterDex ahead of OaksLabYouWant...Text). StarterDex temporarily
-- sets the owned bits so ShowPokedexData prints height/weight/text;
@@ -31,7 +33,7 @@ local function starterBall(askText, species, choseFlag, ownBall,
{ "push_screen", "DexEntryMenu",
{ species = species, forceOwned = true } }, -- 5
{ "ask", askText }, -- 6
{ "jump_if_false", 21 }, -- 7
{ "jump_if_false", "end" }, -- 7
-- OaksLab.asm prints ReceivedMon then AddPartyMon (AskName lives
-- inside give_pokemon). Show the received text first so the
-- nickname prompt follows "you got X", matching Gen1.
@@ -52,9 +54,16 @@ local function starterBall(askText, species, choseFlag, ownBall,
{ RAM = rivalBall == "OAKSLAB_CHARMANDER_POKE_BALL" and "CHARMANDER"
or rivalBall == "OAKSLAB_SQUIRTLE_POKE_BALL" and "SQUIRTLE"
or "BULBASAUR" } }, -- 17
{ "jump", 21 }, -- 18
{ "jump", 21 }, -- 19 (spacer)
{ "show_text", "_OaksLabThoseArePokeBallsText" }, -- 20
{ "jump", "end" }, -- 18
{ "jump", "end" }, -- 19 (spacer)
-- a leftover ball after the player's pick: Oak turns to face the
-- player and reads the last-mon line instead of re-offering the
-- starter (scripts/OaksLab.asm OaksLabSelectedPokeBallScript ->
-- OaksLabLastMonScript; #601). The ROM's "#MON" ligature is spelled
-- out as Pokémon here.
{ "face_object", 5, "down" }, -- 20
{ "show_text", "That's PROF.OAK's\nlast Pokémon!" }, -- 21
{ "show_text", "_OaksLabThoseArePokeBallsText" }, -- 22
}
end
+112
View File
@@ -0,0 +1,112 @@
-- Driver: regression coverage for #601 "Wrong dialogue when interacting
-- with Prof. Oak's last ball".
--
-- After the player picks a starter and the rival takes his, the leftover
-- ball on the lab table must show "That's PROF.OAK's last Pokémon!" --
-- pret/pokered scripts/OaksLab.asm OaksLabSelectedPokeBallScript jumps
-- every ball handler to OaksLabLastMonScript once EVENT_GOT_STARTER is
-- set (Oak turns to face the player first). The buggy port fell through
-- to _OaksLabThoseArePokeBallsText ("Those are POKé BALLs...") instead.
--
-- Scenario A (the #601 regression): with a starter already picked, talk
-- to the leftover ball -> Oak faces down, box says "last Pokémon!",
-- and no starter offer/dex appears. Fails before the fix (the box
-- says "Those are POKé BALLs").
-- Scenario B (guard): with NO starter and not escorted in, the ball still
-- says "Those are POKé BALLs". Passes before and after the fix.
--
-- Setup: flags are set directly (pick flow never runs), so all three
-- balls stay visible; the player stands left of the Charmander ball
-- (cell 6,3), the leftover slot for the Squirtle pick (rival took the
-- Bulbasaur ball). The lab battle flag is set so the rival is gone and
-- cannot intercept the talk. TextBox.new is hooked to capture the raw
-- box text.
return function(game)
local U = dofile("tests/drivers/util.lua")
local DIR = os.getenv("SHOT_DIR") or "/tmp/shots"
local TextBox = require("src.render.TextBox")
local origNew = TextBox.new
local lastText
TextBox.new = function(g, text, ...)
lastText = text
return origNew(g, text, ...)
end
local function restore()
TextBox.new = origNew
end
local function setFlags(postPick)
local flags = game.save.flags or {}
game.save.flags = flags
flags.EVENT_FOLLOWED_OAK_INTO_LAB = true
if postPick then
flags.EVENT_GOT_STARTER = true
flags.EVENT_CHOSE_SQUIRTLE = true
else
flags.EVENT_GOT_STARTER = nil
end
-- rival already fought + gone, so he cannot intercept the talk
flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB = true
end
-- Talk to the ball at cell 6,3 (Charmander slot): stand one cell left
-- facing right and press A. Returns once a TextBox has been built.
local function talkToBall()
lastText = nil
U.teleport(game, "OAKS_LAB", 5, 3, "right")
U.wait(6)
for _ = 1, 8 do
U.tap(game, "a")
for _ = 1, 30 do
if lastText then return true end
U.wait(1)
end
end
return lastText ~= nil
end
-- ---- Scenario A: leftover ball after the pick
setFlags(true)
local aBoxOpened = talkToBall()
U.wait(30) -- let the typewriter reveal the line
U.shot(game, DIR .. "/a_last_ball.png")
local aText = lastText or "<none>"
local aPass = aBoxOpened
and aText:find("last Pokémon!", 1, true) ~= nil
and aText:find("Those are", 1, true) == nil
U.log("SCENARIO A box:", aText)
U.log("SCENARIO A", aPass and "PASS" or "FAIL")
-- close the box
for _ = 1, 10 do
if game.stack:top() == game.overworld then break end
U.tap(game, "a")
U.wait(2)
end
-- ---- Scenario B: pre-escort ball text unchanged
setFlags(false)
local bBoxOpened = talkToBall()
U.wait(30)
U.shot(game, DIR .. "/b_pre_escort.png")
local bText = lastText or "<none>"
local bPass = bBoxOpened
and bText:find("ThoseArePokeBalls", 1, true) ~= nil
or bText:find("Those are", 1, true) ~= nil
U.log("SCENARIO B box:", bText)
U.log("SCENARIO B", bPass and "PASS" or "FAIL")
-- restore hooks before any assert so a failure can't leave them installed
restore()
U.log("RESULT bug601", (aPass and bPass) and "PASS" or "FAIL")
assert(aPass,
"Leftover ball after the pick must say 'That's PROF.OAK's last "
.. "Pokémon!' (no 'Those are POKé BALLs'); got: " .. aText)
assert(bPass,
"Pre-escort balls must keep the 'Those are POKé BALLs' line; got: "
.. bText)
end
+129
View File
@@ -0,0 +1,129 @@
-- Regression coverage for #601 "Wrong dialogue when interacting with Prof.
-- Oak's last ball" (T2, ROM-free).
--
-- pret/pokered scripts/OaksLab.asm OaksLabSelectedPokeBallScript: once
-- EVENT_GOT_STARTER is set, EVERY ball's text handler jumps to
-- OaksLabLastMonScript -- Oak turns to face the player and reads
-- "_OaksLabLastMonText" ("That's PROF.OAK's last #MON!") instead of
-- re-offering the starter. The buggy port fell through to
-- _OaksLabThoseArePokeBallsText ("Those are POKé BALLs...") on every ball
-- once a starter had been picked. The fix also spells the ROM's "#MON"
-- ligature out as "Pokémon".
--
-- The three ball scripts share one table (starterBall), so this suite
-- drives that table through a mini ScriptRunner-compatible executor:
-- flag checks, jumps, "end" halts and text rows are executed, UI-heavy
-- commands (push_screen, ask, give_pokemon, npc moves) are no-ops with
-- ask recording the offer text. It then asserts the whole flow:
-- * GOT_STARTER + talk -> Oak faces down + "last Pokémon!" line, ends
-- * no GOT_STARTER, not escorted in -> "Those are POKé BALLs"
-- * no GOT_STARTER, escorted in -> the dex/ask offer (unchanged path)
-- plus MapScripts.validateContribution stays clean (the pre-fix table
-- carried nine out-of-range "jump 21" findings -- its run-time "end").
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local MapScripts = require("src.script.MapScripts")
local contribution = dofile("data/scripts/oaks_lab.lua")
local problems = MapScripts.validateContribution(contribution)
T.eq(#problems, 0, "oaks_lab contribution validates cleanly")
for _, p in ipairs(problems) do
T.check(false, "unexpected finding: " .. p)
end
local BALL = "TEXT_OAKSLAB_CHARMANDER_POKE_BALL"
-- ---- mini executor over the talk rows (ScriptRunner semantics: a jump
-- command returns the next row index or "end" to halt)
local function run(script, flags, answer)
local pc, texts, offers = 1, {}, {}
local lastCheck = nil
while pc <= #script do
local row = script[pc]
local verb = row[1]
if verb == "check_flag" then
lastCheck = flags[row[2]] == true
elseif verb == "jump_if_true" then
if lastCheck then
if row[2] == "end" then break end
pc = row[2] goto next
end
elseif verb == "jump_if_false" then
if not lastCheck then
if row[2] == "end" then break end
pc = row[2] goto next
end
elseif verb == "jump" then
if row[2] == "end" then break end
pc = row[2]
goto next
elseif verb == "show_text" then
texts[#texts + 1] = row[2]
elseif verb == "ask" then
offers[#offers + 1] = row[2]
if answer == false then
pc = pc + 1 -- decline: the next row's jump_if_false decides
goto next
end
end
-- push_screen / give_pokemon / set_flag / hide_object / move_npc_to /
-- face_object: no-op here (set_flag is exercised via the fixture
-- flags table instead of being run)
pc = pc + 1
::next::
end
return texts, offers
end
local function concat(list)
return table.concat(list, "\n")
end
-- ---- leftover ball after the pick: Oak faces down + the last-mon line
local got = { EVENT_GOT_STARTER = true, EVENT_FOLLOWED_OAK_INTO_LAB = true }
local texts, offers = run(contribution.talk[BALL], got, true)
T.eq(#offers, 0, "no starter offer after the pick")
local box = concat(texts)
T.check(box:find("last Pokémon!", 1, true) ~= nil,
"leftover ball says the last-mon line (got: " .. box .. ")")
T.check(box:find("Those are", 1, true) == nil,
"leftover ball no longer says 'Those are POKé BALLs'")
T.check(box:find("#MON", 1, true) == nil,
"the ROM #MON ligature is spelled out as Pokémon")
-- the pokered beat also turns Oak to face the player
T.check(contribution.talk[BALL][20][1] == "face_object"
and contribution.talk[BALL][20][2] == 5
and contribution.talk[BALL][20][3] == "down",
"row 20 faces Oak down before the line (OaksLabLastMonScript)")
-- ---- pre-escort: still the vanilla "Those are POKé BALLs" line
local pre = { EVENT_GOT_STARTER = false, EVENT_FOLLOWED_OAK_INTO_LAB = false }
local t2, o2 = run(contribution.talk[BALL], pre, true)
T.eq(#o2, 0, "no offer before Oak escorts the player in")
T.check(concat(t2):find("ThoseArePokeBalls", 1, true) ~= nil,
"pre-escort balls keep the 'Those are POKé BALLs' line")
-- ---- escorted in but no pick yet: the dex + "You want X?" offer
local mid = { EVENT_GOT_STARTER = false, EVENT_FOLLOWED_OAK_INTO_LAB = true }
local t3, o3 = run(contribution.talk[BALL], mid, true)
T.eq(#o3, 1, "the starter offer still runs before the pick")
T.check(concat(t3):find("last Pokémon!", 1, true) == nil,
"no last-mon line before the pick")
-- ---- all three balls share the same table shape (last-mon beat present)
for _, key in ipairs({
"TEXT_OAKSLAB_CHARMANDER_POKE_BALL",
"TEXT_OAKSLAB_SQUIRTLE_POKE_BALL",
"TEXT_OAKSLAB_BULBASAUR_POKE_BALL",
}) do
local script = contribution.talk[key]
T.check(script and script[21] and script[21][2] and
script[21][2]:find("Pokémon", 1, true) ~= nil,
key .. " carries the last-mon line")
end
T.finish("oaks_lab_last_ball_bug601")