Merge pull request #2816 from bryanthaboi/dev

This commit is contained in:
bryanthaboi
2026-10-09 13:47:50 -04:00
committed by GitHub
18 changed files with 867 additions and 37 deletions
+11 -4
View File
@@ -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
+3
View File
@@ -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
+1 -1
View File
@@ -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()
+1
View File
@@ -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)
+66 -11
View File
@@ -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,62 @@ 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
-- 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
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 +1737,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 +2004,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 +2032,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
+7 -4
View File
@@ -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
@@ -726,15 +729,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)
+13 -6
View File
@@ -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
+96
View File
@@ -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
+2 -3
View File
@@ -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()
+12 -8
View File
@@ -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}))
+143
View File
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
+121
View File
@@ -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()
+43
View File
@@ -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")