From 0dd187fe3066c1047a68072a2d9f098029728b75 Mon Sep 17 00:00:00 2001 From: Shane McGovern Date: Sat, 1 Aug 2026 20:55:31 +0100 Subject: [PATCH] Fix wrong dialogue on Prof. Oak's leftover ball (#601) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After the player picks a starter and the rival takes his, every ball on the lab table fell through to 'Those are POKé BALLs' instead of the leftover-mon line. Port the pokered OaksLabSelectedPokeBallScript -> OaksLabLastMonScript beat: with EVENT_GOT_STARTER set, Oak turns to face the player and reads 'That's PROF.OAK's last Pokémon!'. The ROM's '#MON' ligature is spelled out as Pokémon. The renumbering also drops the table's nine out-of-range 'jump 21' rows (run-time 'end' idioms) for explicit "end" targets, so the script now validates cleanly. Tests: T2 suite drives the ball talk table through a ScriptRunner- compatible executor (leftover text, pre-escort text, pre-pick offer) plus a T3 driver that talks to the leftover ball in a real game. --- data/scripts/oaks_lab.lua | 19 ++- tests/drivers/oak_last_ball_bug601_test.lua | 112 +++++++++++++++++ tests/engine/oaks_lab_last_ball_bug601.lua | 129 ++++++++++++++++++++ 3 files changed, 255 insertions(+), 5 deletions(-) create mode 100644 tests/drivers/oak_last_ball_bug601_test.lua create mode 100644 tests/engine/oaks_lab_last_ball_bug601.lua diff --git a/data/scripts/oaks_lab.lua b/data/scripts/oaks_lab.lua index 2105ae85..82ebb4bb 100644 --- a/data/scripts/oaks_lab.lua +++ b/data/scripts/oaks_lab.lua @@ -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 diff --git a/tests/drivers/oak_last_ball_bug601_test.lua b/tests/drivers/oak_last_ball_bug601_test.lua new file mode 100644 index 00000000..2c3e6485 --- /dev/null +++ b/tests/drivers/oak_last_ball_bug601_test.lua @@ -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 "" + 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 "" + 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 diff --git a/tests/engine/oaks_lab_last_ball_bug601.lua b/tests/engine/oaks_lab_last_ball_bug601.lua new file mode 100644 index 00000000..9320da0b --- /dev/null +++ b/tests/engine/oaks_lab_last_ball_bug601.lua @@ -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")