From a12952dccbf447d53cf4d6a278efc87e824eb0a6 Mon Sep 17 00:00:00 2001 From: bryanthaboi Date: Fri, 9 Oct 2026 13:13:34 -0400 Subject: [PATCH 1/3] CLOSES #2814 --- src/core/game3/map.lua | 8 +- src/core/game3/time_events.lua | 5 +- src/ui/game3/rs/trainer_card_policy.lua | 20 +++-- .../game3_rs_trainer_card_continue_2814.lua | 78 +++++++++++++++++++ tests/game3_rs_trainer_card_flags_test.lua | 9 +++ tests/game3_time_events_store_test.lua | 43 ++++++++++ 6 files changed, 148 insertions(+), 15 deletions(-) create mode 100644 tests/drivers/game3_rs_trainer_card_continue_2814.lua create mode 100644 tests/game3_time_events_store_test.lua diff --git a/src/core/game3/map.lua b/src/core/game3/map.lua index 05675d02..3489ed14 100644 --- a/src/core/game3/map.lua +++ b/src/core/game3/map.lua @@ -726,15 +726,15 @@ function Map.load(mod, game, mapId, opts) -- pokeemerald/src/overworld.c:801 require("src.core.game3.rse.rematch").tryUpdateRandomTrainerRematchesForMap(session, mapId) end - -- pokeemerald/src/overworld.c:802 - if session and require("src.core.game3.rtc").enabled(session) then - require("src.core.game3.time_events").run(session) - end if opts.keepScript and Space and Space.retarget then Space.retarget(mod or Runtime._mod, mapId, game, world) elseif Space and Space.activate then Space.activate(mod or Runtime._mod, mapId, game, world) end + -- pokeemerald/src/overworld.c:802 + if session and require("src.core.game3.rtc").enabled(session) then + require("src.core.game3.time_events").run(session) + end -- pokefirered/src/overworld.c:805 local savedFlash = session and tonumber(session.flashLevel) diff --git a/src/core/game3/time_events.lua b/src/core/game3/time_events.lua index f764439d..b2366a58 100644 --- a/src/core/game3/time_events.lua +++ b/src/core/game3/time_events.lua @@ -57,13 +57,12 @@ local function storeOf(session, opts) local Runtime = package.loaded["src.core.game3.runtime"] local live = Runtime and Runtime.getSession and Runtime.getSession() or nil if Space and Space.store and (live == nil or live == session) then return Space.store end - if type(session) == "table" then - session.store = session.store or { flags = {}, vars = {} } + if type(session) == "table" and type(session.store) == "table" then session.store.flags = session.store.flags or {} session.store.vars = session.store.vars or {} return session.store end - return nil + return { flags = {}, vars = {} } end local function flags() diff --git a/src/ui/game3/rs/trainer_card_policy.lua b/src/ui/game3/rs/trainer_card_policy.lua index 0598a8f2..e5f0bf93 100644 --- a/src/ui/game3/rs/trainer_card_policy.lua +++ b/src/ui/game3/rs/trainer_card_policy.lua @@ -10,16 +10,20 @@ function P.generate(session, version) local Space = package.loaded["src.core.game3.scripting.space"] local Runtime = package.loaded["src.core.game3.runtime"] local current = Runtime and Runtime.getSession and Runtime.getSession() - local live = s.store == nil and (current == nil or current == s) and Space and Space.getStore and Space.getStore() or nil - local store, stats = s.store or live or s, s.gameStats or {} + local Flags = require("src.core.game3.scripting.flags") + local live = (current == nil or current == s) and Space and Space.getStore and Space.getStore() or nil + local sources = {} + for _, src in ipairs({live or false, s.store or false, s}) do + if src and type(src.flags) == "table" then sources[#sources + 1] = src end + end + local store, stats = sources[1] or s, s.gameStats or {} local tower = s.battleTower or {} local function flag(name) - local id, flags = C:require("flags", name), store.flags or {} - local v = flags[id] - if v == nil then v = flags[tostring(id)] end - if v == nil then v = flags[string.format("0x%X", id)] end - if v == nil then v = flags[name] end - return v == true or v == 1 + local id = C:require("flags", name) + for _, src in ipairs(sources) do + if Flags.getFlag(src, nil, id) then return true end + end + return false end -- save_menu_util.c:118 local _, caught = Policy.counts(s, Dex.nationalEnabled({version = version, dex = s.dex, flags = store.flags, vars = store.vars})) diff --git a/tests/drivers/game3_rs_trainer_card_continue_2814.lua b/tests/drivers/game3_rs_trainer_card_continue_2814.lua new file mode 100644 index 00000000..b7651e4e --- /dev/null +++ b/tests/drivers/game3_rs_trainer_card_continue_2814.lua @@ -0,0 +1,78 @@ +local U = require("tests.drivers.util") + +local MARKER = "drv2814_seeded" +local failures = 0 +local function check(ok, label) + print((ok and "PASS " or "FAIL ") .. "card2814 " .. label) + if not ok then failures = failures + 1 end + return ok +end +local function finish() + print((failures == 0 and "PASS " or "FAIL ") .. "card2814 failures=" .. failures) + love.event.quit(failures == 0 and 0 or 1) +end + +local function cardChecks(session, tag) + local Space = require("src.core.game3.scripting.space") + local Flags = require("src.core.game3.scripting.flags") + local C = require("src.core.game3.constants").of(session.version) + local dex, b1 = C:require("flags", "FLAG_SYS_POKEDEX_GET"), C:require("flags", "FLAG_BADGE01_GET") + check(session.store == nil, tag .. "_no_fabricated_session_store") + check(Flags.getFlag(Space.store, nil, dex) and Flags.getFlag(Space.store, nil, b1) and Flags.getFlag(Space.store, nil, b1 + 1), + tag .. "_live_store_has_dex_and_badges") + if session.version == "ruby" or session.version == "sapphire" then + local card = require("src.ui.game3.rs.trainer_card_policy").generate(session) + check(card.hasPokedex == true, tag .. "_card_hasPokedex") + check(card.badges[1] == true and card.badges[2] == true and card.badges[3] == false, tag .. "_card_badges_1_2") + end +end + +local function bootReady(game) + for _ = 1, 600 do + if game.phase == "boot" and game.boot then break end + U.wait(1) + end + return check(game.phase == "boot" and game.boot ~= nil, "boot_ready") +end + +local function seed(game) + game:_handleBootAction({ action = "new_game", name = "TAI" }) + U.wait(180) + local session = require("src.core.game3.runtime").getSession() + if not check(session ~= nil, "seed_field_session_ready") then return end + local Space = require("src.core.game3.scripting.space") + local Flags = require("src.core.game3.scripting.flags") + local C = require("src.core.game3.constants").of(session.version) + Flags.setFlag(Space.store, nil, C:require("flags", "FLAG_SYS_POKEDEX_GET"), true) + Flags.setFlag(Space.store, nil, C:require("flags", "FLAG_SYS_CLOCK_SET"), true) + for i = 0, 1 do Flags.setFlag(Space.store, nil, C:require("flags", "FLAG_BADGE01_GET") + i, true) end + cardChecks(session, "seed") + if check(game:saveGame() ~= false, "seed_save_written") then + love.filesystem.write(MARKER, "1") + print("card2814 seeded; run again to continue") + end +end + +local function continueAndOpen(game) + game:_handleBootAction({ action = "continue" }) + U.wait(180) + local session = require("src.core.game3.runtime").getSession() + if not check(session ~= nil, "continue_field_session_ready") then return end + cardChecks(session, "continue") + local Card = require("src.ui.game3.screens").get("trainer_card", session) + Card.show({ session = session }) + U.wait(120) + local shots = os.getenv("POKEPORT_SHOT_DIR") + if shots then U.still(game, shots .. "/2814_card_front_" .. tostring(session.version) .. ".png") end + love.filesystem.remove(MARKER) +end + +return function(game) + if not bootReady(game) then return finish() end + if love.filesystem.getInfo(MARKER) then + continueAndOpen(game) + else + seed(game) + end + finish() +end diff --git a/tests/game3_rs_trainer_card_flags_test.lua b/tests/game3_rs_trainer_card_flags_test.lua index 70fce494..a856283f 100644 --- a/tests/game3_rs_trainer_card_flags_test.lua +++ b/tests/game3_rs_trainer_card_flags_test.lua @@ -51,7 +51,16 @@ for _, version in ipairs({ "ruby", "sapphire" }) do card = Policy.generate(stale) check(card.hasPokedex == true, version .. " live script store: pokedex row shown") check(owned(card) == "1,2,3,4,5,6,7,8", version .. " live script store: all badges (" .. owned(card) .. ")") + + local withEmpty = { version = version, name = "MAY", flags = saved.flags, vars = saved.vars, store = { flags = {}, vars = {} } } + card = Policy.generate(withEmpty) + check(card.hasPokedex == true, version .. " empty session.store + live store: pokedex row shown") + check(owned(card) == "1,2,3,4,5,6,7,8", version .. " empty session.store + live store: live badges (" .. owned(card) .. ")") package.loaded["src.core.game3.scripting.space"] = nil + + card = Policy.generate(withEmpty) + check(card.hasPokedex == true, version .. " empty session.store, no live store: session.flags pokedex") + check(owned(card) == "1,4", version .. " empty session.store, no live store: session.flags badges 1,4 (" .. owned(card) .. ")") end if failed > 0 then diff --git a/tests/game3_time_events_store_test.lua b/tests/game3_time_events_store_test.lua new file mode 100644 index 00000000..d753b12f --- /dev/null +++ b/tests/game3_time_events_store_test.lua @@ -0,0 +1,43 @@ +#!/usr/bin/env luajit + +package.path = "./?.lua;./?/init.lua;" .. package.path + +local failed = 0 +local function check(cond, msg) + if cond then + print("[ok] " .. msg) + else + failed = failed + 1 + print("[FAIL] " .. msg) + end +end + +package.loaded["src.core.game3.scripting.space"] = { store = nil, getStore = function() return nil end } +local Flags = require("src.core.game3.scripting.flags") +local TimeEvents = require("src.core.game3.time_events") + +for _, version in ipairs({ "ruby", "sapphire", "emerald" }) do + local session = { version = version, name = "MAY", flags = {}, vars = {} } + TimeEvents.run(session) + check(session.store == nil, version .. " run without live store leaves session.store nil") + TimeEvents.init(session) + check(session.store == nil, version .. " init without live store leaves session.store nil") + + local live = Flags.newStore() + package.loaded["src.core.game3.scripting.space"] = { store = live, getStore = function() return live end } + local s2 = { version = version, name = "MAY", flags = {}, vars = {} } + TimeEvents.run(s2) + check(s2.store == nil, version .. " run with live store leaves session.store nil") + package.loaded["src.core.game3.scripting.space"] = { store = nil, getStore = function() return nil end } + + local own = { flags = {}, vars = {} } + local s3 = { version = version, name = "MAY", flags = {}, vars = {}, store = own } + TimeEvents.run(s3) + check(s3.store == own, version .. " existing session.store is kept") +end + +if failed > 0 then + print(string.format("%d failure(s)", failed)) + os.exit(1) +end +print("all ok") From 4db70c576495493094dc99d94e6432a0bfe05c81 Mon Sep 17 00:00:00 2001 From: bryanthaboi Date: Fri, 9 Oct 2026 13:36:14 -0400 Subject: [PATCH 2/3] CLOSES #2813, CLOSES #2815 --- data/scripts/oaks_lab_yellow.lua | 15 +- src/core/game3/dive.lua | 2 +- src/core/game3/field.lua | 1 + src/core/game3/field_effects.lua | 71 +++++++-- src/core/game3/player.lua | 19 ++- src/core/game3/surf_bob.lua | 96 ++++++++++++ tests/drivers/em_surf_bob_dive_2813.lua | 143 +++++++++++++++++ .../yellow_lab_rival_exit_bug2815_test.lua | 145 ++++++++++++++++++ .../oaks_lab_rival_paths_bug1989_bug1987.lua | 5 + tests/game3_surf_bob_dive_test.lua | 121 +++++++++++++++ 10 files changed, 596 insertions(+), 22 deletions(-) create mode 100644 src/core/game3/surf_bob.lua create mode 100644 tests/drivers/em_surf_bob_dive_2813.lua create mode 100644 tests/drivers/yellow_lab_rival_exit_bug2815_test.lua create mode 100644 tests/game3_surf_bob_dive_test.lua diff --git a/data/scripts/oaks_lab_yellow.lua b/data/scripts/oaks_lab_yellow.lua index 0d04923f..bf57692e 100644 --- a/data/scripts/oaks_lab_yellow.lua +++ b/data/scripts/oaks_lab_yellow.lua @@ -11,8 +11,12 @@ -- a lab loss (OaksLabRivalEndBattleScript), and Route 22's first battle -- upgrades FLAREON back to JOLTEON (Route22Rival1AfterBattleScript). +local Sound = require("src.core.Sound") + local OAK1 = 3 local RIVAL = 1 +-- pokeyellow engine/overworld/movement.asm:871 +local FAST = { stepFrames = 16 } -- engine/overworld/pathfinding.asm:36-70 (FindPathToPlayer): step whichever local function findPathRows(rows, objIndex, sx, sy, tx, ty) @@ -338,17 +342,20 @@ return { table.insert(rows, { "move_npc", RIVAL, side, 1 }) table.insert(rows, { "move_npc", RIVAL, "down", 1 }) table.insert(rows, { "face_player_dir", side }) - table.insert(rows, { "move_npc", RIVAL, "down", 1 }) + -- pokeyellow scripts/OaksLab.asm:431 + table.insert(rows, { "walk_npc", RIVAL, { "down" }, FAST }) table.insert(rows, { "face_player_dir", "down" }) - table.insert(rows, { "move_npc", RIVAL, "down", 4 }) + table.insert(rows, { "walk_npc", RIVAL, { "down", "down", "down", "down" }, FAST }) table.insert(rows, { "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" }) table.insert(rows, { "play_music", "Music_OaksLab" }) -- OaksLabPikachuEscapesPokeballScript: the follower reaches the map (#1009) table.insert(rows, { "face_player_dir", "up" }) table.insert(rows, { "set_field", "pikachuInBall", false }) + -- pokeyellow scripts/OaksLab.asm:1094 + table.insert(rows, { "show_text", "_OaksLabPikachuDislikesPokeballsText1", false, + { preSound = function() return Sound.playPikaCry(game.data, 2) end } }) + -- pokeyellow scripts/OaksLab.asm:478 table.insert(rows, { "spawn_pikachu_follower" }) - table.insert(rows, { "play_cry", "PIKACHU" }) - table.insert(rows, { "show_text", "_OaksLabPikachuDislikesPokeballsText1" }) table.insert(rows, { "show_text", "_OaksLabPikachuDislikesPokeballsText2" }) ow.runner:run(rows, { npc = rival }) return true diff --git a/src/core/game3/dive.lua b/src/core/game3/dive.lua index 5b85d567..4b34ad76 100644 --- a/src/core/game3/dive.lua +++ b/src/core/game3/dive.lua @@ -156,7 +156,7 @@ function Dive.useDive(slot, mon) Field.lock() t.state = 1 -- pokeemerald/src/field_effect.c:1924 - lazyReq("src.core.game3.field_move_show_mon").start(t.mon, { pose = true }, function() t.state = 2 end) + lazyReq("src.core.game3.field_move_show_mon").start(t.mon, { pose = false }, function() t.state = 2 end) return false elseif t.state == 1 then Field.lock() diff --git a/src/core/game3/field.lua b/src/core/game3/field.lua index e80598e5..14f79519 100644 --- a/src/core/game3/field.lua +++ b/src/core/game3/field.lua @@ -234,6 +234,7 @@ function Field.update(_dt) if NativesEvents and NativesEvents.pollWalkaway then NativesEvents.pollWalkaway(Space and Space.vm, input) end + lazyReq("src.core.game3.field_effects").stepSurfBob() Player.update(game, walkInput) lazyReq("src.world.game3.Follower").update(game) Field.updateWaterfall(game) diff --git a/src/core/game3/field_effects.lua b/src/core/game3/field_effects.lua index 2fa534ca..49ffee61 100644 --- a/src/core/game3/field_effects.lua +++ b/src/core/game3/field_effects.lua @@ -18,7 +18,8 @@ FieldEffects._sheets = {} -- [name] = { image = ..., quads = ..., fw = ..., fh = FieldEffects._fx = nil -- tall grass FieldEffects._anims = {} -- transient active field animations FieldEffects._ground = nil -- pokefirered/src/event_object_movement.c:8721 -FieldEffects._surfClock = 0 +FieldEffects._surfBob = nil +FieldEffects._underwaterBob = nil FieldEffects._logged = false -- pokefirered/src/scrcmd.c:2051 — gFieldEffectArguments, written by -- setfieldeffectargument and read by the effect that dofieldeffect starts. @@ -404,7 +405,8 @@ function FieldEffects.install(cache) FieldEffects._fx = nil FieldEffects._anims = {} FieldEffects._ground = nil - FieldEffects._surfClock = 0 + FieldEffects._surfBob = nil + FieldEffects._underwaterBob = nil FieldEffects._logged = false local FieldView = modFieldView() if FieldView and FieldView.setCameraPanning then @@ -1643,6 +1645,56 @@ function FieldEffects.groundEffects() end end +-- pokeemerald/src/field_effect_helpers.c:1052 UpdateSurfBlobFieldEffect +function FieldEffects.stepSurfBob() + local SurfBob = lazyReq("src.core.game3.surf_bob") + local P = package.loaded["src.core.game3.player"] + if P and P.underwater then + FieldEffects._surfBob = nil + -- pokeemerald/src/field_player_avatar.c:893 + FieldEffects._underwaterBob = FieldEffects._underwaterBob or SurfBob.newUnderwater() + SurfBob.tickUnderwater(FieldEffects._underwaterBob) + return + end + FieldEffects._underwaterBob = nil + if not (P and (P.surfing or P.surfHopping or P.dismounting)) then + FieldEffects._surfBob = nil + return + end + local b = FieldEffects._surfBob + if not b then + b = SurfBob.newBlob(rse() and "rse" or "frlg") + FieldEffects._surfBob = b + end + SurfBob.syncAnim(b, P.facing or "down") + local x, y = P.cellX, P.cellY + if P.moving then x, y = P.targetX or x, P.targetY or y end + local Collision = package.loaded["src.core.game3.collision"] + SurfBob.syncPosition(b, x, y, Collision and Collision.elevationAt) + local state = SurfBob.BOB_PLAYER_AND_MON + if P.surfHopping then + state = SurfBob.BOB_NONE + elseif P.dismounting then + -- pokeemerald/src/field_player_avatar.c:1671 + state = SurfBob.BOB_JUST_MON + end + SurfBob.tick(b, state) +end + +function FieldEffects.surfBlobY2() + local b = FieldEffects._surfBob + return b and b.y2 or 0 +end + +function FieldEffects.surfPlayerY2() + local P = package.loaded["src.core.game3.player"] + if P and P.underwater then + local u = FieldEffects._underwaterBob + return u and u.y2 or 0 + end + return lazyReq("src.core.game3.surf_bob").playerY2(FieldEffects._surfBob) +end + -- ---------------------------------------------------------------- Step & Update function FieldEffects.step() FieldEffects.groundEffects() @@ -1679,9 +1731,6 @@ function FieldEffects.step() end end - -- Surfing blob clock (48 ticks per frame * 2 frames = 96 ticks per loop) - FieldEffects._surfClock = (FieldEffects._surfClock + 1) % 96 - -- Update active transient animations local active = {} for _, anim in ipairs(FieldEffects._anims) do @@ -1949,7 +1998,9 @@ function FieldEffects.drawBehind(camX, camY) local surfSheet = load_sheet("surf_blob", 32, 32, 6) if surfSheet then local facing = P.facing or "down" - local step = math.floor(FieldEffects._surfClock / 48) % 2 + local b = FieldEffects._surfBob + local step = b and lazyReq("src.core.game3.surf_bob").animCmdIndex(b) or 0 + local bob = b and b.y2 or 0 local frameIdx = 0 local flip = false @@ -1975,15 +2026,13 @@ function FieldEffects.drawBehind(camX, camY) if q then local sx, sy if P.surfHopping then - -- Player is hopping onto water: blob is in position on the destination tile sx = (P.targetX or P.cellX) * CELL - camX - 8 - sy = (P.targetY or P.cellY) * CELL - camY - 8 + sy = (P.targetY or P.cellY) * CELL - camY - 8 + bob elseif P.dismounting then - -- Player is hopping off water to land: blob remains at origin tile sx = P.cellX * CELL - camX - 8 - sy = P.cellY * CELL - camY - 8 + sy = P.cellY * CELL - camY - 8 + bob else - local bob = (not P.moving and not P.jumping) and ((step == 1) and -1 or 0) or 0 + -- pokeemerald/src/field_effect_helpers.c:1131 sx = P.px - camX - 8 sy = P.py - camY - 8 + bob end diff --git a/src/core/game3/player.lua b/src/core/game3/player.lua index d6f33309..efa5e6ff 100644 --- a/src/core/game3/player.lua +++ b/src/core/game3/player.lua @@ -318,6 +318,12 @@ local function walkInPlaceFrames() return Player.walkInPlaceFast and RUN_FRAMES or WALK_FRAMES end +local function surfBobY() + if not (Player.surfing or Player.underwater) then return nil end + local FieldEffects = package.loaded["src.core.game3.field_effects"] + return FieldEffects and FieldEffects.surfPlayerY2 and FieldEffects.surfPlayerY2() or 0 +end + -- pokefirered/src/field_effect.c:1215 FallWarpEffect_4 local function warp_owns_sprite() local Warp = package.loaded["src.core.game3.warp"] @@ -398,7 +404,7 @@ local function beginStep(tx, ty, run, ledge) Player.targetY = ty Player.running = (not ledge) and run and true or false Player.jumping = ledge and true or false - Player.spriteYOffset = 0 + Player.spriteYOffset = (not ledge) and surfBobY() or 0 if Player.surfHopping or Player.dismounting then -- pokeemerald/src/event_object_movement.c:8495 DoJumpSpecialSpriteMovement Player.stepFrames = JUMP_FRAMES @@ -1205,10 +1211,9 @@ function Player.tick(game) end end if Player.action then return tickAction() end - if Player.surfing and not Player.jumping then - local okFx, FieldEffects = pcall(lazyReq, "src.core.game3.field_effects") - local clock = (okFx and FieldEffects and FieldEffects._surfClock) or 0 - Player.spriteYOffset = (math.floor(clock / 48) % 2 == 1) and -1 or 0 + local bobY = (not Player.jumping) and surfBobY() or nil + if bobY then + Player.spriteYOffset = bobY elseif not Player.walkInPlace and not warp_owns_sprite() then Player.spriteYOffset = 0 end @@ -1227,7 +1232,7 @@ function Player.tick(game) if Player.jumping then Player.spriteYOffset = Player.jumpSpriteY() else - Player.spriteYOffset = 0 + Player.spriteYOffset = surfBobY() or 0 end -- pokefirered/src/data/object_events/object_event_anims.h:556 if Player.spinning then @@ -1237,6 +1242,8 @@ function Player.tick(game) end if Player.progress >= frames then finishStep(game) + local bobY = (not Player.moving) and surfBobY() or nil + if bobY then Player.spriteYOffset = bobY end return true end return false diff --git a/src/core/game3/surf_bob.lua b/src/core/game3/surf_bob.lua new file mode 100644 index 00000000..254efbd7 --- /dev/null +++ b/src/core/game3/surf_bob.lua @@ -0,0 +1,96 @@ +-- pokeemerald/src/field_effect_helpers.c:1052 UpdateSurfBlobFieldEffect + +local SurfBob = {} + +-- pokeemerald/include/field_effect_helpers.h:7 +SurfBob.BOB_NONE = 0 +SurfBob.BOB_PLAYER_AND_MON = 1 +SurfBob.BOB_JUST_MON = 2 + +SurfBob.FAMILY = { + -- pokeemerald/src/field_effect_helpers.c:1111 + rse = { velocity = -1, intervals = { 3, 7 }, flipMask = 15, animBump = false }, + -- pokefirered/src/field_effect_helpers.c:1049 + frlg = { velocity = 0, intervals = { 7, 15 }, flipMask = 31, animBump = true }, +} + +-- pokeemerald/include/global.fieldmap.h:18 +local ELEVATION_DEFAULT = 3 + +-- pokefirered/src/data/field_effects/field_effect_objects.h:184 +SurfBob.FRLG_FRAME_DURATION = 48 + +local DIRS = { { 0, 1 }, { 0, -1 }, { -1, 0 }, { 1, 0 } } + +-- pokeemerald/src/field_effect_helpers.c:999 FldEff_SurfBlob +function SurfBob.newBlob(family) + local p = SurfBob.FAMILY[family] or SurfBob.FAMILY.frlg + return { + p = p, velocity = p.velocity, timer = 0, y2 = 0, intervalIdx = 1, + prevX = -1, prevY = -1, dir = nil, animClock = 0, state = SurfBob.BOB_NONE, + } +end + +-- pokefirered/src/field_effect_helpers.c:1009 SynchroniseSurfAnim +function SurfBob.syncAnim(b, dir) + if b.dir ~= dir then + b.dir = dir + b.animClock = 0 + else + b.animClock = b.animClock + 1 + end +end + +function SurfBob.animCmdIndex(b) + return math.floor(b.animClock / SurfBob.FRLG_FRAME_DURATION) % 2 +end + +-- pokeemerald/src/field_effect_helpers.c:1081 SynchronizeSurfPosition +function SurfBob.syncPosition(b, x, y, elevAt) + if b.y2 ~= 0 or (x == b.prevX and y == b.prevY) then return end + b.intervalIdx = 1 + b.prevX, b.prevY = x, y + if not elevAt then return end + for _, d in ipairs(DIRS) do + if elevAt(x + d[1], y + d[2]) == ELEVATION_DEFAULT then + b.intervalIdx = 2 + return + end + end +end + +-- pokeemerald/src/field_effect_helpers.c:1107 UpdateBobbingEffect +function SurfBob.tick(b, state) + b.state = state + if state == SurfBob.BOB_NONE then return end + b.timer = (b.timer + 1) % 65536 + if b.timer % (b.p.intervals[b.intervalIdx] + 1) == 0 then + b.y2 = b.y2 + b.velocity + end + if b.timer % (b.p.flipMask + 1) == 0 then + b.velocity = -b.velocity + end +end + +-- pokefirered/src/field_effect_helpers.c:1061 +function SurfBob.playerY2(b) + if not b or b.state ~= SurfBob.BOB_PLAYER_AND_MON then return 0 end + local y2 = b.y2 + if b.p.animBump and SurfBob.animCmdIndex(b) ~= 0 then y2 = y2 + 1 end + return y2 +end + +-- pokeemerald/src/field_effect_helpers.c:1150 StartUnderwaterSurfBlobBobbing +function SurfBob.newUnderwater() + return { bobY = 1, timer = 0, y2 = 0 } +end + +-- pokeemerald/src/field_effect_helpers.c:1164 SpriteCB_UnderwaterSurfBlob +function SurfBob.tickUnderwater(u) + local t = u.timer + u.timer = (t + 1) % 65536 + if t % 4 == 0 then u.y2 = u.y2 + u.bobY end + if u.timer % 16 == 0 then u.bobY = -u.bobY end +end + +return SurfBob diff --git a/tests/drivers/em_surf_bob_dive_2813.lua b/tests/drivers/em_surf_bob_dive_2813.lua new file mode 100644 index 00000000..74ca5c3b --- /dev/null +++ b/tests/drivers/em_surf_bob_dive_2813.lua @@ -0,0 +1,143 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/em_surf_bob_dive_2813" + +local failures = 0 +local function check(ok, label) + print((ok and "PASS " or "FAIL ") .. label) + if not ok then failures = failures + 1 end + return ok +end + +local function finish() + print((failures == 0 and "PASS" or "FAIL") .. " em_surf_bob_dive_2813 failures=" .. failures) + love.event.quit(failures == 0 and 0 or 1) +end + +return function(game) + for _ = 1, 900 do + if game.phase == "boot" and game.boot then break end + U.wait(1) + end + if not check(game.boot ~= nil, "boot reached") then return finish() end + game:_handleBootAction({ action = "new_game", name = "BRENDAN", gender = 0 }) + U.wait(60) + + local Runtime = require("src.core.game3.runtime") + local Map = require("src.core.game3.map") + local Player = require("src.core.game3.player") + local Warp = require("src.core.game3.warp") + local Message = require("src.ui.game3.message") + local Choice = require("src.ui.game3.choice") + local Party = require("src.core.game3.party") + local Space = require("src.core.game3.scripting.space") + local Flags = require("src.core.game3.scripting.flags") + local ShowMon = require("src.core.game3.field_move_show_mon") + local FieldEffects = require("src.core.game3.field_effects") + local C = require("src.core.game3.constants").of("emerald") + local session = Runtime.getSession() + if not check(session ~= nil, "field session exists") then return finish() end + local IDS = Flags.forVersion("emerald").IDS + + session.party = {} + Party.giveMon(session, C.species.byName.SPECIES_WAILMER, 40, "WAILMER") + local m = session.party[1] + m.moves = { C.moves.byName.MOVE_DIVE, C.moves.byName.MOVE_SURF, C.moves.byName.MOVE_WATER_GUN, C.moves.byName.MOVE_REST } + m.pp = { 10, 15, 25, 10 } + Flags.setFlag(Space.store, nil, IDS.FLAG_BADGE05_GET, true) + Flags.setFlag(Space.store, nil, IDS.FLAG_BADGE07_GET, true) + + local function mapNow() local s = Runtime.getSession(); return s and s.map end + + local function place(mapId, x, y, facing, surfing) + Map.load(nil, game, mapId, { x = x, y = y, facing = facing }) + local s = Runtime.getSession() + s.x, s.y, s.facing = x, y, facing + Player.surfing = surfing == true + require("src.core.game3.field").unlock() + U.wait(20) + end + + local function sample(n, shots) + local ys, blobs, lo, hi, seenAt = {}, {}, 99, -99, {} + for i = 1, n do + U.wait(1) + local y = Player.spriteYOffset or 0 + ys[i], blobs[i] = y, FieldEffects.surfBlobY2() + lo, hi = math.min(lo, y), math.max(hi, y) + if shots and shots[y] and not seenAt[y] then + seenAt[y] = true + U.still(game, DIR .. "/" .. shots[y]) + end + end + return ys, blobs, lo, hi + end + + place("EM_ROUTE124", 10, 3, "down", true) + U.wait(40) + local ys, blobs, lo, hi = sample(64, { [0] = "2813_01_surf_bob_rest.png", [-4] = "2813_02_surf_bob_peak.png" }) + print("[driver] surf y2: " .. table.concat(ys, ",")) + check(lo == -4 and hi == 0, string.format("surfing bob spans 0..-4 (got %d..%d)", hi, lo)) + local synced = true + for i = 1, #ys do if ys[i] ~= blobs[i] then synced = false end end + check(synced, "player sprite and surf blob share y2 every frame") + local runs, last, len = {}, nil, 0 + for i = 1, #ys do + if ys[i] == last then len = len + 1 else if last then runs[#runs + 1] = len end last, len = ys[i], 1 end + end + local steady = #runs >= 4 + for i = 2, #runs do if runs[i] ~= 4 then steady = false end end + check(steady, "bob steps one pixel every 4 frames") + + local movingYs = {} + for _ = 1, 40 do + table.insert(game.input.pressQueue, "left") + game.input.state.left = true + U.wait(1) + if Player.moving then movingYs[#movingYs + 1] = Player.spriteYOffset or 0 end + end + game.input.state.left = false + local movedBob, smooth = false, true + for i, y in ipairs(movingYs) do + if y ~= 0 then movedBob = true end + if i > 1 and math.abs(y - movingYs[i - 1]) > 1 then smooth = false end + end + check(#movingYs > 0 and movedBob, "bob keeps running while swimming (" .. table.concat(movingYs, ",") .. ")") + check(smooth, "bob never snaps back to 0 between swim steps") + for _ = 1, 40 do if not Player.moving then break end U.wait(1) end + + place("EM_ROUTE124", 10, 3, "down", true) + U.wait(10) + U.tap(game, "a") + local posed, cutin, shotCut = false, false, false + for _ = 1, 1500 do + if mapNow() == "EM_UNDERWATER_ROUTE124" and not Warp.isBusy() then break end + if (Player.fieldMoveAnim or 0) > 0 then posed = true end + if ShowMon.isActive() then + if not cutin then U.still(game, DIR .. "/2813_03_dive_start_no_pose.png") end + cutin = true + if not shotCut and ShowMon._fx and ShowMon._fx.sprite and ShowMon._fx.sprite.state == "wait" then + shotCut = true + U.still(game, DIR .. "/2813_04_dive_cutin.png") + end + end + if Choice.isOpen and Choice.isOpen() then + U.tap(game, "a") + elseif Message.isOpen and Message.isOpen() then + U.tap(game, "a") + U.wait(3) + else + U.wait(1) + end + end + check(cutin, "Dive shows the field move mon") + check(not posed, "Dive never puts the player in the field move pose") + check(mapNow() == "EM_UNDERWATER_ROUTE124", "dive warps underwater (" .. tostring(mapNow()) .. ")") + U.wait(30) + check(Player.underwater == true, "avatar is underwater") + + ys, _, lo, hi = sample(64, { [0] = "2813_05_underwater_bob_rest.png", [4] = "2813_06_underwater_bob_low.png" }) + print("[driver] underwater y2: " .. table.concat(ys, ",")) + check(lo == 0 and hi == 4, string.format("underwater bob spans 0..+4 (got %d..%d)", lo, hi)) + + finish() +end diff --git a/tests/drivers/yellow_lab_rival_exit_bug2815_test.lua b/tests/drivers/yellow_lab_rival_exit_bug2815_test.lua new file mode 100644 index 00000000..d75234d7 --- /dev/null +++ b/tests/drivers/yellow_lab_rival_exit_bug2815_test.lua @@ -0,0 +1,145 @@ +-- pokeyellow scripts/OaksLab.asm:403-516 +return function(game) + local U = dofile("tests/drivers/util.lua") + local GameVersion = require("src.core.GameVersion") + local Commands = require("src.script.Commands") + local PF = require("src.world.PikachuFollower") + local Pokemon = require("src.pokemon.Pokemon") + local Sound = require("src.core.Sound") + local TextBox = require("src.render.TextBox") + + local SHOT_DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/shots" + local LAB = "OAKS_LAB" + local RIVAL = 1 + local failed = false + + local function check(label, ok) + U.log(ok and "PASS" or "FAIL", label) + if not ok then failed = true end + return ok + end + local function finish() + love.event.quit(failed and 1 or 0) + while true do coroutine.yield() end + end + local function ow() return game.overworld end + local function ctx() + return { game = game, save = game.save, overworld = game.overworld } + end + + if not check("running the Yellow cache", GameVersion.isYellow()) then finish() end + + local boxText = setmetatable({}, { __mode = "k" }) + local newBox = TextBox.new + TextBox.new = function(g, text, onDone, opts) + local box = newBox(g, text, onDone, opts) + boxText[box] = type(text) == "string" and text or "" + return box + end + local pikaCries = {} + local playPika = Sound.playPikaCry + Sound.playPikaCry = function(data, n) + pikaCries[#pikaCries + 1] = n + return playPika(data, n) + end + local plainCries = {} + local playCry = Sound.playCry + Sound.playCry = function(data, species, ...) + plainCries[#plainCries + 1] = species + return playCry(data, species, ...) + end + + local flags = game.save.flags or {} + game.save.flags = flags + flags.EVENT_GOT_STARTER = true + flags.EVENT_CHOSE_PIKACHU = true + flags.EVENT_FOLLOWED_OAK_INTO_LAB = true + flags.EVENT_FOLLOWED_OAK_INTO_LAB_2 = true + flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB = nil + flags.EVENT_OAK_ASKED_TO_CHOOSE_MON = true + game.save.party = { Pokemon.new(game.data, "PIKACHU", 40) } + game.save.party[1].moves = { { id = "THUNDERBOLT", pp = 15 } } + game.save.onBike = false + game.save.pikachuInBall = true + + U.teleport(game, LAB, 5, 5, "down") + U.wait(10) + Commands.show_object(ctx(), LAB, "OAKSLAB_OAK1") + Commands.show_object(ctx(), LAB, "OAKSLAB_RIVAL") + U.wait(5) + U.hold(game, "down", 20) + game.input.state.down = false + + local whatBox, rivalSteps, lastCell, lastStep = nil, {}, nil, nil + local exiting = false + for _ = 1, 6000 do + local top = game.stack:top() + local text = boxText[top] + if text and text:find("What%?") and not text:find("look") then + whatBox = top + break + end + local o = game.overworld + if top ~= o then + if text and text:find("Smell") and not exiting then + exiting = true + plainCries, pikaCries = {}, {} + end + U.tap(game, "a") + U.wait(2) + else + local rival = o:npcByIndex(RIVAL) + if exiting and rival then + local cell = rival.cellX .. "," .. rival.cellY + if cell ~= lastCell then + if lastCell then rivalSteps[#rivalSteps + 1] = lastStep end + lastCell = cell + end + if rival.moving then lastStep = rival.stepFrames or 32 end + end + U.wait(1) + end + end + + U.log("rival exit step lengths:", table.concat(rivalSteps, " ")) + check("rival walked side, down, then the $04 steps", #rivalSteps >= 6) + local fastTail = #rivalSteps >= 6 + for i = 3, #rivalSteps do + if not rivalSteps[i] or rivalSteps[i] > 16 then fastTail = false end + end + check("the last five rival steps run at double speed", fastTail) + check("the first two rival steps are normal speed", + rivalSteps[1] and rivalSteps[1] > 16 and rivalSteps[2] and rivalSteps[2] > 16) + + if not check("the OAK: What? box came up", whatBox ~= nil) then finish() end + check("no follower on the map while What? is typing", PF.current(ow()) == nil) + + U.wait(240) + check("What? is still on screen 240 frames later with no input", + game.stack:top() == whatBox) + check("still no follower before the box is closed", PF.current(ow()) == nil) + check("PikachuCry2 played for the What? line", + #pikaCries == 1 and pikaCries[1] == 2) + local defaultPika = false + for _, s in ipairs(plainCries) do + if s == "PIKACHU" then defaultPika = true end + end + check("no default PIKACHU cry played in the escape scene", not defaultPika) + U.still(game, SHOT_DIR .. "/2815_01_what_waits_no_pikachu.png") + + U.tap(game, "a") + local text2 + for _ = 1, 300 do + local top = game.stack:top() + if top ~= whatBox and boxText[top] and boxText[top]:find("look") then + text2 = top + break + end + U.wait(1) + end + check("Would you look at that! follows the A press", text2 ~= nil) + check("the follower is on the map once What? closes", PF.current(ow()) ~= nil) + U.wait(30) + U.still(game, SHOT_DIR .. "/2815_02_look_at_that_pikachu_out.png") + finish() +end diff --git a/tests/engine/oaks_lab_rival_paths_bug1989_bug1987.lua b/tests/engine/oaks_lab_rival_paths_bug1989_bug1987.lua index 06ce46db..e4f76bbd 100644 --- a/tests/engine/oaks_lab_rival_paths_bug1989_bug1987.lua +++ b/tests/engine/oaks_lab_rival_paths_bug1989_bug1987.lua @@ -78,6 +78,11 @@ local function checkExit(rows, from, side, totalDowns, turnAfterDowns) while rows[j] and rows[j][1] ~= "hide_object" do local r = rows[j] if isMove(r, "down") then downs = downs + r[4] end + if r[1] == "walk_npc" and r[2] == 1 then + for _, d in ipairs(r[3]) do + if d == "down" then downs = downs + 1 end + end + end if r[1] == "face_player_dir" and r[2] == side then sideTurnAt = downs end if r[1] == "face_player_dir" and r[2] == "down" then downTurnAt = downs end j = j + 1 diff --git a/tests/game3_surf_bob_dive_test.lua b/tests/game3_surf_bob_dive_test.lua new file mode 100644 index 00000000..cc3cbce9 --- /dev/null +++ b/tests/game3_surf_bob_dive_test.lua @@ -0,0 +1,121 @@ +package.path = "./?.lua;./?/init.lua;" .. package.path + +local T = require("tests.harness") +local check, eq = T.check, T.eq + +local GameVersion = require("src.core.GameVersion") +GameVersion.set("emerald") + +local function seq(t) + local out = {} + for _, run in ipairs(t) do + for _ = 1, run[2] do out[#out + 1] = run[1] end + end + return out +end + +local function same(got, want, label) + local ok = #got >= #want + for i = 1, #want do + if got[i] ~= want[i] then ok = false break end + end + check(ok, label .. " (got " .. table.concat(got, ",", 1, math.min(#got, #want)) .. ")") +end + +do + local Show = { calls = {} } + local tasks = {} + package.loaded["src.core.game3.field"] = { + lock = function() end, unlock = function() end, holdInput = function() end, + } + package.loaded["src.core.game3.task"] = { spawn = function(fn) tasks[#tasks + 1] = fn end } + package.loaded["src.core.game3.field_move_show_mon"] = { + start = function(mon, opts, cb) Show.calls[#Show.calls + 1] = { mon = mon, opts = opts, cb = cb } end, + } + local Dive = require("src.core.game3.dive") + Dive.useDive(0, { species = 320 }) + check(#tasks == 1, "FldEff_UseDive spawns its task") + tasks[1]({ frames = 0 }) + eq(#Show.calls, 1, "Dive starts the show-mon cut-in") + local opts = Show.calls[1] and Show.calls[1].opts or {} + check(opts.pose ~= true, "Dive skips the player field-move pose (field_effect.c:1924)") + Dive.reset() + package.loaded["src.core.game3.field"] = nil + package.loaded["src.core.game3.task"] = nil + package.loaded["src.core.game3.field_move_show_mon"] = nil + package.loaded["src.core.game3.dive"] = nil +end + +local FE = require("src.core.game3.field_effects") +local P = require("src.core.game3.player") +local function blobY2() return FE.surfBlobY2 and FE.surfBlobY2() end + +local function resetPlayer() + P.surfing, P.underwater, P.surfHopping, P.dismounting = false, false, false, false + P.moving, P.jumping, P.action, P.walkInPlace = false, false, nil, false + P.cellX, P.cellY, P.px, P.py, P.facing = 5, 5, 80, 80, "down" + P.spriteYOffset = 0 + FE._surfBob, FE._underwaterBob = nil, nil +end + +local function run(n, each) + local ys = {} + for i = 1, n do + if each then each(i) end + ;(FE.stepSurfBob or FE.step)() + P.tick(nil) + ys[i] = P.spriteYOffset + end + return ys +end + +local RSE_SURF = seq({ { 0, 3 }, { -1, 4 }, { -2, 4 }, { -3, 4 }, { -4, 4 }, { -3, 4 }, { -2, 4 }, { -1, 4 }, { 0, 4 }, { -1, 4 } }) + +FE._manifest = { family = "rse" } +resetPlayer() +P.surfing = true +same(run(#RSE_SURF), RSE_SURF, "RSE surf: player bobs 0..-4 on 4-frame steps (field_effect_helpers.c:1107)") +eq(blobY2(), P.spriteYOffset, "RSE surf: blob and player share y2") + +resetPlayer() +P.surfing = true +P.moving, P.targetX, P.targetY, P.progress, P.stepFrames = true, 5, 6, 0, 1000 +local moving = run(16) +eq(moving[16], -4, "RSE surf: bob keeps running while the player swims") +P.moving = false + +resetPlayer() +P.surfHopping = true +P.moving, P.jumping, P.targetX, P.targetY, P.progress, P.stepFrames = true, true, 5, 6, 0, 1000 +run(20) +eq(blobY2(), 0, "RSE surf hop: blob holds still until the hop ends (field_effect.c:3068)") + +local Collision = require("src.core.game3.collision") +local realElev = Collision.elevationAt +Collision.elevationAt = function(x, y) return (x == 5 and y == 4) and 3 or 1 end +resetPlayer() +P.surfing = true +local nearLand = seq({ { 0, 7 }, { -1, 8 }, { -2, 8 }, { -1, 8 }, { 0, 8 } }) +same(run(#nearLand), nearLand, "RSE surf next to land bobs every 8th frame (field_effect_helpers.c:1097)") +Collision.elevationAt = realElev + +resetPlayer() +P.underwater = true +local UNDER = seq({ { 1, 4 }, { 2, 4 }, { 3, 4 }, { 4, 4 }, { 3, 4 }, { 2, 4 }, { 1, 4 }, { 0, 4 }, { 1, 4 } }) +same(run(#UNDER), UNDER, "underwater: player sprite bobs 0..+4 (field_effect_helpers.c:1164)") + +FE._manifest = false +resetPlayer() +P.surfing = true +local frlg = run(150) +check(frlg[1] == 0 and frlg[48] == 0, "FRLG surf: player rests on blob frame 0") +check(frlg[49] == 1 and frlg[96] == 1, "FRLG surf: player drops 1px on blob frame 1 (pokefirered field_effect_helpers.c:1069)") +eq(frlg[97], 0, "FRLG surf: 96-frame blob cycle") +eq(frlg[150], 1, "FRLG surf: second cycle on frame 1") +eq(blobY2(), 0, "FRLG surf: blob never moves") +P.facing = "left" +eq(run(1)[1], 0, "FRLG surf: turning restarts the blob anim (pokefirered field_effect_helpers.c:1020)") + +resetPlayer() +FE._manifest = nil +T.finish() From 7a5054a60f275aad3198d9090ece15c9c90298ba Mon Sep 17 00:00:00 2001 From: bryanthaboi Date: Fri, 9 Oct 2026 13:42:02 -0400 Subject: [PATCH 3/3] CLOSES #2813 --- src/core/game3/battle_bridge.lua | 3 + src/core/game3/field_effects.lua | 6 + src/core/game3/map.lua | 3 + .../drivers/em_surf_bob_reload_fish_2813.lua | 111 ++++++++++++++++++ 4 files changed, 123 insertions(+) create mode 100644 tests/drivers/em_surf_bob_reload_fish_2813.lua diff --git a/src/core/game3/battle_bridge.lua b/src/core/game3/battle_bridge.lua index 5018bab8..d35dc069 100644 --- a/src/core/game3/battle_bridge.lua +++ b/src/core/game3/battle_bridge.lua @@ -484,6 +484,9 @@ function BattleBridge.start(mod, game, foe, opts) if okF and Fade and Fade.begin and not opts.headless and opts.fade ~= false then Fade.begin(Fade.MODE.FROM_BLACK, 1) end + -- pokeemerald/src/field_player_avatar.c:883 + local FieldFx = package.loaded["src.core.game3.field_effects"] + if FieldFx and FieldFx.resetSurfBob then FieldFx.resetSurfBob() end -- pokefirered/src/battle_setup.c:432 local okS, Space = pcall(require, "src.core.game3.scripting.space") if okS and Space and Space.returnToField then diff --git a/src/core/game3/field_effects.lua b/src/core/game3/field_effects.lua index 49ffee61..eae50cf3 100644 --- a/src/core/game3/field_effects.lua +++ b/src/core/game3/field_effects.lua @@ -1681,6 +1681,12 @@ function FieldEffects.stepSurfBob() SurfBob.tick(b, state) end +-- pokeemerald/src/field_player_avatar.c:883 +function FieldEffects.resetSurfBob() + FieldEffects._surfBob = nil + FieldEffects._underwaterBob = nil +end + function FieldEffects.surfBlobY2() local b = FieldEffects._surfBob return b and b.y2 or 0 diff --git a/src/core/game3/map.lua b/src/core/game3/map.lua index 3489ed14..2d75daa8 100644 --- a/src/core/game3/map.lua +++ b/src/core/game3/map.lua @@ -498,6 +498,9 @@ function Map.load(mod, game, mapId, opts) -- pokeemerald/src/overworld.c:2170 local CamObj = package.loaded["src.core.game3.camera_object"] if CamObj and CamObj.reset then CamObj.reset() end + -- pokeemerald/src/field_player_avatar.c:883 + local FieldFx = package.loaded["src.core.game3.field_effects"] + if FieldFx and FieldFx.resetSurfBob then FieldFx.resetSurfBob() end end -- pret RestartWildEncounterImmunitySteps on LoadMap / LoadMapFromWarp: every -- map entry restarts the wild encounter grace period. Unconditional, so the diff --git a/tests/drivers/em_surf_bob_reload_fish_2813.lua b/tests/drivers/em_surf_bob_reload_fish_2813.lua new file mode 100644 index 00000000..6eb59c67 --- /dev/null +++ b/tests/drivers/em_surf_bob_reload_fish_2813.lua @@ -0,0 +1,111 @@ +local U = require("tests.drivers.util") +local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/em_surf_bob_reload_fish_2813" + +local failures = 0 +local function check(ok, label) + print((ok and "PASS " or "FAIL ") .. label) + if not ok then failures = failures + 1 end + return ok +end + +local function finish() + print((failures == 0 and "PASS" or "FAIL") .. " em_surf_bob_reload_fish_2813 failures=" .. failures) + love.event.quit(failures == 0 and 0 or 1) +end + +return function(game) + for _ = 1, 900 do + if game.phase == "boot" and game.boot then break end + U.wait(1) + end + if not check(game.boot ~= nil, "boot reached") then return finish() end + game:_handleBootAction({ action = "new_game", name = "BRENDAN", gender = 0 }) + U.wait(60) + + local Runtime = require("src.core.game3.runtime") + local Map = require("src.core.game3.map") + local Player = require("src.core.game3.player") + local Field = require("src.core.game3.field") + local Party = require("src.core.game3.party") + local Battle = require("src.core.game3.battle") + local BattleBridge = require("src.core.game3.battle_bridge") + local Message = require("src.ui.game3.message") + local FieldEffects = require("src.core.game3.field_effects") + local C = require("src.core.game3.constants").of("emerald") + local session = Runtime.getSession() + if not check(session ~= nil, "field session exists") then return finish() end + + session.party = {} + Party.giveMon(session, C.species.byName.SPECIES_WAILMER, 40, "WAILMER") + + local function place(x, y, facing) + Map.load(nil, game, "EM_ROUTE124", { x = x, y = y, facing = facing }) + local s = Runtime.getSession() + s.x, s.y, s.facing = x, y, facing + Player.surfing = true + Field.unlock() + end + local function timer() + local b = FieldEffects._surfBob + return b and b.timer or -1 + end + + place(10, 3, "down") + U.wait(70) + local before = timer() + place(10, 3, "down") + U.wait(10) + local after = timer() + print(string.format("[driver] blob timer before reload %d, 10 frames after %d", before, after)) + check(before > 60 and after >= 0 and after <= 11, "a warp load starts a fresh surf blob") + + U.wait(50) + before = timer() + local ok, err = BattleBridge.startWild(Runtime._mod, game, + { species = C.species.byName.SPECIES_TENTACOOL, level = 5 }, {}) + check(ok == true, "wild battle started " .. tostring(err or "")) + for _ = 1, 600 do + if Battle.isActive() then break end + U.wait(1) + end + U.wait(30) + Battle.abort("run") + local returned + for i = 1, 900 do + if not Battle.isActive() then returned = i break end + U.wait(1) + end + U.wait(10) + after = timer() + print(string.format("[driver] blob timer before battle %d, 10 frames after return %d", before, after)) + check(returned ~= nil and Player.surfing, "back on the water after the battle") + check(after >= 0 and after <= 11, "returning from battle starts a fresh surf blob") + + for _ = 1, 60 do + if not (Message.isOpen and Message.isOpen()) then break end + U.tap(game, "a") + U.wait(3) + end + place(10, 3, "down") + U.wait(40) + check(Field.startFishing(1), "fishing starts while surfing") + local synced, offsets, blobYs, shot = true, {}, {}, false + for _ = 1, 90 do + U.wait(1) + local _, _, fy = Field.fishingPose() + local blob = FieldEffects.surfBlobY2() + offsets[fy or 0] = true + blobYs[blob] = true + if (Player.spriteYOffset or 0) ~= blob then synced = false end + if fy and fy ~= 0 and not shot then + shot = true + U.still(game, DIR .. "/2813_07_fishing_on_water.png") + end + end + local nBlob = 0 + for _ in pairs(blobYs) do nBlob = nBlob + 1 end + check(nBlob > 1, "surf blob keeps bobbing while the rod is out") + check(offsets[8] or offsets[-8], "the rod pose offset fired during the cast") + check(synced, "player y2 is the rod offset plus the blob y2 every frame") + finish() +end