local U = require("tests.drivers.util") local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/game3_vs_seeker" -- pokefirered/include/constants/items.h:434 local ITEM_VS_SEEKER = 362 local LASS_MEGAN, LASS_MEGAN_2 = 130, 648 local TWINS_ELI_ANNE, TWINS_ELI_ANNE_2 = 484, 533 local IVYSAUR, WARTORTLE = 2, 8 local CLEFAIRY, JIGGLYPUFF = 35, 39 local FLAG_GOT_VS_SEEKER, FLAG_CELADON, FLAG_FUCHSIA = 0x292, 0x896, 0x897 local FLAG_CHARGING = 0x801 local EMOTE_EXCL, EMOTE_DOUBLE = 0, 6 local failures = 0 local function result(ok, label) print((ok and "PASS " or "FAIL ") .. label) if not ok then failures = failures + 1 end return ok end local function finish() if failures == 0 then print("PASS game3_vs_seeker") love.event.quit(0) else print("FAIL game3_vs_seeker failures=" .. failures) love.event.quit(1) end end local function raise_hand(mt) mt = tonumber(mt) or 0 return mt >= 0x4D and mt <= 0x4F end return function(game) for _ = 1, 600 do if game.phase == "boot" and game.boot then break end U.wait(1) end game:_handleBootAction({ action = "new_game", name = "RED" }) U.wait(180) local Runtime = require("src.core.game3.runtime") local Map = require("src.core.game3.map") local Space = require("src.core.game3.scripting.space") local Flags = require("src.core.game3.scripting.flags") local Party = require("src.core.game3.party") local Bag = require("src.core.game3.bag") local Objects = require("src.core.game3.objects") local Collision = require("src.core.game3.collision") local TrainerSight = require("src.core.game3.trainer_sight") local FieldEffects = require("src.core.game3.field_effects") local ItemUse = require("src.core.game3.item_use") local StepEvents = require("src.core.game3.step_events") local Rng = require("src.core.game3.rng") local VsSeeker = require("src.core.game3.vs_seeker") local Battle = require("src.core.game3.battle") local Message = package.loaded["src.ui.game3.message"] or require("src.ui.game3.message") local session = Runtime.getSession() if not result(session ~= nil, "new game reached the field") then return finish() end session.party = {} Party.giveMon(session, IVYSAUR, 45) Party.giveMon(session, WARTORTLE, 45) session.bag = session.bag or Bag.new() Bag.add(session.bag, ITEM_VS_SEEKER, 1) local function setFlag(id, on) Flags.setFlag(Space.store, nil, id, on ~= false) end local function getFlag(id) return Flags.getFlag(Space.store, nil, id) end setFlag(FLAG_GOT_VS_SEEKER) setFlag(FLAG_CELADON) setFlag(FLAG_FUCHSIA) local okC, MapCatalog = pcall(require, "src.import.gba.map_catalog") local mapId = (okC and MapCatalog.pretToEngine and MapCatalog.pretToEngine("Route8")) or "FR_ROUTE_8" Map.load(nil, game, mapId, { x = 40, y = 6, facing = "up" }) game.session.x, game.session.y, game.session.facing = 40, 6, "up" U.wait(90) session = Runtime.getSession() setFlag(Flags.trainerFlagId(LASS_MEGAN)) setFlag(Flags.trainerFlagId(TWINS_ELI_ANNE)) local function find_trainers(tid) local found = {} for _, lid in ipairs(Objects.listActive()) do local eo = Objects.find(lid) if eo and TrainerSight.getTrainerId(eo) == tid then found[#found + 1] = eo end end table.sort(found, function(a, b) return (a.cellX or 0) < (b.cellX or 0) end) return found[1], found[2] end local function stand_facing(tid) local eo = find_trainers(tid) if not eo then return nil end local tx, ty = eo.cellX, eo.cellY local cands = { { 1, 0, "left" }, { 0, 1, "up" }, { -1, 0, "right" }, { 0, -1, "down" } } for _, c in ipairs(cands) do local x, y = tx + c[1], ty + c[2] if Collision.canEnter(game, x, y, {}) and not Objects.at(x, y) then Map.load(nil, game, mapId, { x = x, y = y, facing = c[3] }) game.session.x, game.session.y, game.session.facing = x, y, c[3] U.wait(45) return find_trainers(tid) end end return nil end local function accept_seed() for seed = 1, 65535 do Rng.SeedRng(seed) if Rng.Random() % 100 >= 30 then return seed end end return 1 end local forcedSeed = nil local origCompute = VsSeeker.computeResponse VsSeeker.computeResponse = function(...) if forcedSeed then Rng.SeedRng(forcedSeed) end return origCompute(...) end local function use_seeker(label, shotName) local ok, kind, text = ItemUse.useField(session, session.bag, ITEM_VS_SEEKER) result(ok and kind == "vs_seeker", label .. ": VS SEEKER accepted on Route 8 (" .. tostring(text) .. ")") local seen = {} local done, response = false, nil forcedSeed = accept_seed() VsSeeker.use(session, game, function(_, resp) done = true response = resp end) local shot = false for i = 1, 600 do for _, a in ipairs(FieldEffects._anims or {}) do if a.kind == "emote" and a.targetObj and a.targetObj.localId then seen[a.targetObj.localId] = a.baseFrame end end if not shot and i == VsSeeker.EFFECT_FRAMES + 20 and shotName then shot = true result(U.shot(game, DIR .. "/" .. shotName), "screenshot " .. shotName) end if done then break end if Message.isOpen and Message.isOpen() then U.tap(game, "a") end U.wait(1) end forcedSeed = nil result(done, label .. ": VS SEEKER sequence finished") return response, seen end local function diag(label) local Hud = package.loaded["src.ui.game3.hud"] local Stack = package.loaded["src.ui.game3.stack"] local Choice = package.loaded["src.ui.game3.choice"] local Field = package.loaded["src.core.game3.field"] local Fade = package.loaded["src.ui.game3.fade"] local BT = package.loaded["src.core.game3.battle_transition"] local ctx = Space.vm and Space.vm.ctx or {} local layers = {} for _, l in ipairs(Stack and Stack._layers or {}) do layers[#layers + 1] = tostring(l.id) end print(string.format("DIAG %s phase=%s rt=%s vm=%s/%s pc=%s:%s msg=%s stay=%s waiting=%s page=%s/%s waitBtn=%s stack=[%s] choice=%s locked=%s running=%s fade=%s bt=%s battle=%s steps=%s", label, tostring(game.phase), tostring(Runtime.isActive and Runtime.isActive()), tostring(ctx.status), tostring(ctx.mode), tostring(ctx.pc and ctx.pc.listKey), tostring(ctx.pc and ctx.pc.index), tostring(Message.isOpen and Message.isOpen()), tostring(Message._stay), tostring(Message._waiting), tostring(Message._page), tostring(Message._pages and #Message._pages), tostring(Hud and Hud._waitButton ~= nil), table.concat(layers, ","), tostring(Choice and Choice.active), tostring(Field and Field.locked), tostring(Field and Field.running), tostring(Fade and Fade.isActive and Fade.isActive()), tostring(BT and BT.isActive and BT.isActive()), tostring(Battle.isActive()), tostring(StepEvents.busy()))) end local function wants_a() local Hud = package.loaded["src.ui.game3.hud"] return (Message.isOpen and Message.isOpen()) or (Hud and Hud._waitButton ~= nil) end local function talk_until_battle() local sawMsg = false local openFor = 0 U.tap(game, "a") for _ = 1, 600 do if Battle.isActive() then break end if wants_a() then sawMsg = true openFor = openFor + 1 if openFor == 100 then diag("waiting on A for 100 taps") end U.tap(game, "a") else openFor = 0 if sawMsg and not (Space.vm and Space.vm:isRunning()) then break end end U.wait(3) end return sawMsg end local function settle() for _ = 1, 600 do local busy = (Space.vm and Space.vm:isRunning()) or wants_a() or Battle.isActive() or StepEvents.busy() if not busy then break end if wants_a() then U.tap(game, "a") end U.wait(3) end U.wait(30) end local s = VsSeeker.state(session) local megan = stand_facing(LASS_MEGAN) if not result(megan ~= nil, "found LASS MEGAN and stood next to her") then return finish() end local meganLid = megan.localId s.steps = 99 local _, uncharged = VsSeeker.use(session, game) result(uncharged == VsSeeker.NOT_CHARGED and s.steps == 99, "99 steps: not charged, no drain") settle() s.steps = 100 local response, seen = use_seeker("single", "00_response.png") megan = Objects.find(meganLid) result(response == VsSeeker.RESPONSE_FOUND_REMATCHES, "response is FOUND_REMATCHES") result(VsSeeker.getRematch(s, meganLid) == 2, "Megan stamped with rematch column 2 (got " .. VsSeeker.getRematch(s, meganLid) .. ")") result(getFlag(FLAG_CHARGING), "FLAG_SYS_VS_SEEKER_CHARGING set") result(s.steps == 0 and s.charging == 0, "battery drained, charging counter reset") result(megan and raise_hand(megan.movementType), "Megan raised her hand (movementType=" .. tostring(megan and megan.movementType) .. ")") result(seen[meganLid] == EMOTE_DOUBLE, "Megan showed !!") local sawExcl = false for lid, base in pairs(seen) do if lid ~= meganLid and base == EMOTE_EXCL then sawExcl = true end end result(sawExcl, "an unfought trainer in range showed !") settle() talk_until_battle() if not result(Battle.isActive(), "talking to Megan started the rematch") then return finish() end local st = Battle._st result(st and st.double ~= true, "Megan rematch is a single battle") result(st and st.trainerId == LASS_MEGAN_2, "tier check: CELADON set picks LASS_MEGAN_2 (got " .. tostring(st and st.trainerId) .. ")") U.wait(30) Battle.abort("win") local sawPost = false for _ = 1, 200 do if Message.isOpen and Message.isOpen() and not Battle.isActive() then sawPost = true end if not (Space.vm and Space.vm:isRunning()) and not Battle.isActive() then break end U.wait(3) end U.wait(60) result(not sawPost and not (Message.isOpen and Message.isOpen()), "rematch script ends without the post-battle msgbox") result(getFlag(Flags.trainerFlagId(LASS_MEGAN_2)), "FLAG for LASS_MEGAN_2 set after the win") result(VsSeeker.getRematch(s, meganLid) == 0, "Megan rematch state cleared") megan = Objects.find(meganLid) result(megan and not raise_hand(megan.movementType), "Megan stopped raising her hand") result(U.shot(game, DIR .. "/01_single_after.png"), "screenshot 01_single_after.png") settle() talk_until_battle() result(not Battle.isActive(), "talking again without a VS SEEKER response gives no battle") settle() local eli, anne = find_trainers(TWINS_ELI_ANNE) if not result(eli ~= nil and anne ~= nil, "found both twins") then return finish() end local eliLid, anneLid = eli.localId, anne.localId local ex, ey = eli.cellX, eli.cellY + 1 Map.load(nil, game, mapId, { x = ex, y = ey, facing = "up" }) game.session.x, game.session.y, game.session.facing = ex, ey, "up" U.wait(45) s.steps = 100 response, seen = use_seeker("double", "02_double_response.png") result(response == VsSeeker.RESPONSE_FOUND_REMATCHES, "twins response is FOUND_REMATCHES") result(VsSeeker.getRematch(s, eliLid) == 3 and VsSeeker.getRematch(s, anneLid) == 3, "both twins stamped with column 3") result(seen[eliLid] == EMOTE_DOUBLE and seen[anneLid] == EMOTE_DOUBLE, "both twins showed !!") settle() talk_until_battle() if not result(Battle.isActive(), "talking to Eli started the double rematch") then return finish() end st = Battle._st result(st and st.double == true, "twins rematch is a double battle") result(st and st.battlersCount == 4, "battlersCount is 4") result(st and st.trainerId == TWINS_ELI_ANNE_2, "foe is TWINS_ELI_ANNE_2 (got " .. tostring(st and st.trainerId) .. ")") local fp = st and st.foeParty or {} result(fp[1] and fp[2] and fp[1].species == CLEFAIRY and fp[2].species == JIGGLYPUFF and fp[1].level == 28 and fp[2].level == 28, "foe party is Clefairy L28 + Jigglypuff L28") U.wait(30) Battle.abort("win") for _ = 1, 200 do if not (Space.vm and Space.vm:isRunning()) and not Battle.isActive() then break end U.wait(3) end U.wait(60) result(getFlag(Flags.trainerFlagId(TWINS_ELI_ANNE_2)), "FLAG for TWINS_ELI_ANNE_2 set after the win") result(VsSeeker.getRematch(s, eliLid) == 0 and VsSeeker.getRematch(s, anneLid) == 0, "both twin rematch states cleared") anne = Objects.find(anneLid) eli = Objects.find(eliLid) result(anne and anne.movementType == 8, "Anne set to FACE_DOWN (got " .. tostring(anne and anne.movementType) .. ")") result(eli and not raise_hand(eli.movementType), "Eli stopped raising her hand") result(U.shot(game, DIR .. "/03_double_after.png"), "screenshot 03_double_after.png") settle() megan = stand_facing(LASS_MEGAN) if not result(megan ~= nil, "back next to Megan") then return finish() end s.steps = 100 response = use_seeker("charge", nil) megan = Objects.find(meganLid) result(VsSeeker.getRematch(s, meganLid) == 4 and megan and raise_hand(megan.movementType), "Megan wants the next tier (column 4) and raises her hand") settle() s.charging = 99 StepEvents.onStepTaken(session, game) for _ = 1, 120 do if not StepEvents.busy() then break end U.wait(1) end megan = Objects.find(meganLid) result(not getFlag(FLAG_CHARGING) and s.charging == 0, "100 charging steps clear the charging flag") result(VsSeeker.getRematch(s, meganLid) == 0, "charging done clears the rematch states") result(megan and megan.movementType >= 7 and megan.movementType <= 10, "raise-hand replaced by a random face type (got " .. tostring(megan and megan.movementType) .. ")") result(U.shot(game, DIR .. "/04_charge_reset.png"), "screenshot 04_charge_reset.png") VsSeeker.computeResponse = origCompute finish() end