Files
gen1recomp/tests/drivers/em_story_util.lua
T
bryanthaboi 690f0cd9a1 TOUCHDOWN
2026-09-27 16:07:31 -04:00

947 lines
36 KiB
Lua

local U = require("tests.drivers.util")
local S = {}
local DELTA = { up = { 0, -1 }, down = { 0, 1 }, left = { -1, 0 }, right = { 1, 0 } }
local OPP = { up = "down", down = "up", left = "right", right = "left" }
local CONN_DIR = { north = "up", south = "down", west = "left", east = "right" }
local function mod(name) return require(name) end
function S.new(name, defaultDir)
local d = {
name = name,
dir = os.getenv("POKEPORT_SHOT_DIR") or defaultDir,
failures = 0,
passes = 0,
vmLogs = {},
started = {},
}
S.d = d
function d.check(ok, label)
print((ok and "PASS " or "FAIL ") .. label)
if ok then d.passes = d.passes + 1 else d.failures = d.failures + 1 end
return ok
end
function d.note(msg) print("[driver] " .. msg) end
local function capture(msg)
msg = tostring(msg)
if msg:find("skip unknown", 1, true) or msg:find("skip op", 1, true) or msg:find("not ported", 1, true)
or msg:find("missing script", 1, true) or msg:find("runaway", 1, true) or msg:find("LUA ERROR", 1, true)
or msg:find("unknown special", 1, true) or msg:find("unhandled special", 1, true) then
d.vmLogs[#d.vmLogs + 1] = msg
end
end
local origPrint = print
print = function(...)
local parts = {}
for i = 1, select("#", ...) do parts[i] = tostring((select(i, ...))) end
capture(table.concat(parts, "\t"))
return origPrint(...)
end
local Vm = mod("src.core.game3.scripting.vm")
local origNew = Vm.new
Vm.new = function(opts)
local vm = origNew(opts)
local a = vm.adapters
if a and a.log and not a._storyWrapped then
local inner = a.log
a.log = function(msg) capture(msg) return inner(msg) end
a._storyWrapped = true
end
return vm
end
local origStart = Vm.start
Vm.start = function(self, key, facing)
d.started[#d.started + 1] = key
return origStart(self, key, facing)
end
function d.ran(label)
local Space = mod("src.core.game3.scripting.space")
local key = Space.bundle and Space.bundle.labels and Space.bundle.labels[label]
for _, k in ipairs(d.started) do
if k == label or (key and k == key) then return true end
end
return false
end
function d.finish(label)
if d.finished then return end
d.finished = true
d.check(#d.vmLogs == 0, "no unknown-op / unported-system VM logs" .. (label and (" " .. label) or "") .. " (" .. #d.vmLogs .. ")")
for i = 1, math.min(#d.vmLogs, 12) do d.note("VM LOG " .. d.vmLogs[i]) end
print((d.failures == 0 and "PASS" or "FAIL") .. " " .. d.name .. " passes=" .. d.passes .. " failures=" .. d.failures)
love.event.quit(d.failures == 0 and 0 or 1)
U.wait(10)
end
function d.shot(game, file)
return U.still(game, d.dir .. "/" .. file .. ".png")
end
return d
end
function S.session() return mod("src.core.game3.runtime").getSession() end
function S.mapNow() local s = S.session() return s and s.map end
function S.C() return mod("src.core.game3.constants").of("emerald") end
local function F() return mod("src.core.game3.scripting.flags") end
local function EM() return F().forVersion("emerald") end
local function store() return mod("src.core.game3.scripting.space").store end
function S.flag(name) return F().getFlag(store(), nil, assert(EM().IDS[name], name)) == true end
function S.setFlag(name, on) F().setFlag(store(), nil, assert(EM().IDS[name], name), on ~= false) end
function S.var(name) return tonumber(F().getVar(store(), nil, assert(EM().VAR_IDS[name], name))) or 0 end
function S.setVar(name, v) F().setVar(store(), nil, assert(EM().VAR_IDS[name], name), v) end
function S.trainerBeaten(name)
local id = S.C():require("trainers", name)
return F().getFlag(store(), nil, (EM().IDS.TRAINER_FLAGS_START or 0x500) + id) == true
end
function S.item(name) return S.C():require("items", name) end
function S.hasItem(name)
local s = S.session()
return s and s.bag and mod("src.core.game3.bag").has(s.bag, S.item(name), 1) or false
end
function S.giveItem(name, n)
local s = S.session()
return mod("src.core.game3.bag").add(s.bag, S.item(name), n or 1)
end
function S.species(name) return S.C():require("species", name) end
function S.move(name) return S.C():require("moves", name) end
function S.giveMon(species, level, moves)
local Party = mod("src.core.game3.party")
local ok, _, mon = Party.giveMon(S.session(), S.species(species), level, "")
if ok and moves then S.setMoves(mon, moves) end
return mon
end
function S.setMoves(mon, moves)
local Moves = mod("src.core.game3.battle.moves")
mon.moves, mon.pp, mon.maxPp = {}, {}, {}
for i, name in ipairs(moves) do
local id = S.move(name)
local def = Moves.get(id)
local pp = def and tonumber(def.pp) or 10
mon.moves[i], mon.pp[i], mon.maxPp[i] = id, pp, pp
end
end
function S.setLevel(mon, level, species)
local Pokemon = mod("src.core.game3.pokemon")
local SummaryData = mod("src.core.game3.summary_data")
if species then
mon.species, mon.speciesId = S.species(species), S.species(species)
mon.name = Pokemon.name(mon.species)
end
mon.level = level
mon.exp = SummaryData.expForLevel(mon.growthRate or 0, level)
mon.hp = nil
Pokemon.applyStats(mon)
mon.hp = mon.maxHp or mon.hp
mon.status = nil
end
function S.healParty()
mod("src.core.game3.party").healAll(S.session().party)
end
local function Player() return mod("src.core.game3.player") end
local function Warp() return mod("src.core.game3.warp") end
local function Message() return mod("src.ui.game3.message") end
local function Choice() return mod("src.ui.game3.choice") end
local function Battle() return mod("src.core.game3.battle") end
local function Field() return mod("src.core.game3.field") end
function S.vmRunning()
local Space = mod("src.core.game3.scripting.space")
return Space.vm and Space.vm:isRunning() or false
end
function S.busy()
local SE = package.loaded["src.core.game3.step_events"]
return S.vmRunning() or Warp().isBusy() or Message().isOpen() or Field().locked
or Player().moving or Choice().isOpen() or Battle().isActive()
or mod("src.ui.game3.rse.pokenav.call_window").isOpen() or (SE and SE.busy and SE.busy()) or false
end
function S.vmWhere()
local Space = mod("src.core.game3.scripting.space")
local vm = Space.vm
if not (vm and vm:isRunning()) then return "idle" end
local pc = vm.ctx.pc
local list = pc and vm.scripts[pc.listKey]
local row = list and list[pc.index]
return string.format("%s/%s #%s op=%s mode=%s", tostring(vm._scriptKey), tostring(pc and pc.listKey),
tostring(pc and pc.index), tostring(row and row.op), tostring(vm.ctx.mode))
end
local function pickMove(st, id)
local Commands = mod("src.core.game3.battle.commands")
local Moves = mod("src.core.game3.battle.moves")
local Types = mod("src.core.game3.battle.types")
local b = st and st.battlers and st.battlers[id]
local mon = b and b.mon
if not mon then return nil end
local foe
for fid = 1, 3, 2 do
local f = st.battlers[fid]
if f and f.mon and (tonumber(f.mon.hp) or 0) > 0 then foe = f break end
end
local best, bestScore
for slot = 1, 4 do
local mv = mon.moves and mon.moves[slot]
if mv and mv ~= 0 and Commands.moveUsable(st, slot, id) then
local def = Moves.get(mv)
local pow = def and tonumber(def.power) or 0
local t = def and tonumber(def.type)
local score = pow
if foe and t then score = score * Types.effectiveness(t, foe.type1, foe.type2) end
if t and (t == b.type1 or t == b.type2) then score = score * 1.5 end
if not best or score > bestScore then best, bestScore = slot, score end
end
end
return best
end
function S.fight(game, opts)
opts = opts or {}
local Ui = mod("src.core.game3.battle.ui")
local Anim = mod("src.core.game3.battle.anim")
local PartyMenu = mod("src.ui.game3.party_menu")
local StatGrowth = mod("src.ui.game3.stat_growth")
local B = Battle()
local st = B._st
local guard = 0
while B.isActive() and guard < (opts.guard or 60000) do
guard = guard + 1
st = B._st or st
if opts.onFrame then opts.onFrame(st, B._phase) end
if guard % 3000 == 0 and S.d and os.getenv("EM_STORY_DEBUG") then
S.d.note(string.format("fight loop %d phase=%s mode=%s menu=%s move=%s turn=%s party=%s", guard, tostring(B._phase),
tostring(Ui._mode), tostring(Ui._menuIndex), tostring(Ui._moveIndex), tostring(st and st.turn),
tostring(PartyMenu.isOpen and PartyMenu.isOpen())))
if guard == 3000 and os.getenv("EM_STORY_DEBUG") then U.still(game, S.d.dir .. "/stuck_fight.png") end
end
local phase = B._phase
if PartyMenu.isOpen and PartyMenu.isOpen() then
local pick
for i = 1, #(PartyMenu._party or {}) do
local ok = PartyMenu._validate == nil or PartyMenu._validate(i) == nil
local mon = PartyMenu._party[i]
if ok and mon and (tonumber(mon.hp) or 0) > 0 and not mon.isEgg then pick = i break end
end
if not pick then U.tap(game, "b") U.wait(10) else
PartyMenu.cursor = pick
U.wait(10)
U.tap(game, "a")
U.wait(10)
PartyMenu.actionCursor = 1
U.tap(game, "a")
U.wait(10)
end
elseif phase == "command" and Ui._mode == "menu" and not Anim.busy() and S.pendingBattleShot and S.d then
local file = S.pendingBattleShot
S.pendingBattleShot = nil
U.wait(10)
S.d.shot(game, file)
elseif phase == "command" and Ui._mode == "menu" and not Anim.busy() then
if os.getenv("EM_STORY_DEBUG") and S.d and st and st.turn ~= S._lastTurnLogged then
S._lastTurnLogged = st.turn
local p, e = st.battlers and st.battlers[0], st.battlers and st.battlers[1]
local function desc(b)
local m = b and b.mon
return m and string.format("%s lv%s %s/%s st=%s spd=%s", tostring(m.species), tostring(m.level), tostring(m.hp),
tostring(m.maxHp), tostring(m.status), tostring(m.speed)) or "-"
end
S.d.note(" turn " .. tostring(st.turn) .. " P " .. desc(p) .. " | E " .. desc(e))
end
if (Ui._menuIndex or 1) ~= 1 then
U.tap(game, "up") U.wait(3) U.tap(game, "left") U.wait(3)
end
U.tap(game, "a")
U.wait(6)
elseif phase == "command" and Ui._mode == "moves" then
local who = Ui.activeBattler and Ui.activeBattler() or 0
local slot = pickMove(st, who)
if os.getenv("EM_STORY_LOGALL") and S.d then
local b = st.battlers[who]
local f = st.battlers[1]
S.d.note(string.format("pick who=%s slot=%s cur=%s moves=%s types=%s/%s foe=%s/%s", tostring(who), tostring(slot),
tostring(Ui._moveIndex), table.concat(b and b.mon and b.mon.moves or {}, ","), tostring(b and b.type1),
tostring(b and b.type2), tostring(f and f.type1), tostring(f and f.type2)))
end
if slot then
for _ = 1, 6 do
local cur = (Ui._moveIndex or 1) - 1
local want = slot - 1
if cur == want then break end
local key = (cur % 2 ~= want % 2) and ((cur % 2 == 0) and "right" or "left")
or ((cur < want) and "down" or "up")
U.tap(game, key)
U.wait(4)
end
end
U.tap(game, "a")
U.wait(6)
elseif phase == "command" and (Ui._mode == "selmsg" or Ui._mode == "target") then
U.tap(game, "a")
U.wait(6)
elseif StatGrowth.isOpen() then
U.tap(game, "a")
U.wait(4)
else
if Ui.choiceActive and Ui.choiceActive() then
local log = Ui.log and Ui.log() or {}
local last = log[#log]
if type(last) == "table" then
local ok, txt = pcall(mod("src.core.game3.scripting.text_ir").toAscii, last, {})
last = ok and txt or ""
end
if tostring(last):upper():find("STOP", 1, true) then U.tap(game, "a") else U.tap(game, "b") end
U.wait(4)
elseif Ui.dialogPending and Ui.dialogPending() then U.tap(game, "a") else U.wait(1) end
end
end
return st and st.result, st
end
local function answerChoice(game, ans)
local Ch = Choice()
local want
if ans == "no" then want = 2
elseif ans == "yes" or ans == nil or ans == "a" then want = nil
elseif type(ans) == "number" then want = ans
elseif type(ans) == "string" then
for i, o in ipairs(Ch.options or {}) do
local txt = type(o) == "table" and (o.text or o.label or o[1]) or o
if tostring(txt):upper():find(ans:upper(), 1, true) then want = i break end
end
end
if ans == "b" then U.tap(game, "b") U.wait(6) return end
if want then
for _ = 1, 12 do
if Ch.cursor == want then break end
local cols = Ch.cols or 1
if cols > 1 and (Ch.cursor - 1) % cols ~= (want - 1) % cols then
U.tap(game, ((Ch.cursor - 1) % cols < (want - 1) % cols) and "right" or "left")
else
U.tap(game, Ch.cursor < want and "down" or "up")
end
U.wait(3)
end
end
U.tap(game, "a")
U.wait(6)
end
S.battles = {}
function S.settle(game, opts)
opts = opts or {}
local limit = opts.limit or 3000
local pred = opts.until_
local n = 0
local d = S.d
for _ = 1, limit do
if pred and pred() then return true end
if opts.watch then opts.watch() end
local B = Battle()
if B.isActive() then
local st = B._st
local foe = st and st.enemy and st.enemy.mon
local tr = st and (st.trainerId or (st.trainer and st.trainer.id))
local rec = { map = S.mapNow(), trainer = tr, foe = foe and foe.species, level = foe and foe.level }
if opts.onBattleStart then opts.onBattleStart(st) end
if os.getenv("EM_STORY_DEBUG") and d and st and st.player and st.player.mon then
local pm = st.player.mon
d.note(string.format("battle start player %s lv%s hp %s/%s atk %s spa %s moves %s", tostring(pm.species), tostring(pm.level),
tostring(pm.hp), tostring(pm.maxHp), tostring(pm.atk or (pm.stats and pm.stats.atk)), tostring(pm.spa or (pm.stats and pm.stats.spa)),
table.concat(pm.moves or {}, ",")))
end
local result = S.fight(game, { onFrame = opts.onBattleFrame })
rec.result = result
if os.getenv("EM_STORY_DEBUG") and d then
local Ui = mod("src.core.game3.battle.ui")
if result ~= "win" or os.getenv("EM_STORY_LOGALL") then
for i, t in ipairs(Ui.log and Ui.log() or {}) do
if type(t) == "table" then t = select(2, pcall(mod("src.core.game3.scripting.text_ir").toAscii, t, {})) end
d.note(" log " .. i .. " " .. tostring(t):gsub("\n", " "))
end
end
end
S.battles[#S.battles + 1] = rec
if d then d.note(string.format("battle on %s trainer=%s foe=%s lv%s -> %s", tostring(rec.map), tostring(tr),
tostring(rec.foe), tostring(rec.level), tostring(result))) end
if opts.onBattleEnd then opts.onBattleEnd(result, st) end
elseif Choice().isOpen() then
answerChoice(game, opts.choice and opts.choice(Choice()) or "yes")
elseif Message().isOpen() then
n = n + 1
if Message().isTyping() then Message().skipReveal() end
if n % 4 == 0 then U.tap(game, "a") else U.wait(1) end
elseif opts.onIdleUi and opts.onIdleUi() then
elseif mod("src.ui.game3.rse.pokenav.call_window").isOpen() then
n = n + 1
if n % 4 == 0 then U.tap(game, "a") else U.wait(1) end
else
U.wait(1)
end
if not pred and not S.busy() then
U.wait(3)
if not S.busy() then return true end
end
end
if pred then return pred() and true or false end
return not S.busy()
end
local function passable(game, fx, fy, tx, ty, dir, elev)
local Collision = mod("src.core.game3.collision")
if Player().underwater then
return Collision.canEnter(game, tx, ty, { fromX = fx, fromY = fy, dir = dir, elevation = elev, surfing = true })
end
local surfing
if S.canSurf then
local fw, tw = Collision.isWater(fx, fy), Collision.isWater(tx, ty)
surfing = fw or tw
if tw and not fw then elev = nil end
end
local b = Collision.behavior(tx, ty)
if b and (Collision.isBumpySlope(b) or Collision.isIsolatedVerticalRail(b) or Collision.isIsolatedHorizontalRail(b)
or Collision.isVerticalRail(b) or Collision.isHorizontalRail(b) or (dir == "up" and Collision.isMuddySlope(b))) then
return false
end
return Collision.canEnter(game, tx, ty, { fromX = fx, fromY = fy, dir = dir, elevation = elev, surfing = surfing })
end
local SLIDE_DIR = {
PushedSouthByCurrent = "down", PushedNorthByCurrent = "up", PushedWestByCurrent = "left", PushedEastByCurrent = "right",
WalkSouth = "down", WalkNorth = "up", WalkWest = "left", WalkEast = "right",
}
local function slideEnd(game, x, y, dir, e)
local Collision = mod("src.core.game3.collision")
local FM = mod("src.core.game3.forced_movement")
for _ = 1, 80 do
local _, row = FM.lookup(Collision.behavior(x, y))
local sdir = row and (SLIDE_DIR[row.name] or (row.name == "Slip" and dir))
if not sdir then break end
local dd = DELTA[sdir]
if not passable(game, x, y, x + dd[1], y + dd[2], sdir, e) then break end
x, y, dir = x + dd[1], y + dd[2], sdir
end
return x, y
end
function S.mountSurf(game, dir)
local P = Player()
S.face(game, dir)
U.tap(game, "a")
U.wait(6)
S.settle(game, { limit = 4000, until_ = function() return P.surfing and not S.busy() end })
if P.surfing then
S.surfMounts = (S.surfMounts or 0) + 1
if S.onSurf then S.onSurf() end
end
return P.surfing
end
local function nextElev(px, py, x, y, e)
local Collision = mod("src.core.game3.collision")
local z = Collision.elevationAt and Collision.elevationAt(x, y)
local p = Collision.elevationAt and Collision.elevationAt(px, py)
if z == nil or z == 15 or p == 15 then return e end
return z
end
function S.path(game, goal, opts)
opts = opts or {}
local P = Player()
local Collision = mod("src.core.game3.collision")
local sx, sy = P.cellX, P.cellY
local se = P.currentElevation
if opts.from then sx, sy, se = opts.from[1], opts.from[2], opts.from[3] end
local function skey(x, y, e) return x .. "," .. y .. "," .. tostring(e) end
local s0 = { sx, sy, se }
local prev = { [skey(sx, sy, s0[3])] = false }
local q, head = { s0 }, 1
local found
local isGoal = type(goal) == "function" and goal or function(x, y) return x == goal[1] and y == goal[2] end
local avoid = opts.avoid or {}
while head <= #q do
local c = q[head]
head = head + 1
if isGoal(c[1], c[2]) and not (c[1] == sx and c[2] == sy and opts.notHere) then found = c break end
local e = c[3]
for _, dir in ipairs({ "up", "down", "left", "right" }) do
local dd = DELTA[dir]
local nx, ny = c[1] + dd[1], c[2] + dd[2]
local lx, ly = Collision.ledgeLanding(game, c[1], c[2], dir)
local land = lx ~= nil
if land then
if passable(game, lx - dd[1], ly - dd[2], lx, ly, dir, nil) then nx, ny = lx, ly else land = false nx = nil end
end
if nx and not avoid[nx .. "," .. ny] and #q < 30000 then
local ok = land and true or passable(game, c[1], c[2], nx, ny, dir, e)
if not ok and opts.goalBlocked and isGoal(nx, ny) then ok = true end
if ok and not land and S.slides ~= false then nx, ny = slideEnd(game, nx, ny, dir, e) end
if ok then
local ne = nextElev(c[1], c[2], nx, ny, e)
local k = skey(nx, ny, ne)
if prev[k] == nil then
prev[k] = { c, dir }
q[#q + 1] = { nx, ny, ne }
end
end
end
end
end
if not found then return nil, head end
local out, k = {}, skey(found[1], found[2], found[3])
while prev[k] do
local p = prev[k]
table.insert(out, 1, p[2])
k = skey(p[1][1], p[1][2], p[1][3])
end
return out, found
end
function S.step(game, dir)
local P = Player()
local x0, y0, m0 = P.cellX, P.cellY, S.mapNow()
for _ = 1, 3 do
for i = 1, 16 do
if i == 1 then table.insert(game.input.pressQueue, dir) end
game.input.state[dir] = true
U.wait(1)
if P.moving or P.cellX ~= x0 or P.cellY ~= y0 or S.mapNow() ~= m0 or Warp().isBusy() or S.busy() then break end
end
game.input.state[dir] = false
for _ = 1, 90 do
if not P.moving and not Warp().isBusy() then break end
U.wait(1)
end
if P.cellX ~= x0 or P.cellY ~= y0 or S.mapNow() ~= m0 or S.busy() then break end
end
local lx, ly, stable = P.cellX, P.cellY, 0
for _ = 1, 600 do
if not P.moving and not Warp().isBusy() then
if P.cellX == lx and P.cellY == ly then stable = stable + 1 else stable, lx, ly = 0, P.cellX, P.cellY end
if stable >= 3 then break end
else
stable = 0
end
U.wait(1)
end
end
function S.face(game, dir)
local P = Player()
if P.facing ~= dir then
U.tap(game, dir)
U.wait(12)
for _ = 1, 60 do
if not P.moving then break end
U.wait(1)
end
end
end
function S.goTo(game, goal, opts)
opts = opts or {}
local P = Player()
local m0 = S.mapNow()
local isGoal = type(goal) == "function" and goal or function(x, y) return x == goal[1] and y == goal[2] end
local misses = 0
for attempt = 1, opts.tries or 30 do
if isGoal(P.cellX, P.cellY) then return true end
if misses >= (opts.maxMisses or 3) then break end
if S.mapNow() ~= m0 then return false, "map" end
if S.busy() then S.settle(game, opts.settle) end
if S.mapNow() ~= m0 then return false, "map" end
local p, explored = S.path(game, goal, opts)
if not p then
misses = misses + 1
if S.d then
local Collision = mod("src.core.game3.collision")
local parts = {}
for dir, dd in pairs(DELTA) do
local ok, why = Collision.canEnter(game, P.cellX + dd[1], P.cellY + dd[2],
{ fromX = P.cellX, fromY = P.cellY, dir = dir, elevation = P.currentElevation })
parts[#parts + 1] = dir .. "=" .. tostring(ok) .. "/" .. tostring(why)
end
S.d.note(string.format("no path on %s from %d,%d elev %s (attempt %d, explored %s) %s", tostring(S.mapNow()), P.cellX,
P.cellY, tostring(P.currentElevation), attempt, tostring(explored), table.concat(parts, " ")))
end
U.wait(40)
else
misses = 0
for _, dir in ipairs(p) do
local bx, by = P.cellX, P.cellY
local dd = DELTA[dir]
if S.canSurf and not P.surfing and not P.underwater and mod("src.core.game3.collision").isWater(bx + dd[1], by + dd[2]) then
S.mountSurf(game, dir)
break
end
S.step(game, dir)
if S.mapNow() ~= m0 or S.busy() then break end
if P.cellX == bx and P.cellY == by then
S._blockCount = (S._lastBlock == bx .. "," .. by .. dir) and (S._blockCount or 0) + 1 or 1
S._lastBlock = bx .. "," .. by .. dir
if (os.getenv("EM_STORY_DEBUG") or S._blockCount == 4) and S.d then
local held = {}
for k, v in pairs(game.input.state) do if v then held[#held + 1] = tostring(k) end end
local SE = package.loaded["src.core.game3.step_events"]
local RT = package.loaded["src.core.game3.runtime"]
local FD = package.loaded["src.ui.game3.fade"]
S.d.note(string.format("step %s blocked at %d,%d on %s (path len %d) facing %s held {%s} turn %s/%s se=%s ui=%s fade=%s",
dir, bx, by, tostring(S.mapNow()), #p, tostring(P.facing), table.concat(held, ","), tostring(P.turnTimer),
tostring(P.turnArmed), tostring(SE and SE.busy and SE.busy()), tostring(RT and RT.uiBusy and RT.uiBusy()),
tostring(FD and FD.lockInput)))
local H = package.loaded["src.ui.game3.hud"]
local ev = SE and SE._activeEvent
S.d.note(string.format(" hud menu=%s fade=%s wait=%s msg=%s ev=%s queue=%d call=%s", tostring(H and H.isMenuOpen()),
tostring(FD and FD.isActive and FD.isActive()), tostring(H and H._waitButton ~= nil), tostring(Message().isOpen()),
tostring(ev and ev.type), SE and #SE._queue or -1,
tostring(mod("src.ui.game3.rse.pokenav.call_window").isOpen())))
end
U.wait(8)
break
end
end
end
end
return isGoal(P.cellX, P.cellY)
end
function S.adjacentTo(game, tx, ty, opts)
opts = opts or {}
local ok = S.goTo(game, function(x, y)
return math.abs(x - tx) + math.abs(y - ty) == 1
end, opts)
if not ok then return false end
local P = Player()
local dir
for dd, v in pairs(DELTA) do
if P.cellX + v[1] == tx and P.cellY + v[2] == ty then dir = dd end
end
S.face(game, dir)
return true
end
function S.object(localId)
return mod("src.core.game3.objects").find(localId)
end
function S.objectByScript(label)
local Objects = mod("src.core.game3.objects")
local Space = mod("src.core.game3.scripting.space")
local want = Space.bundle and Space.bundle.labels and Space.bundle.labels[label]
for _, lid in ipairs(Objects.listActive() or {}) do
local eo = Objects.find(lid)
local s = eo and (eo.scriptKey or (eo.def and (eo.def.script or eo.def.scriptKey)))
if s == label or (want and s == want) then return eo end
end
if S.d then
for _, lid in ipairs(Objects.listActive() or {}) do
local eo = Objects.find(lid)
S.d.note(string.format("object %s at %s,%s script=%s def.script=%s", tostring(lid), tostring(eo.cellX), tostring(eo.cellY),
tostring(eo.scriptKey), tostring(eo.def and eo.def.script)))
end
end
return nil
end
function S.useRepel()
local s = S.session()
if (tonumber(s.repelSteps) or 0) > 0 then return true end
if not S.hasItem("ITEM_MAX_REPEL") then S.giveItem("ITEM_MAX_REPEL", 1) end
local ok = mod("src.core.game3.item_use").useField(s, s.bag, S.item("ITEM_MAX_REPEL"), nil)
return ok
end
function S.talkAt(game, tx, ty, opts)
if not S.adjacentTo(game, tx, ty, opts) then return false end
U.tap(game, "a")
U.wait(4)
return true
end
function S.talkTo(game, eo, opts)
if not eo then return false end
local P = Player()
local Objects = mod("src.core.game3.objects")
for _ = 1, 16 do
local tx, ty = eo.cellX, eo.cellY
if eo.moving and eo.targetX then tx, ty = eo.targetX, eo.targetY end
if os.getenv("EM_STORY_DEBUG") and S.d then S.d.note(string.format("talkTo lid %s at %d,%d", tostring(eo.localId), tx, ty)) end
if not S.adjacentTo(game, tx, ty, opts) then U.wait(20) end
if S.busy() and not P.moving then
S.settle(game, opts and opts.settle)
elseif math.abs(P.cellX - tx) + math.abs(P.cellY - ty) == 1 then
for _ = 1, 180 do
if Objects.at(tx, ty) == eo and not P.moving then break end
U.wait(1)
end
if Objects.at(tx, ty) == eo then
U.tap(game, "a")
for _ = 1, 10 do
if S.busy() then return true end
U.wait(1)
end
end
end
end
return false
end
function S.partyHealthy()
for _, m in ipairs(S.session().party or {}) do
if not m.isEgg and (tonumber(m.hp) or 0) < (tonumber(m.maxHp) or 0) then return false end
end
return true
end
function S.needsHeal(threshold)
for _, m in ipairs(S.session().party or {}) do
if not m.isEgg then
local st = m.status
if (type(st) == "number" and st ~= 0) or (type(st) == "string" and st ~= "") or type(st) == "table" then return true end
if (tonumber(m.hp) or 0) < (tonumber(m.maxHp) or 0) * (threshold or 0.8) then return true end
end
end
return false
end
function S.healAtCenter(game, cityMap, labelPrefix)
S.travel(game, { cityMap, cityMap .. "_POKEMON_CENTER_1F" }, { settle = { limit = 20000 } })
if S.mapNow() ~= cityMap .. "_POKEMON_CENTER_1F" then return false end
local nurse = S.objectByScript(labelPrefix .. "_PokemonCenter_1F_EventScript_Nurse")
if not nurse then return false end
S.goTo(game, { nurse.cellX, nurse.cellY + 2 })
S.face(game, "up")
U.tap(game, "a")
S.settle(game, { limit = 8000 })
local ok = S.partyHealthy()
S.travel(game, { cityMap }, { settle = { limit = 8000 } })
return ok
end
local function currentDef(game)
return game.data.maps[S.mapNow()]
end
local function exitCandidates(game, nextMap, arrive)
local def = currentDef(game)
local out = {}
local dest = game.data.maps[nextMap]
for i, w in ipairs(def and def.warps or {}) do
if w.destMap == nextMap then
local ok = true
if arrive then
local dw = dest and dest.warps and dest.warps[tonumber(w.destWarp) or 0]
ok = dw ~= nil and math.abs(dw.x - arrive[1]) + math.abs(dw.y - arrive[2]) <= 3
end
if ok then out[#out + 1] = { kind = "warp", x = w.x, y = w.y, index = i } end
end
end
if not arrive then
local Connections = mod("src.core.game3.connections")
for _, c in ipairs(Connections.each(def)) do
if c.map == nextMap then
local dw, dh = Connections.sizeOf(dest)
local span = (c.dir == "north" or c.dir == "south") and dw or dh
out[#out + 1] = { kind = "conn", dir = CONN_DIR[c.dir], lo = c.offset, hi = c.offset + span - 1 }
end
end
end
return out
end
function S.exitTo(game, nextMap, opts)
opts = opts or {}
local P = Player()
local m0 = S.mapNow()
local cands = exitCandidates(game, nextMap, opts.arrive)
if #cands == 0 then
if S.d then S.d.note("no exit from " .. tostring(m0) .. " to " .. tostring(nextMap)) end
return false
end
local Collision = mod("src.core.game3.collision")
local function edgeW()
local ox, oy = mod("src.core.game3.connections").sizeOf(currentDef(game))
return ox, oy
end
local reach, other = {}, {}
for _, c in ipairs(cands) do
local ok
if c.kind == "warp" then
ok = S.path(game, { c.x, c.y }, { goalBlocked = true }) ~= nil
else
local w, h = edgeW()
ok = S.path(game, function(x, y)
local along = (c.dir == "up" or c.dir == "down") and x or y
if c.lo and (along < c.lo or along > c.hi) then return false end
if c.dir == "up" then return y == 0 elseif c.dir == "down" then return y == h - 1
elseif c.dir == "left" then return x == 0 else return x == w - 1 end
end) ~= nil
end
if ok then reach[#reach + 1] = c else other[#other + 1] = c end
end
for _, c in ipairs(other) do reach[#reach + 1] = c end
cands = reach
for _ = 1, opts.tries or 6 do
if S.mapNow() == nextMap then return true end
if S.mapNow() ~= m0 then return false end
for _, c in ipairs(cands) do
if S.mapNow() ~= m0 then break end
if c.kind == "warp" then
local wx, wy = c.x, c.y
if opts.warpXY then wx, wy = opts.warpXY[1], opts.warpXY[2] end
local avoid
if opts.avoidWarps then
avoid = {}
for _, w in ipairs(currentDef(game).warps or {}) do
if not (w.x == wx and w.y == wy) then avoid[w.x .. "," .. w.y] = true end
end
end
local reached = S.goTo(game, { wx, wy }, { goalBlocked = true, tries = opts.goToTries or 12, settle = opts.settle,
avoid = avoid })
if os.getenv("EM_STORY_DEBUG") and S.d then
S.d.note(string.format("exit warp %d,%d reached=%s now %s %d,%d", wx, wy, tostring(reached), tostring(S.mapNow()), P.cellX, P.cellY))
end
if S.mapNow() == m0 and not reached then
S.adjacentTo(game, wx, wy, { tries = 6 })
local dx, dy = wx - P.cellX, wy - P.cellY
local dir = dx > 0 and "right" or dx < 0 and "left" or dy > 0 and "down" or "up"
if math.abs(dx) + math.abs(dy) == 1 then S.step(game, dir) end
end
if S.mapNow() == m0 and P.cellX == wx and P.cellY == wy then
local order = {}
if opts.pushDir then order[1] = opts.pushDir end
for _, dir in ipairs({ "down", "up", "left", "right" }) do
local dd = DELTA[dir]
if not passable(game, wx, wy, wx + dd[1], wy + dd[2], dir, P.currentElevation) then order[#order + 1] = dir end
end
for _, dir in ipairs(order) do
if S.mapNow() ~= m0 or P.cellX ~= wx or P.cellY ~= wy then break end
S.step(game, dir)
S.settle(game, { limit = 200 })
end
end
else
local w, h = Collision._w or (currentDef(game).width * 2), Collision._h or (currentDef(game).height * 2)
local ox, oy = mod("src.core.game3.connections").sizeOf(currentDef(game))
w, h = ox > 0 and ox or w, oy > 0 and oy or h
local edge = function(x, y)
local along = (c.dir == "up" or c.dir == "down") and x or y
if c.lo and (along < c.lo or along > c.hi) then return false end
if c.dir == "up" then return y == 0 elseif c.dir == "down" then return y == h - 1
elseif c.dir == "left" then return x == 0 else return x == w - 1 end
end
local preferX = opts.edgeNear
local dead = {}
local goal = function(x, y)
if not edge(x, y) or dead[x .. "," .. y] then return false end
if preferX then
return math.abs((c.dir == "up" or c.dir == "down") and x - preferX or y - preferX) <= (opts.edgeSlack or 3)
end
return true
end
for _ = 1, 12 do
if S.mapNow() ~= m0 then break end
S.goTo(game, goal, { tries = opts.goToTries or 20, settle = opts.settle })
if S.mapNow() == m0 and edge(P.cellX, P.cellY) then
local ex, ey = P.cellX, P.cellY
for _ = 1, 2 do
if S.mapNow() ~= m0 then break end
S.step(game, c.dir)
end
if S.mapNow() == m0 and not S.busy() then dead[P.cellX .. "," .. P.cellY] = true end
if S.mapNow() == nextMap and not S.busy() then
local _, explored = S.path(game, function() return false end)
if (explored or 0) < 8 then
S.step(game, OPP[c.dir])
S.settle(game, { limit = 600 })
if S.mapNow() == m0 then dead[ex .. "," .. ey] = true end
end
end
elseif S.mapNow() == m0 and not S.busy() then
break
end
end
end
if S.mapNow() ~= m0 then break end
end
if S.busy() then S.settle(game, opts.settle) end
end
if S.busy() then S.settle(game, opts.settle) end
return S.mapNow() == nextMap
end
function S.travel(game, chain, opts)
opts = opts or {}
for _, entry in ipairs(chain) do
local m, eopts = entry, opts[entry] or opts
if type(entry) == "table" then
m = entry[1]
eopts = setmetatable({ arrive = entry.arrive, pushDir = entry.pushDir }, { __index = opts })
end
if S.mapNow() ~= m then
if opts.repel ~= false then S.useRepel() end
local ok = S.exitTo(game, m, eopts)
S.settle(game, opts.settle)
if not ok and S.mapNow() ~= m then
if S.d then S.d.note("travel stuck on " .. tostring(S.mapNow()) .. " heading to " .. m .. " at "
.. Player().cellX .. "," .. Player().cellY .. " vm " .. S.vmWhere()) end
return false
end
end
end
return true
end
function S.checkpointPath(name)
return (os.getenv("EM_STORY_CKPT") or "/tmp/em_story_ckpt") .. "/" .. name .. ".lua"
end
function S.saveCheckpoint(game, name)
local SaveData = mod("src.core.SaveData")
local Schema = mod("src.core.game3.save_schema_firered")
local Space = mod("src.core.game3.scripting.space")
local s = S.session()
local P = Player()
s.x, s.y, s.facing = P.cellX, P.cellY, P.facing
pcall(function() Space.persistSession(nil, game) end)
local t = Schema.toSaveTable(s)
local path = S.checkpointPath(name)
os.execute('mkdir -p "' .. path:match("^(.*)/") .. '"')
local f = io.open(path, "wb")
if not f then return false end
f:write(SaveData.encode(t))
f:close()
return true
end
function S.loadCheckpoint(game, name)
local SaveData = mod("src.core.SaveData")
local Schema = mod("src.core.game3.save_schema_firered")
local f = io.open(S.checkpointPath(name), "rb")
if not f then return false end
local raw = f:read("*a")
f:close()
local save = SaveData.decode(raw)
local session = Schema.fromSaveTable(save)
mod("src.core.game3.options").bind(session, game.options)
game:adoptSave(session, true)
game.sessionStartedAt = os.time()
game:_enterField(session, "continue")
U.wait(30)
S.settle(game)
return true
end
return S