mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-28 23:00:21 -04:00
Binary file not shown.
@@ -132,6 +132,7 @@ function love.conf(t)
|
||||
t.accelerometerjoystick = false
|
||||
else
|
||||
t.window.resizable = true
|
||||
pcall(function() require("src.core.PadHints").apply(osName) end)
|
||||
if os.getenv("POKEPORT_BACKGROUND") == "1" and osName == "OS X" then
|
||||
pcall(function()
|
||||
local ffi = require("ffi")
|
||||
|
||||
+19
-14
@@ -1212,9 +1212,9 @@ identifiers: `pc_box_withdraw`, `pc_box_deposit`, `pc_box_release`,
|
||||
control the battle text/menu layer and the HP/status panels. Both receive
|
||||
`(next, state)` and default to `true`, so vanilla rendering is unchanged.
|
||||
Both hooks apply to Gen 1 and Gen 2 battles.
|
||||
Text boxes and YES/NO prompts pushed above a battle inherit a `false` result
|
||||
for that battle, so hiding the bottom layer cannot leave their white backing
|
||||
behind under another overlay. Text boxes also pass through the hook as their
|
||||
Text boxes and YES/NO prompts pushed above a battle of either generation
|
||||
inherit a `false` result for that battle, so hiding the bottom layer cannot
|
||||
leave their white backing behind under another overlay. Text boxes also pass through the hook as their
|
||||
own state, preserving selective control outside a battle; a wrapper that only
|
||||
owns battle presentation should return `false` only for its active battle or
|
||||
text-box state.
|
||||
@@ -1251,17 +1251,22 @@ level is part of a row label rather than a drawn field. Both are noted in
|
||||
RFC 0019 as follow-ups.
|
||||
|
||||
`core.logic_speed` receives `(next, game)` once per `Game:logicSpeed()` call
|
||||
(once per frame). Vanilla behavior resolves the per-category GAME SPEED
|
||||
option (`GameSpeed.CATEGORIES`: overworld/battle/menu) for whichever
|
||||
category `Game.speedCategoryInStack` says is active right now. A mod may
|
||||
call `next(game)` and return its result to pass that resolution through, or
|
||||
return a different number outright to override it for that frame (a bot mod
|
||||
forcing 1X for one route segment, say, regardless of the category or saved
|
||||
option). The result is clamped to the nearest valid `GameSpeed.LEVELS` entry
|
||||
regardless of what a subscriber returns, so a bad value (0, negative, `nil`)
|
||||
cannot destabilize the fixed-step accumulator. This hook runs *after* link
|
||||
play's 1X lock and the `--speed`/equivalent run-argument override, both of
|
||||
which stay unconditional and are never visible to a subscriber.
|
||||
(once per frame), but only on frames where the speed is not locked. Every
|
||||
battle (wild, trainer, link, online, tournament, spectate), link play and a
|
||||
fixed-speed minigame lock the logic clock to 1X, and `Game:speedLocked()`
|
||||
is checked before anything else: the hook is never called on those frames,
|
||||
so it cannot fast-forward a battle. On every other frame vanilla behavior
|
||||
resolves the per-category GAME SPEED option (`speedOverworld` or
|
||||
`speedMenu`) for whichever category `Game.speedCategoryInStack` says is
|
||||
active. A mod may call `next(game)` and return its result to pass that
|
||||
resolution through, or return a different number outright to override it
|
||||
for that frame (a bot mod forcing 1X for one route segment, say). The
|
||||
result is clamped to the nearest valid `GameSpeed.LEVELS` entry regardless
|
||||
of what a subscriber returns, so a bad value (0, negative, `nil`) cannot
|
||||
destabilize the fixed-step accumulator. The `--speed`/`POKEPORT_SPEED`
|
||||
run-argument override also wins over the hook, but not over the lock.
|
||||
Gen 2 and Gen 3 have no hook; their `logicSpeed()` facades report the
|
||||
shell's own value, lock included.
|
||||
|
||||
Developer mode also arms the mod loader's dev tripwire, which flags mods
|
||||
that reach outside their permission set.
|
||||
|
||||
@@ -27,6 +27,8 @@ Features intentionally added beyond the original Pokémon Red, Blue, and Yellow
|
||||
* **Filter Find mods by game**, a generation or single-game filter of its own, with every listing showing the games and tags it declares
|
||||
* **Update all** in one press from the MODS tab, installing every outdated mod and every installed cart the index lists ahead of it, with a summary of what failed
|
||||
* **Rebindable GAME SPEED shortcuts**, SPEED - / SPEED + rows in CONTROLS that move the shoulder hotkeys to any pad button or switch them off
|
||||
* **Fast-forward locks to 1X in battles and link rooms**, every battle, link session and the FireRed/LeafGreen Union Room on every game
|
||||
* **40-player Union Room** on FireRed and LeafGreen online, a larger square room where every trainer keeps a fixed spot and anyone can be talked to
|
||||
* **Key bar on the touch pad**, a corner toggle that slides out SAVE, LOAD, SPEED, COLOR, TILT and ZOOM for phones with no keyboard
|
||||
* **Save editor item verbs**, sorting the bag and PC by item number or name, filling one stack or every stack to x99, and a coin editor on every game
|
||||
* **Save editor bag / PC move**, a PC / BAG button on every item row that moves the stack across, clamped to the stack and slot caps
|
||||
|
||||
@@ -16,6 +16,7 @@ end
|
||||
local editorMode = os.getenv("POKEPORT_EDITOR") == "1" or POKEPORT_EDITOR_MODE == true
|
||||
|
||||
local SwitchDiagnostics = require("src.debug.SwitchDiagnostics")
|
||||
local PadHints = require("src.core.PadHints")
|
||||
local LaunchOptions = require("src.core.LaunchOptions")
|
||||
local NxDisplay = require("src.core.NxDisplay")
|
||||
local PlatformHooks = require("src.core.PlatformHooks")
|
||||
@@ -132,6 +133,7 @@ do
|
||||
end
|
||||
|
||||
local Game, EditorApp, Importer, TouchEditor, Studio, Prelaunch
|
||||
local launcherSplash
|
||||
|
||||
-- #887: quit-to-launcher state, shared by love.load and love.quit (both need
|
||||
-- it, so it is declared here rather than next to love.quit).
|
||||
@@ -591,7 +593,7 @@ end
|
||||
local deferredLaunchRequest
|
||||
|
||||
local function launcherBusy()
|
||||
return Importer ~= nil and (Importer._updateAll ~= nil
|
||||
return launcherSplash ~= nil or Importer ~= nil and (Importer._updateAll ~= nil
|
||||
or Importer._modInstall ~= nil or Importer._cartInstall ~= nil
|
||||
or Importer._autoUpdateAll ~= nil)
|
||||
end
|
||||
@@ -840,6 +842,7 @@ function love.load(args)
|
||||
-- Edit on a save row opens the bundled editor on that slot (openEditor).
|
||||
Importer = makeLauncher()
|
||||
if not relaunched then
|
||||
launcherSplash = require("src.import.LauncherSplash").new()
|
||||
autoUpdateMods(resolvedLaunch,
|
||||
not resolvedLaunch.game and "mods" or nil)
|
||||
end
|
||||
@@ -851,6 +854,14 @@ function love.update(dt)
|
||||
SwitchDiagnostics.maybeFlush(false)
|
||||
-- NX only (no-op elsewhere): follow dock/undock without waiting for SDL.
|
||||
NxDisplay.sync()
|
||||
if launcherSplash then
|
||||
if launcherSplash:update(dt) then
|
||||
launcherSplash:release()
|
||||
launcherSplash = nil
|
||||
if Importer and Importer.resumeAfterOverlay then Importer:resumeAfterOverlay() end
|
||||
end
|
||||
return
|
||||
end
|
||||
if editorMode then return EditorApp.update(dt) end
|
||||
if TouchEditor then return TouchEditor.update(dt) end
|
||||
if Studio then return Studio.update(dt) end
|
||||
@@ -946,6 +957,7 @@ function love.draw()
|
||||
GameViewport.reset()
|
||||
HostDisplay.beginFrame("launcher", Importer)
|
||||
local result = Importer:draw()
|
||||
if launcherSplash then launcherSplash:draw() end
|
||||
HostDisplay.endFrame("launcher", Importer)
|
||||
return result
|
||||
end
|
||||
@@ -973,6 +985,7 @@ function love.draw()
|
||||
end
|
||||
|
||||
function love.keypressed(key, scancode, isrepeat)
|
||||
if launcherSplash then return end
|
||||
if editorMode then return EditorApp.keypressed(key) end
|
||||
if TouchEditor then return TouchEditor.keypressed(key) end
|
||||
if Studio then return Studio.keypressed(key) end
|
||||
@@ -983,6 +996,7 @@ function love.keypressed(key, scancode, isrepeat)
|
||||
end
|
||||
|
||||
function love.keyreleased(key)
|
||||
if launcherSplash then return end
|
||||
if editorMode or TouchEditor or Studio then return end
|
||||
if Importer then return end
|
||||
if not Game then return end
|
||||
@@ -990,7 +1004,9 @@ function love.keyreleased(key)
|
||||
end
|
||||
|
||||
function love.gamepadpressed(joystick, button)
|
||||
if launcherSplash then return end
|
||||
SwitchDiagnostics.onJoystickEvent("gamepadpressed", joystick, button)
|
||||
if PadHints.windowMinimized() then return end
|
||||
if editorMode then
|
||||
if EditorApp and EditorApp.gamepadpressed then
|
||||
return EditorApp.gamepadpressed(joystick, button)
|
||||
@@ -1011,6 +1027,7 @@ function love.gamepadpressed(joystick, button)
|
||||
end
|
||||
|
||||
function love.gamepadreleased(joystick, button)
|
||||
if launcherSplash then return end
|
||||
SwitchDiagnostics.onJoystickEvent("gamepadreleased", joystick, button)
|
||||
if editorMode then
|
||||
if EditorApp and EditorApp.gamepadreleased then
|
||||
@@ -1031,7 +1048,9 @@ function love.gamepadreleased(joystick, button)
|
||||
end
|
||||
|
||||
function love.gamepadaxis(joystick, axis, value)
|
||||
if launcherSplash then return end
|
||||
SwitchDiagnostics.onJoystickEvent("gamepadaxis", joystick, axis, { value = value })
|
||||
if PadHints.windowMinimized() then value = 0 end
|
||||
if editorMode then
|
||||
if EditorApp and EditorApp.gamepadaxis then
|
||||
return EditorApp.gamepadaxis(joystick, axis, value)
|
||||
@@ -1051,7 +1070,9 @@ function love.gamepadaxis(joystick, axis, value)
|
||||
end
|
||||
|
||||
function love.joystickpressed(joystick, button)
|
||||
if launcherSplash then return end
|
||||
SwitchDiagnostics.onJoystickEvent("joystickpressed", joystick, button)
|
||||
if PadHints.windowMinimized() then return end
|
||||
if editorMode then
|
||||
if EditorApp and EditorApp.joystickpressed then
|
||||
return EditorApp.joystickpressed(joystick, button)
|
||||
@@ -1071,6 +1092,7 @@ function love.joystickpressed(joystick, button)
|
||||
end
|
||||
|
||||
function love.joystickreleased(joystick, button)
|
||||
if launcherSplash then return end
|
||||
SwitchDiagnostics.onJoystickEvent("joystickreleased", joystick, button)
|
||||
if editorMode then
|
||||
if EditorApp and EditorApp.joystickreleased then
|
||||
@@ -1091,7 +1113,9 @@ function love.joystickreleased(joystick, button)
|
||||
end
|
||||
|
||||
function love.joystickaxis(joystick, axis, value)
|
||||
if launcherSplash then return end
|
||||
SwitchDiagnostics.onJoystickEvent("joystickaxis", joystick, axis, { value = value })
|
||||
if PadHints.windowMinimized() then value = 0 end
|
||||
if editorMode then
|
||||
if EditorApp and EditorApp.joystickaxis then
|
||||
return EditorApp.joystickaxis(joystick, axis, value)
|
||||
@@ -1111,7 +1135,9 @@ function love.joystickaxis(joystick, axis, value)
|
||||
end
|
||||
|
||||
function love.joystickhat(joystick, hat, direction)
|
||||
if launcherSplash then return end
|
||||
SwitchDiagnostics.onJoystickEvent("joystickhat", joystick, hat, { direction = direction })
|
||||
if PadHints.windowMinimized() then direction = "c" end
|
||||
if editorMode then
|
||||
if EditorApp and EditorApp.joystickhat then
|
||||
return EditorApp.joystickhat(joystick, hat, direction)
|
||||
@@ -1150,6 +1176,7 @@ end
|
||||
-- direction's key-up can be delivered to the OS instead of the game while
|
||||
-- unfocused, so reset input on either transition rather than trust it.
|
||||
function love.focus(f)
|
||||
SwitchDiagnostics.onFocus(f)
|
||||
if editorMode or TouchEditor then return end
|
||||
if Studio then
|
||||
if Studio.focus then Studio.focus(f) end
|
||||
@@ -1227,6 +1254,7 @@ function love.handlers.intent_uri(uri)
|
||||
end
|
||||
|
||||
function love.touchpressed(id, x, y, dx, dy, pressure)
|
||||
if launcherSplash then return end
|
||||
if editorMode then
|
||||
-- iOS synthesizes mousepressed for the primary touch; forwarding here
|
||||
-- would double-fire. Android / NX need the explicit touch → click path
|
||||
@@ -1256,6 +1284,7 @@ function love.touchpressed(id, x, y, dx, dy, pressure)
|
||||
end
|
||||
|
||||
function love.touchmoved(id, x, y, dx, dy, pressure)
|
||||
if launcherSplash then return end
|
||||
if editorMode then return end
|
||||
if TouchEditor then
|
||||
if love.system.getOS() == "iOS" then return end
|
||||
@@ -1270,6 +1299,7 @@ function love.touchmoved(id, x, y, dx, dy, pressure)
|
||||
end
|
||||
|
||||
function love.touchreleased(id, x, y, dx, dy, pressure)
|
||||
if launcherSplash then return end
|
||||
if editorMode then return end
|
||||
if TouchEditor then
|
||||
if love.system.getOS() == "iOS" then return end
|
||||
@@ -1284,6 +1314,7 @@ function love.touchreleased(id, x, y, dx, dy, pressure)
|
||||
end
|
||||
|
||||
function love.wheelmoved(x, y)
|
||||
if launcherSplash then return end
|
||||
if editorMode then
|
||||
if EditorApp.wheelmoved then return EditorApp.wheelmoved(x, y) end
|
||||
return
|
||||
@@ -1321,6 +1352,7 @@ if love.system and love.system.getOS() == "Linux"
|
||||
end
|
||||
|
||||
function love.mousepressed(x, y, button, istouch)
|
||||
if launcherSplash then return end
|
||||
if not istouch then eventMouseX, eventMouseY = x, y end
|
||||
if TouchEditor then
|
||||
-- Android primary touch already arrived via love.touchpressed; a second
|
||||
@@ -1368,6 +1400,7 @@ function love.mousepressed(x, y, button, istouch)
|
||||
end
|
||||
|
||||
function love.mousereleased(x, y, button, istouch)
|
||||
if launcherSplash then return end
|
||||
if TouchEditor then
|
||||
if love.system.getOS() == "Android" then return end
|
||||
return TouchEditor.mousereleased(x, y, button)
|
||||
@@ -1388,6 +1421,7 @@ function love.mousereleased(x, y, button, istouch)
|
||||
end
|
||||
|
||||
function love.mousemoved(x, y, dx, dy, istouch)
|
||||
if launcherSplash then return end
|
||||
if not istouch then eventMouseX, eventMouseY = x, y end
|
||||
if TouchEditor then
|
||||
if love.system.getOS() == "Android" then return end
|
||||
@@ -1406,6 +1440,7 @@ function love.mousemoved(x, y, dx, dy, istouch)
|
||||
end
|
||||
|
||||
function love.textinput(text)
|
||||
if launcherSplash then return end
|
||||
if TouchEditor then return end
|
||||
if Studio then return Studio.textinput(text) end
|
||||
if Importer then return Importer:textinput(text) end
|
||||
@@ -1421,6 +1456,7 @@ end
|
||||
local quitToLauncher = false
|
||||
|
||||
function love.quit()
|
||||
if launcherSplash then launcherSplash:release(); launcherSplash = nil end
|
||||
if Importer then
|
||||
require("src.import.LauncherWindow").observe(0)
|
||||
require("src.import.LauncherWindow").flush()
|
||||
@@ -1484,6 +1520,7 @@ function love.quit()
|
||||
end
|
||||
|
||||
function love.filedropped(file)
|
||||
if launcherSplash then return end
|
||||
local filename = file and file.getFilename and file:getFilename()
|
||||
if LaunchOptions.isLaunchURI(filename) then
|
||||
local request = LaunchOptions.parseURI(filename)
|
||||
|
||||
@@ -1 +1 @@
|
||||
{"mods":[],"schema_version":1}
|
||||
{"schema_version":1,"mods":[]}
|
||||
+41
-13
@@ -6,11 +6,9 @@ local FixedStep = {}
|
||||
|
||||
FixedStep.GB_HZ = 4194304 / 70224
|
||||
FixedStep.STEP = 1 / 60
|
||||
-- Hard ceiling on catch-up debt (seconds of logic time drained in one
|
||||
-- rendered frame). 0.25s = 15 GB steps. Game:update may lower this for the
|
||||
-- current speed target but must never raise it: a hitch at high multipliers
|
||||
-- cannot dump unbounded steps and starve input / spiral the frame.
|
||||
FixedStep.MAX_ACCUM = 0.25
|
||||
FixedStep.WORK_FRACTION = 0.75
|
||||
FixedStep.clock = nil
|
||||
local MAX_ACCUM = FixedStep.MAX_ACCUM
|
||||
local SMOOTH_FRAMES = 4
|
||||
local SMOOTH_MAX = 1 / 60 * 2.5
|
||||
@@ -69,35 +67,49 @@ function FixedStep:init(callback)
|
||||
self.suppressCatchup = false
|
||||
self.dtHistory, self.dtSum = nil, 0
|
||||
self.phasedFor = nil
|
||||
self.frameBreak = false
|
||||
end
|
||||
|
||||
-- The anti-spiral clamp is the steps-per-frame ceiling. Game:update sets
|
||||
-- maxAccum to the current speed's one-frame budget, hard-capped at MAX_ACCUM
|
||||
-- so high fast-forward cannot turn a hitch into input starvation.
|
||||
FixedStep.maxAccum = MAX_ACCUM
|
||||
|
||||
-- Compute the live catch-up ceiling for a logic-speed multiplier.
|
||||
-- Always ≤ MAX_ACCUM; at least two steps so ordinary vsync wobble can recover.
|
||||
function FixedStep.catchupLimit(speed)
|
||||
function FixedStep.catchupLimit(speed, dt)
|
||||
speed = tonumber(speed) or 1
|
||||
if speed < 1 then speed = 1 end
|
||||
local target = speed * FixedStep.STEP * 1.5
|
||||
local floor = FixedStep.STEP * 2
|
||||
local step = FixedStep.STEP
|
||||
local frame = tonumber(dt) or step
|
||||
if frame ~= frame or frame < step or frame > SMOOTH_MAX then frame = step end
|
||||
local target = speed * frame * 1.5
|
||||
local floor = step * 2
|
||||
if target < floor then target = floor end
|
||||
if target > MAX_ACCUM then target = MAX_ACCUM end
|
||||
return target
|
||||
end
|
||||
|
||||
local function workBudget(self, dt)
|
||||
local base = self.refreshPeriod or self.STEP
|
||||
local frame = dt
|
||||
if frame < base then frame = base end
|
||||
if frame > self.STEP * 2 then frame = math.max(base, self.STEP * 2) end
|
||||
return frame * FixedStep.WORK_FRACTION
|
||||
end
|
||||
|
||||
local function workClock()
|
||||
if FixedStep.clock then return FixedStep.clock end
|
||||
local timer = love and love.timer
|
||||
return timer and timer.getTime or nil
|
||||
end
|
||||
|
||||
function FixedStep:update(dt, speed)
|
||||
-- A hitch's oversized dt lands on the frame AFTER discardCatchup was
|
||||
-- called (the hitch itself already ran inside the current step); absorb
|
||||
-- that one frame as a single step instead of the normal accumulator so
|
||||
-- the burst it would otherwise release doesn't play out as a slide.
|
||||
self.frameBreak = false
|
||||
if self.suppressCatchup then
|
||||
self.suppressCatchup = false
|
||||
self.dtHistory, self.dtSum = nil, 0
|
||||
self.accum = self.STEP * RESEED_PHASE
|
||||
self.callback(self.STEP)
|
||||
self.frameBreak = false
|
||||
return
|
||||
end
|
||||
-- Snap/smooth against the wall-clock frame dt first. Game speed is a
|
||||
@@ -109,6 +121,7 @@ function FixedStep:update(dt, speed)
|
||||
-- Pathological wall-clock stalls: do not let dt alone exceed the catch-up
|
||||
-- ceiling before speed is applied (speed amplify would just hit the clamp).
|
||||
if dt > MAX_ACCUM then dt = MAX_ACCUM end
|
||||
local frameDt = dt
|
||||
local period = self.refreshPeriod
|
||||
local snapped = false
|
||||
if period and dt > 0 then
|
||||
@@ -140,12 +153,27 @@ function FixedStep:update(dt, speed)
|
||||
self.accum = self.accum - self.accum % self.STEP + target
|
||||
end
|
||||
self.accum = math.min(self.accum + dt * speed, self.maxAccum or MAX_ACCUM)
|
||||
local clock = speed > 1 and workClock() or nil
|
||||
local deadline = clock and (clock() + workBudget(self, frameDt)) or nil
|
||||
while self.accum >= self.STEP - STEP_EPS do
|
||||
self.accum = self.accum - self.STEP
|
||||
self.callback(self.STEP)
|
||||
if self.frameBreak then
|
||||
self.frameBreak = false
|
||||
self.accum = self.accum % self.STEP
|
||||
break
|
||||
end
|
||||
if deadline and self.accum >= self.STEP - STEP_EPS and clock() >= deadline then
|
||||
self.accum = self.accum % self.STEP
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function FixedStep:endFrame()
|
||||
self.frameBreak = true
|
||||
end
|
||||
|
||||
-- Drop any pending catch-up steps and arm the one-frame clamp above. A
|
||||
-- hitch inside one logic step (map seam setMap / song start) makes the
|
||||
-- next real-time dt huge; without this the while-loop above would advance
|
||||
|
||||
+29
-16
@@ -101,7 +101,10 @@ function Game:load(opts)
|
||||
-- apply the persisted audio + display options before anything plays
|
||||
self:applyOptions(self.save.options)
|
||||
|
||||
FixedStep:init(function(step) self:step(step) end)
|
||||
FixedStep:init(function(step)
|
||||
self:step(step)
|
||||
self:_speedLockEdge()
|
||||
end)
|
||||
self.fixedStep = FixedStep
|
||||
|
||||
local OverworldState = require("src.world.OverworldController")
|
||||
@@ -369,12 +372,7 @@ function Game:_resolveLogicSpeed()
|
||||
return GameSpeed.clamp(opts and opts[key] or GameSpeed.DEFAULT)
|
||||
end
|
||||
|
||||
-- The logic multiplier for this frame. Read live rather than cached so the
|
||||
-- Options rows take effect immediately; speedOverride is the --speed /
|
||||
-- POKEPORT_SPEED run argument, which wins over the saved option so a bot
|
||||
-- or screenshot run does not depend on whatever the player last chose.
|
||||
function Game:logicSpeed()
|
||||
local GameSpeed = require("src.core.GameSpeed")
|
||||
function Game:speedLocked()
|
||||
-- Link play is always 1X on both machines, and this wins over every other
|
||||
-- source including POKEPORT_SPEED and every per-category option.
|
||||
-- Fast-forward multiplies the logic clock, so a peer at 10X burned a
|
||||
@@ -384,11 +382,30 @@ function Game:logicSpeed()
|
||||
-- either player set this to -- checked here, before the core.logic_speed
|
||||
-- hook ever runs, so a mod cannot defeat it either.
|
||||
if self.linkSession or (self.linkNet and not self.linkNet.closed) then
|
||||
return 1
|
||||
return true, "link"
|
||||
end
|
||||
if Game.isFixedSpeedInStack and Game.isFixedSpeedInStack(self.stack) then
|
||||
return 1
|
||||
return true, "minigame"
|
||||
end
|
||||
if Game.speedCategoryInStack(self.stack) == "battle" then
|
||||
return true, "battle"
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function Game:_speedLockEdge()
|
||||
if (self._frameSpeed or 1) > 1 and self:speedLocked() then
|
||||
FixedStep:endFrame()
|
||||
end
|
||||
end
|
||||
|
||||
-- The logic multiplier for this frame. Read live rather than cached so the
|
||||
-- Options rows take effect immediately; speedOverride is the --speed /
|
||||
-- POKEPORT_SPEED run argument, which wins over the saved option so a bot
|
||||
-- or screenshot run does not depend on whatever the player last chose.
|
||||
function Game:logicSpeed()
|
||||
if self:speedLocked() then return 1 end
|
||||
local GameSpeed = require("src.core.GameSpeed")
|
||||
if self.speedOverride then return GameSpeed.clamp(self.speedOverride) end
|
||||
-- Clamp here too, not just in _resolveLogicSpeed's vanilla path: a mod's
|
||||
-- core.logic_speed hook can return anything (0, negative, nil, NaN) and
|
||||
@@ -404,7 +421,8 @@ function Game:update(dt)
|
||||
-- Give the accumulator room for one full frame at the current speed,
|
||||
-- or the anti-spiral clamp quietly caps every level above ~15X.
|
||||
local speed = self:logicSpeed()
|
||||
FixedStep.maxAccum = FixedStep.catchupLimit(speed)
|
||||
self._frameSpeed = speed
|
||||
FixedStep.maxAccum = FixedStep.catchupLimit(speed, dt)
|
||||
FixedStep:update(dt, speed)
|
||||
-- Audio runs off real time at a fixed 60Hz regardless of game speed or
|
||||
-- display refresh, so fades and chip synthesis keep their intended tempo
|
||||
@@ -783,6 +801,7 @@ end
|
||||
|
||||
function Game:_cycleSpeed(dir)
|
||||
if not (self.save and self.save.options) then return end
|
||||
if self:speedLocked() then return end
|
||||
local busy
|
||||
local ow = self.overworld
|
||||
if ow then
|
||||
@@ -794,12 +813,6 @@ function Game:_cycleSpeed(dir)
|
||||
or ow.engaging or ow.emote))
|
||||
end
|
||||
if busy then return end
|
||||
-- Cycles whichever category Game.speedCategoryInStack says is active
|
||||
-- right now (RFC 0007) -- pressing the hotkey during a battle speeds up
|
||||
-- just the battle, on the overworld just the walk, in a menu just the
|
||||
-- menu. A single physical control that means "speed up whatever I'm
|
||||
-- looking at right now" needs no new UI and matches what a player
|
||||
-- pressing it mid-battle almost certainly wants.
|
||||
local GameSpeed = require("src.core.GameSpeed")
|
||||
local key = GameSpeed.optionKey(Game.speedCategoryInStack(self.stack))
|
||||
self.save.options[key] = GameSpeed.cycle(self.save.options[key], dir)
|
||||
|
||||
+33
-3
@@ -1208,7 +1208,12 @@ function Game2:load(opts)
|
||||
self:showCopyright()
|
||||
end
|
||||
|
||||
local stepBody
|
||||
FixedStep:init(function(dt)
|
||||
stepBody(dt)
|
||||
self:_speedLockEdge()
|
||||
end)
|
||||
stepBody = function(dt)
|
||||
-- Tool mods (autoplay, accessibility drivers, input visualizers) act on the
|
||||
-- same fixed-step boundary a physical controller does. Raised HERE, ahead
|
||||
-- of both the AUTO_INPUT arm and Input:step, for the reason Gen 1 raises it
|
||||
@@ -1270,7 +1275,7 @@ function Game2:load(opts)
|
||||
self.world:interact()
|
||||
end
|
||||
self.world:step()
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
function Game2:inFillBoot()
|
||||
@@ -1279,7 +1284,29 @@ function Game2:inFillBoot()
|
||||
return self.phase == "boot" and self.stack:top() ~= nil
|
||||
end
|
||||
|
||||
function Game2:speedLocked()
|
||||
if self.linkSession or (self.linkNet and not self.linkNet.closed) then
|
||||
return true, "link"
|
||||
end
|
||||
local states = self.stack and self.stack.states
|
||||
for i = #(states or {}), 1, -1 do
|
||||
local state = states[i]
|
||||
if state and state.isBattle then return true, "battle" end
|
||||
if state and (state.isFixedSpeed or state.isMinigame) then
|
||||
return true, "minigame"
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function Game2:_speedLockEdge()
|
||||
if (self._frameSpeed or 1) > 1 and self:speedLocked() then
|
||||
FixedStep:endFrame()
|
||||
end
|
||||
end
|
||||
|
||||
function Game2:logicSpeed()
|
||||
if self:speedLocked() then return 1 end
|
||||
return math.max(1,
|
||||
tonumber(self.speedOverride) or tonumber(self.options and self.options.speed)
|
||||
or 1)
|
||||
@@ -1310,13 +1337,14 @@ function Game2:update(dt)
|
||||
-- driver/CLI hook and wins over the saved option.
|
||||
-- pokegold engine/menus/intro_menu.asm:848 IntroSequence: boot cinema runs on the same clock as the overworld
|
||||
local speed = self:logicSpeed()
|
||||
self._frameSpeed = speed
|
||||
if self.phase == "boot" then
|
||||
FixedStep.maxAccum = FixedStep.catchupLimit(speed)
|
||||
FixedStep.maxAccum = FixedStep.catchupLimit(speed, dt)
|
||||
FixedStep:update(dt, speed)
|
||||
return
|
||||
end
|
||||
if not self.world or not self.world.map then return end
|
||||
FixedStep.maxAccum = FixedStep.catchupLimit(speed)
|
||||
FixedStep.maxAccum = FixedStep.catchupLimit(speed, dt)
|
||||
FixedStep:update(dt, speed)
|
||||
end
|
||||
|
||||
@@ -2057,6 +2085,7 @@ function Game2:hotkey(key)
|
||||
if loaded then self:continueGame(loaded) end
|
||||
return true
|
||||
elseif hk == "1" then
|
||||
if self:speedLocked() then return true end
|
||||
local GameSpeed = require("src.core.GameSpeed")
|
||||
options.speed = GameSpeed.cycle(options.speed, 1)
|
||||
persist()
|
||||
@@ -2391,6 +2420,7 @@ function Game2:applyOptions()
|
||||
end
|
||||
|
||||
function Game2:_cycleSpeed(dir)
|
||||
if self:speedLocked() then return end
|
||||
local GameSpeed = require("src.core.GameSpeed")
|
||||
self.options.speed = GameSpeed.cycle(self.options.speed, dir)
|
||||
if self.save then self.save.options = self.options end
|
||||
|
||||
+52
-3
@@ -185,6 +185,7 @@ function Game3:load(opts)
|
||||
if opts.arena then
|
||||
FixedStep:init(function(dt)
|
||||
self:fixedUpdate(dt)
|
||||
self:_speedLockEdge()
|
||||
end)
|
||||
pcall(function()
|
||||
require("src.core.PresentSync").applyFixedStepPeriod()
|
||||
@@ -208,6 +209,7 @@ function Game3:load(opts)
|
||||
|
||||
FixedStep:init(function(dt)
|
||||
self:fixedUpdate(dt)
|
||||
self:_speedLockEdge()
|
||||
end)
|
||||
pcall(function()
|
||||
require("src.core.PresentSync").applyFixedStepPeriod()
|
||||
@@ -414,7 +416,7 @@ function Game3:_handleRegisteredItem()
|
||||
if not item then return end
|
||||
-- src/item_menu.c:2025
|
||||
local Map = package.loaded["src.core.game3.map"]
|
||||
if Map and Map.current == "FR_UNION_ROOM" then return end
|
||||
if Map and require("src.core.game3.link.union_room").isUnionMap(Map.current) then return end
|
||||
-- src/overworld.c:2813
|
||||
local Link = package.loaded["src.core.game3.link"]
|
||||
if type(Link) == "table" and Link.link ~= nil and Link.inLinkRoom() == true then return end
|
||||
@@ -585,8 +587,53 @@ function Game3:isFixedSpeed()
|
||||
return type(MG) == "table" and MG.isActive ~= nil and MG.isActive() == true
|
||||
end
|
||||
|
||||
local function unionRoomMap(id)
|
||||
return require("src.core.game3.link.union_room").isUnionMap(id)
|
||||
end
|
||||
|
||||
function Game3:speedLocked()
|
||||
if self:isFixedSpeed() then return true, "link" end
|
||||
local Battle = package.loaded["src.core.game3.battle"]
|
||||
if type(Battle) == "table" and Battle.isActive and Battle.isActive() then
|
||||
return true, "battle"
|
||||
end
|
||||
local Transition = package.loaded["src.core.game3.battle_transition"]
|
||||
if type(Transition) == "table" and Transition.isActive and Transition.isActive() then
|
||||
return true, "battle"
|
||||
end
|
||||
local Union = package.loaded["src.core.game3.link.union_room"]
|
||||
if type(Union) == "table" and Union.isActive and Union.isActive() then
|
||||
return true, "link"
|
||||
end
|
||||
local LinkMenu = package.loaded["src.ui.game3.link_menu"]
|
||||
if type(LinkMenu) == "table" then
|
||||
if LinkMenu.isOpen and LinkMenu.isOpen() then return true, "link" end
|
||||
local Direct = LinkMenu.Direct
|
||||
if type(Direct) == "table" and Direct.isOpen and Direct.isOpen() then
|
||||
return true, "link"
|
||||
end
|
||||
end
|
||||
if self.phase ~= "field" then return false end
|
||||
local Map = package.loaded["src.core.game3.map"]
|
||||
if type(Map) == "table" and unionRoomMap(Map.current) then
|
||||
return true, "link"
|
||||
end
|
||||
local Link = package.loaded["src.core.game3.link"]
|
||||
if type(Link) == "table" and Link.inLinkRoom then
|
||||
local ok, inRoom = pcall(Link.inLinkRoom)
|
||||
if ok and inRoom == true then return true, "link" end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function Game3:_speedLockEdge()
|
||||
if (self._frameSpeed or 1) > 1 and self:speedLocked() then
|
||||
FixedStep:endFrame()
|
||||
end
|
||||
end
|
||||
|
||||
function Game3:logicSpeed()
|
||||
if self:isFixedSpeed() then return 1 end
|
||||
if self:speedLocked() then return 1 end
|
||||
local override = tonumber(self.speedOverride)
|
||||
if override then return math.max(1, override) end
|
||||
local b = self.phase == "boot" and self.boot
|
||||
@@ -603,6 +650,7 @@ end
|
||||
|
||||
function Game3:_cycleSpeed(dir)
|
||||
if type(self.options) ~= "table" then return end
|
||||
if self:speedLocked() then return end
|
||||
local GameSpeed = require("src.core.GameSpeed")
|
||||
local key = GameSpeed.optionKey(self:speedCategory())
|
||||
self.options[key] = GameSpeed.cycle(self.options[key], dir)
|
||||
@@ -633,7 +681,8 @@ end
|
||||
|
||||
function Game3:update(dt)
|
||||
local speed = self:logicSpeed()
|
||||
FixedStep.maxAccum = FixedStep.catchupLimit(speed)
|
||||
self._frameSpeed = speed
|
||||
FixedStep.maxAccum = FixedStep.catchupLimit(speed, dt)
|
||||
FixedStep:update(dt, speed)
|
||||
self._audioAccum = (self._audioAccum or 0) + dt
|
||||
local STEP = 1 / 60
|
||||
|
||||
+6
-1
@@ -2,6 +2,7 @@
|
||||
-- `down` = held this frame; `pressed` = edge, consumed per fixed step.
|
||||
|
||||
local GamepadMap = require("src.core.GamepadMap")
|
||||
local PadHints = require("src.core.PadHints")
|
||||
|
||||
local Input = {}
|
||||
|
||||
@@ -361,16 +362,20 @@ function Input:pollPads()
|
||||
self.padSuppress = suppress
|
||||
end
|
||||
local backDown = nil
|
||||
local minimized = nil
|
||||
for i = 1, #list do
|
||||
local e = list[i]
|
||||
local down = anyPadDown(pads, e.button)
|
||||
local sources = self.sources[e.btn]
|
||||
local held = sources ~= nil and sources[e.source] == true
|
||||
if not down then suppress[e.button] = nil end
|
||||
if down and not held and minimized == nil then
|
||||
minimized = PadHints.windowMinimized()
|
||||
end
|
||||
if held and not down then
|
||||
release(self, e.btn, e.source)
|
||||
notePadRepair("release", e.button)
|
||||
elseif down and not held and not mute and not suppress[e.button]
|
||||
elseif down and not held and not mute and not minimized and not suppress[e.button]
|
||||
and not self.padActions[e.button] then
|
||||
if e.chord and backDown == nil then
|
||||
backDown = self.state.select == true or anyPadDown(pads, "back")
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
local PadHints = {}
|
||||
|
||||
PadHints.BACKGROUND_HINT = "SDL_JOYSTICK_ALLOW_BACKGROUND_EVENTS"
|
||||
|
||||
function PadHints.apply(osName, ffiLike, getenv)
|
||||
if osName ~= "Windows" then return false end
|
||||
getenv = getenv or os.getenv
|
||||
local okEnv, flag = pcall(getenv, "POKEPORT_PAD_BACKGROUND")
|
||||
if okEnv and flag == "0" then return false end
|
||||
local ok, set = pcall(function()
|
||||
local ffi = ffiLike or require("ffi")
|
||||
pcall(ffi.cdef, "int SDL_SetHint(const char *name, const char *value);")
|
||||
local sdl = ffi.load("SDL2")
|
||||
return sdl.SDL_SetHint(PadHints.BACKGROUND_HINT, "1")
|
||||
end)
|
||||
return ok and set ~= nil and set ~= 0
|
||||
end
|
||||
|
||||
function PadHints.windowMinimized()
|
||||
local win = love and love.window
|
||||
if not win then return false end
|
||||
if win.isMinimized then
|
||||
local ok, v = pcall(win.isMinimized)
|
||||
if ok then return v == true end
|
||||
end
|
||||
if win.isVisible then
|
||||
local ok, v = pcall(win.isVisible)
|
||||
if ok then return v == false end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function PadHints.hasFocus()
|
||||
local win = love and love.window
|
||||
if not (win and win.hasFocus) then return nil end
|
||||
local ok, v = pcall(win.hasFocus)
|
||||
if ok then return v end
|
||||
return nil
|
||||
end
|
||||
|
||||
local fgReady = nil
|
||||
|
||||
function PadHints.foreground(osName, ffiLike)
|
||||
if osName ~= "Windows" then return nil end
|
||||
if fgReady == false then return nil end
|
||||
local ok, owner = pcall(function()
|
||||
local ffi = ffiLike or require("ffi")
|
||||
if not fgReady then
|
||||
pcall(ffi.cdef, [[
|
||||
void *GetForegroundWindow(void);
|
||||
unsigned long GetWindowThreadProcessId(void *hwnd, unsigned long *pid);
|
||||
unsigned long GetCurrentProcessId(void);
|
||||
]])
|
||||
fgReady = true
|
||||
end
|
||||
local hwnd = ffi.C.GetForegroundWindow()
|
||||
if hwnd == nil then return "none" end
|
||||
local pid = ffi.new("unsigned long[1]")
|
||||
ffi.C.GetWindowThreadProcessId(hwnd, pid)
|
||||
return pid[0] == ffi.C.GetCurrentProcessId() and "ours" or "other"
|
||||
end)
|
||||
if not ok then
|
||||
fgReady = false
|
||||
return nil
|
||||
end
|
||||
return owner
|
||||
end
|
||||
|
||||
function PadHints._resetForTests()
|
||||
fgReady = nil
|
||||
end
|
||||
|
||||
return PadHints
|
||||
@@ -8,8 +8,6 @@
|
||||
-- unknown rates fail closed.
|
||||
-- 3. Fallback cascade — ungated / failed / abandoned waits force FrameCap
|
||||
-- immediately; logic never snaps to panel Hz unless sync is confirmed.
|
||||
-- 4. (FixedStep) catch-up debt is hard-capped so speed multipliers cannot
|
||||
-- spiral into input-starving multi-frame dumps.
|
||||
|
||||
local PresentSync = {}
|
||||
|
||||
|
||||
+15
-6
@@ -1163,11 +1163,8 @@ function SaveData.slotSummary(save)
|
||||
local gen3 = save.generation == 3 or (vinfo and vinfo.generation == 3) or (save.engine == "game3") or false
|
||||
local dexCount = 0
|
||||
if gen3 then
|
||||
local dex = save.dex or save.pokedex or {}
|
||||
local owned = dex.owned or dex.caught or {}
|
||||
for _, has in pairs(owned) do
|
||||
if has then dexCount = dexCount + 1 end
|
||||
end
|
||||
local okD, Dex = pcall(require, "src.core.game3.dex")
|
||||
dexCount = okD and Dex and Dex.summaryCount and Dex.summaryCount(save) or 0
|
||||
elseif gen2 then
|
||||
for _, has in pairs((save.pokedex and save.pokedex.caught) or {}) do
|
||||
if has then dexCount = dexCount + 1 end
|
||||
@@ -1422,7 +1419,18 @@ local function writeSlotIn(key, slotId, saveTable)
|
||||
end
|
||||
if type(saveTable) ~= "table" then return false, "missing save table" end
|
||||
local main, bak, tmp = slotNames(key, slotId)
|
||||
local encoded = SaveSerializer.encode(saveTable)
|
||||
local vinfo = type(saveTable.version) == "string" and GameVersion.info(saveTable.version)
|
||||
or type(key) == "string" and GameVersion.info(key)
|
||||
local gen3 = saveTable.engine == "game3" or saveTable.generation == 3
|
||||
or (vinfo and vinfo.generation == 3) or false
|
||||
local body = saveTable
|
||||
if not gen3 and saveTable.options ~= nil then
|
||||
body = {}
|
||||
for k, v in pairs(saveTable) do
|
||||
if k ~= "options" then body[k] = v end
|
||||
end
|
||||
end
|
||||
local encoded = SaveSerializer.encode(body)
|
||||
local fs = persistFs(nil)
|
||||
ensureParentDir(fs, main)
|
||||
if fs.getInfo(main) then
|
||||
@@ -2003,6 +2011,7 @@ function SaveData.buildMeta(mods, previous, sessionStart)
|
||||
cartHash = type(previous) == "table" and previous.cartHash or nil,
|
||||
sealBroken = (type(previous) == "table" and previous.sealBroken == true) or nil,
|
||||
mods = list,
|
||||
modCount = #list,
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -1949,7 +1949,12 @@ function Engine.performSwitch(st, adapter, side, slot, opts)
|
||||
end
|
||||
local foe = (side == "player") and st.enemy or st.player
|
||||
if side == "player" then
|
||||
State.trackParticipant(st, st.enemy, slot)
|
||||
if opts.isShift or opts.reason == "shift" then
|
||||
-- pokefirered/src/battle_script_commands.c:5945
|
||||
State.resetSentPokes(st)
|
||||
else
|
||||
State.trackParticipant(st, st.enemy, slot)
|
||||
end
|
||||
elseif st.player then
|
||||
-- pokefirered/src/battle_util.c:254
|
||||
State.opponentSwitchInResetSentPokes(st, nb)
|
||||
|
||||
@@ -1219,8 +1219,7 @@ local function refuse_link_item(input)
|
||||
if not (input and st and st.link) then return false end
|
||||
if Ui._mode ~= "menu" or not input:wasPressed("a") then return false end
|
||||
if Commands.MENU[Ui._menuIndex or 1] ~= "BAG" then return false end
|
||||
-- pokefirered/src/battle_message.c:251
|
||||
Ui.push(BattleText.get("STRINGID_ITEMSCANTBEUSEDNOW"))
|
||||
Ui.refuseItems()
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
@@ -159,8 +159,9 @@ local function switch_in_text(st, battler)
|
||||
return BattleText.get(BattleText.SWITCHINMON, SwitchSeq.switchInFill(st, battler))
|
||||
end
|
||||
|
||||
local function apply_entry_triggers(st, side, pushMsg)
|
||||
local function apply_entry_triggers(st, side, pushMsg, opts)
|
||||
if not st then return end
|
||||
opts = opts or {}
|
||||
local b = st[side]
|
||||
if not b or not b.mon or (tonumber(b.mon.hp) or 0) <= 0 then return end
|
||||
|
||||
@@ -168,7 +169,7 @@ local function apply_entry_triggers(st, side, pushMsg)
|
||||
local sideState = (side == "player") and st.playerSide or st.enemySide
|
||||
local hazards = sideState and sideState.hazards or {}
|
||||
local spikesLayers = tonumber(hazards.spikes or hazards.SPIKES) or 0
|
||||
if spikesLayers > 0 and b and b.mon then
|
||||
if not opts.noSpikes and spikesLayers > 0 and b and b.mon then
|
||||
local isFlying = (b.type1 == 2 or b.type2 == 2 or b.type1 == "FLYING" or b.type2 == "FLYING")
|
||||
local hasLevitate = (b.ability == "LEVITATE" or b.ability == 26)
|
||||
if not isFlying and not hasLevitate then
|
||||
@@ -186,7 +187,7 @@ local function apply_entry_triggers(st, side, pushMsg)
|
||||
end
|
||||
|
||||
-- Ability: Intimidate (ability 22 or "INTIMIDATE")
|
||||
if (b.ability == 22 or b.ability == "INTIMIDATE") and (tonumber(b.mon.hp) or 0) > 0 then
|
||||
if not opts.noIntimidate and (b.ability == 22 or b.ability == "INTIMIDATE") and (tonumber(b.mon.hp) or 0) > 0 then
|
||||
local oppSide = (side == "player") and "enemy" or "player"
|
||||
local opp = st[oppSide]
|
||||
if opp and opp.mon and (tonumber(opp.mon.hp) or 0) > 0 then
|
||||
@@ -232,24 +233,45 @@ local function switch_out_effects(st, battler)
|
||||
capture_events(function(ad) Engine.switchOutEffects(st, ad, battler) end)
|
||||
end
|
||||
|
||||
local function fallback_entry(st, sides, pushMsg, opts)
|
||||
opts = opts or {}
|
||||
local deferred = opts.deferred or {}
|
||||
if not opts.deferIntimidate and #deferred == 0 then
|
||||
for _, side in ipairs(sides) do apply_entry_triggers(st, side, pushMsg) end
|
||||
return
|
||||
end
|
||||
for _, side in ipairs(sides) do apply_entry_triggers(st, side, pushMsg, { noIntimidate = true }) end
|
||||
if opts.deferIntimidate then return end
|
||||
for _, side in ipairs(deferred) do apply_entry_triggers(st, side, pushMsg, { noSpikes = true }) end
|
||||
for _, side in ipairs(sides) do apply_entry_triggers(st, side, pushMsg, { noSpikes = true }) end
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_script_commands.c:4960
|
||||
local function engine_entry_events(st, sides)
|
||||
local function engine_entry_events(st, sides, opts)
|
||||
opts = opts or {}
|
||||
local Engine = package.loaded["src.core.game3.battle.engine"]
|
||||
if not (Engine and Engine.switchInEffects and battle_adapter()) then return nil end
|
||||
return capture_events(function(ad)
|
||||
for _, side in ipairs(sides) do
|
||||
local b = st and ((type(side) == "number") and State.battler(st, side) or st[side])
|
||||
if b and b.mon and (tonumber(b.mon.hp) or 0) > 0 then
|
||||
Engine.switchInEffects(st, ad, b, { spikes = true })
|
||||
Engine.switchInEffects(st, ad, b, { spikes = true, deferIntimidate = opts.deferIntimidate })
|
||||
end
|
||||
end
|
||||
if opts.deferred and not opts.deferIntimidate then
|
||||
local Abilities = require("src.core.game3.battle.abilities")
|
||||
-- pokefirered/src/battle_util.c:1209
|
||||
for _ = 1, 4 do
|
||||
if not (Abilities.runIntimidate(ad) or Abilities.runTrace(ad)) then break end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local function headless_entry(st, sides)
|
||||
local evs = engine_entry_events(st, sides)
|
||||
local function headless_entry(st, sides, opts)
|
||||
local evs = engine_entry_events(st, sides, opts)
|
||||
if evs == nil then
|
||||
for _, side in ipairs(sides) do apply_entry_triggers(st, side, SwitchSeq._pushMsg) end
|
||||
fallback_entry(st, sides, SwitchSeq._pushMsg, opts)
|
||||
return
|
||||
end
|
||||
for _, e in ipairs(evs) do
|
||||
@@ -442,7 +464,7 @@ function SwitchSeq.beginEventSwitchIn(st, side, opts)
|
||||
return true
|
||||
end
|
||||
|
||||
--- Retail Shift sequence: Player recalls active mon -> Enemy sends out replacement FIRST -> Player sends out replacement SECOND.
|
||||
-- pokefirered/data/battle_scripts_1.s:2856
|
||||
function SwitchSeq.beginShiftSwitch(st, playerSlot, enemySlot, opts)
|
||||
opts = opts or {}
|
||||
SwitchSeq.reset()
|
||||
@@ -459,38 +481,39 @@ function SwitchSeq.beginShiftSwitch(st, playerSlot, enemySlot, opts)
|
||||
switch_out_effects(st, oldBattler)
|
||||
State.syncBattlerToParty(st.player, st.playerParty)
|
||||
State.wipeVolatilesAndStages(st.player)
|
||||
st.enemy = State.makeBattler(st.foeParty[enemySlot], "enemy", { partyIndex = enemySlot, st = st })
|
||||
if SwitchSeq._pushMsg then SwitchSeq._pushMsg(switch_in_text(st, st.enemy)) end
|
||||
st.player = State.makeBattler(st.playerParty[playerSlot], "player", { partyIndex = playerSlot, st = st, state = st })
|
||||
State.opponentSwitchInResetSentPokes(st, st.enemy)
|
||||
State.trackParticipant(st, st.enemy, playerSlot)
|
||||
Anim.syncDisplayFromState(st)
|
||||
if SwitchSeq._pushMsg then SwitchSeq._pushMsg(switch_in_text(st, st.player)) end
|
||||
headless_entry(st, { "enemy", "player" })
|
||||
headless_entry(st, { "player" }, { deferIntimidate = true })
|
||||
-- pokefirered/src/battle_script_commands.c:5945
|
||||
State.resetSentPokes(st)
|
||||
st.enemy = State.makeBattler(st.foeParty[enemySlot], "enemy", { partyIndex = enemySlot, st = st })
|
||||
-- pokefirered/src/battle_util.c:254
|
||||
State.opponentSwitchInResetSentPokes(st, st.enemy)
|
||||
Anim.syncDisplayFromState(st)
|
||||
if SwitchSeq._pushMsg then SwitchSeq._pushMsg(switch_in_text(st, st.enemy)) end
|
||||
headless_entry(st, { "enemy" }, { deferred = { "player" } })
|
||||
finish()
|
||||
return false
|
||||
end
|
||||
|
||||
local steps = {
|
||||
-- 1. Player recall
|
||||
{ kind = "msg", data = { text = withdrawMsg } },
|
||||
{ kind = "withdraw", data = { side = "player" } },
|
||||
-- 2. Enemy sendout first (retail FRLG order)
|
||||
{ kind = "swap_data", data = { side = "enemy", newSlot = enemySlot, isShift = true, reason = "shift" } },
|
||||
{ kind = "msg_sendout", data = { side = "enemy" } },
|
||||
{ kind = "sendout_enemy", data = { slot = enemySlot } },
|
||||
{ kind = "shiny_check", data = { side = "enemy" } },
|
||||
{ kind = "cry", data = { side = "enemy" } },
|
||||
{ kind = "healthbox", data = { side = "enemy" } },
|
||||
-- 3. Player sendout second
|
||||
{ kind = "swap_data", data = { side = "player", newSlot = playerSlot, isShift = true, reason = "shift" } },
|
||||
{ kind = "msg_sendout", data = { side = "player" } },
|
||||
{ kind = "sendout_player", data = { slot = playerSlot } },
|
||||
{ kind = "shiny_check", data = { side = "player" } },
|
||||
{ kind = "cry", data = { side = "player" } },
|
||||
{ kind = "healthbox", data = { side = "player" } },
|
||||
-- 4. Entry abilities and hazards in speed order
|
||||
{ kind = "entry_triggers", data = { sides = { "enemy", "player" } } },
|
||||
{ kind = "entry_triggers", data = { side = "player", deferIntimidate = true } },
|
||||
-- pokefirered/data/battle_scripts_1.s:2874
|
||||
{ kind = "swap_data", data = { side = "enemy", newSlot = enemySlot, reason = "shift" } },
|
||||
{ kind = "msg_sendout", data = { side = "enemy" } },
|
||||
{ kind = "sendout_enemy", data = { slot = enemySlot } },
|
||||
{ kind = "shiny_check", data = { side = "enemy" } },
|
||||
{ kind = "cry", data = { side = "enemy" } },
|
||||
{ kind = "healthbox", data = { side = "enemy" } },
|
||||
{ kind = "entry_triggers", data = { side = "enemy", deferred = { "player" } } },
|
||||
}
|
||||
|
||||
SwitchSeq._steps = steps
|
||||
@@ -591,16 +614,22 @@ local function run_step(step)
|
||||
return
|
||||
end
|
||||
switch_out_effects(st, st and st[side])
|
||||
local isShift = d.isShift or d.reason == "shift"
|
||||
if side == "player" then
|
||||
if st and st.player then
|
||||
if not d.isShift and d.reason ~= "shift" then
|
||||
if not isShift then
|
||||
State.trackParticipant(st, st.enemy, st.player.partyIndex or 1)
|
||||
end
|
||||
State.syncBattlerToParty(st.player, st.playerParty)
|
||||
State.wipeVolatilesAndStages(st.player, { batonPass = d.batonPass })
|
||||
end
|
||||
st.player = State.makeBattler(st.playerParty[newSlot], "player", { partyIndex = newSlot, st = st })
|
||||
State.trackParticipant(st, st.enemy, newSlot)
|
||||
if isShift then
|
||||
-- pokefirered/src/battle_script_commands.c:5945
|
||||
State.resetSentPokes(st)
|
||||
else
|
||||
State.trackParticipant(st, st.enemy, newSlot)
|
||||
end
|
||||
else
|
||||
if st and st.enemy then
|
||||
State.syncBattlerToParty(st.enemy, st.foeParty)
|
||||
@@ -608,9 +637,6 @@ local function run_step(step)
|
||||
end
|
||||
st.enemy = State.makeBattler(st.foeParty[newSlot], "enemy", { partyIndex = newSlot, st = st, state = st })
|
||||
State.opponentSwitchInResetSentPokes(st, st.enemy)
|
||||
if d.isShift or d.reason == "shift" then
|
||||
st.enemy.participants = {}
|
||||
end
|
||||
end
|
||||
Anim.syncDisplayFromState(st)
|
||||
advance()
|
||||
@@ -753,11 +779,10 @@ local function run_step(step)
|
||||
end)
|
||||
sides = sorted
|
||||
end
|
||||
local evs = engine_entry_events(st, sides)
|
||||
local entryOpts = { deferIntimidate = d.deferIntimidate, deferred = d.deferred }
|
||||
local evs = engine_entry_events(st, sides, entryOpts)
|
||||
if evs == nil then
|
||||
for _, sSide in ipairs(sides) do
|
||||
apply_entry_triggers(st, sSide, SwitchSeq._pushMsg)
|
||||
end
|
||||
fallback_entry(st, sides, SwitchSeq._pushMsg, entryOpts)
|
||||
elseif #evs > 0 then
|
||||
local AnimSeq = require("src.core.game3.battle.anim_seq")
|
||||
local Ui = require("src.core.game3.battle.ui")
|
||||
|
||||
@@ -435,7 +435,16 @@ local function restore_action_menu()
|
||||
if Message and Message.open then Message.reset() end
|
||||
end
|
||||
|
||||
-- pokefirered/src/battle_main.c:3182 BattleScript_ActionSelectionItemsCantBeUsed
|
||||
function Ui.refuseItems()
|
||||
Ui._selCmd = nil
|
||||
Ui._selReturn = "menu"
|
||||
Ui._mode = "selmsg"
|
||||
Ui.push(BattleText.get("STRINGID_ITEMSCANTBEUSEDNOW"))
|
||||
end
|
||||
|
||||
local function open_battle_bag()
|
||||
if Ui._st and Ui._st.link then return Ui.refuseItems() end
|
||||
local BagMenu = require("src.ui.game3.bag_menu")
|
||||
local Runtime = package.loaded["src.core.game3.runtime"]
|
||||
local session = Ui._session
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
-- Built from mapDef.blocks + tileset.collision (Gen2 COLL_* quads baked from
|
||||
-- FRLG metatile attrs at extract). Does not call World:step / Player:tryMove.
|
||||
|
||||
local Connections = require("src.core.game3.connections")
|
||||
local Collision = {}
|
||||
|
||||
local DELTA = {
|
||||
@@ -582,8 +583,7 @@ function Collision.connectionLanding(destDef, conn, dir, fromCx, fromCy)
|
||||
else
|
||||
return nil
|
||||
end
|
||||
x = math.max(0, math.min(destW - 1, x))
|
||||
y = math.max(0, math.min(destH - 1, y))
|
||||
if x < 0 or y < 0 or x >= destW or y >= destH then return nil end
|
||||
return x, y
|
||||
end
|
||||
|
||||
@@ -614,17 +614,17 @@ function Collision.tryConnection(game, fromX, fromY, dir, run)
|
||||
|
||||
local mapDef = Collision._mapDef
|
||||
if not mapDef or type(mapDef.connections) ~= "table" then return false end
|
||||
local conn = mapDef.connections[dir]
|
||||
or mapDef.connections[DIR_CONN[dir] or ""]
|
||||
if not conn then return false end
|
||||
local destMap = conn.map or conn.mapId
|
||||
if type(destMap) ~= "string" then return false end
|
||||
|
||||
local data = game and game.data and game.data.maps
|
||||
local destDef = data and data[destMap]
|
||||
if not destDef then return false end
|
||||
if not data then return false end
|
||||
local Map = require("src.core.game3.map")
|
||||
if Map.ensureMidLayout then Map.ensureMidLayout(game, destMap, destDef) end
|
||||
-- pokefirered/src/fieldmap.c:673
|
||||
local conn, destDef = Connections.incoming(mapDef, DIR_CONN[dir], fromX, fromY, function(id)
|
||||
local def = data[id]
|
||||
if def and Map.ensureMidLayout then Map.ensureMidLayout(game, id, def) end
|
||||
return def
|
||||
end)
|
||||
if not conn then return false end
|
||||
local destMap = conn.map
|
||||
|
||||
local lx, ly = Collision.connectionLanding(destDef, conn, dir, fromX, fromY)
|
||||
if not lx then return false end
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
local Connections = {}
|
||||
|
||||
local CARDINAL = {
|
||||
north = "north", south = "south", west = "west", east = "east",
|
||||
up = "north", down = "south", left = "west", right = "east",
|
||||
}
|
||||
|
||||
function Connections.cardinal(dir)
|
||||
return CARDINAL[dir]
|
||||
end
|
||||
|
||||
local function entry(dir, c)
|
||||
local d = CARDINAL[dir]
|
||||
local map = type(c) == "table" and (c.map or c.mapId) or c
|
||||
if not d or type(map) ~= "string" then return nil end
|
||||
return { dir = d, map = map, offset = type(c) == "table" and tonumber(c.offset) or 0 }
|
||||
end
|
||||
|
||||
function Connections.each(def)
|
||||
local out = {}
|
||||
local conns = def and def.connections
|
||||
if type(conns) ~= "table" then return out end
|
||||
local n = #conns
|
||||
for i = 1, n do
|
||||
local c = conns[i]
|
||||
local e = type(c) == "table" and entry(c.dir or c.direction, c)
|
||||
if e then out[#out + 1] = e end
|
||||
end
|
||||
local keyed = {}
|
||||
for k, c in pairs(conns) do
|
||||
if type(k) == "string" then
|
||||
local e = entry(k, c)
|
||||
if e then keyed[#keyed + 1] = { k = k, e = e } end
|
||||
end
|
||||
end
|
||||
table.sort(keyed, function(a, b) return a.k < b.k end)
|
||||
for _, kv in ipairs(keyed) do out[#out + 1] = kv.e end
|
||||
return out
|
||||
end
|
||||
|
||||
function Connections.sizeOf(def)
|
||||
local L = def and def.midLayout
|
||||
if L and L.width and L.height then return L.width, L.height end
|
||||
return (tonumber(def and def.width) or 0) * 2, (tonumber(def and def.height) or 0) * 2
|
||||
end
|
||||
|
||||
local function vertical(dir)
|
||||
return dir == "north" or dir == "south"
|
||||
end
|
||||
|
||||
-- pokefirered/src/fieldmap.c:724
|
||||
local function coordInIncoming(coord, srcMax, destMax, offset)
|
||||
local lo = math.max(offset, 0)
|
||||
if destMax + offset < srcMax then srcMax = destMax + offset end
|
||||
return lo <= coord and coord <= srcMax
|
||||
end
|
||||
|
||||
-- pokefirered/src/fieldmap.c:686
|
||||
function Connections.incoming(def, dir, x, y, destDefOf)
|
||||
dir = CARDINAL[dir]
|
||||
if not dir then return nil end
|
||||
local srcW, srcH = Connections.sizeOf(def)
|
||||
for _, c in ipairs(Connections.each(def)) do
|
||||
if c.dir == dir then
|
||||
local destDef = destDefOf(c.map)
|
||||
if destDef then
|
||||
local w, h = Connections.sizeOf(destDef)
|
||||
local hit
|
||||
if vertical(dir) then
|
||||
hit = coordInIncoming(x, srcW, w, c.offset)
|
||||
else
|
||||
hit = coordInIncoming(y, srcH, h, c.offset)
|
||||
end
|
||||
if hit then return c, destDef end
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- pokefirered/src/fieldmap.c:745
|
||||
function Connections.localPos(n, x, y, srcW, srcH)
|
||||
local w, h = Connections.sizeOf(n.def)
|
||||
local off = tonumber(n.offset) or 0
|
||||
local lx, ly
|
||||
if n.dir == "north" then
|
||||
lx, ly = x - off, h + y
|
||||
elseif n.dir == "south" then
|
||||
lx, ly = x - off, y - srcH
|
||||
elseif n.dir == "west" then
|
||||
lx, ly = w + x, y - off
|
||||
elseif n.dir == "east" then
|
||||
lx, ly = x - srcW, y - off
|
||||
else
|
||||
return nil
|
||||
end
|
||||
local along = vertical(n.dir) and lx or ly
|
||||
local span = vertical(n.dir) and w or h
|
||||
if along < 0 or along >= span then return nil end
|
||||
return lx, ly
|
||||
end
|
||||
|
||||
-- pokefirered/src/fieldmap.c:761
|
||||
function Connections.atPos(list, x, y, srcW, srcH)
|
||||
for _, n in ipairs(list or {}) do
|
||||
local dir = n.dir
|
||||
local skip = (dir == "north" and y >= 0) or (dir == "south" and y < srcH)
|
||||
or (dir == "west" and x >= 0) or (dir == "east" and x < srcW)
|
||||
if not skip and n.def then
|
||||
local lx, ly = Connections.localPos(n, x, y, srcW, srcH)
|
||||
if lx then return n, lx, ly end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
return Connections
|
||||
@@ -339,6 +339,7 @@ function Dataset.hydrate(game)
|
||||
Space.ensureBundle(nil)
|
||||
nEvents = Space.attachEventsToMaps(game.data.maps, Space.bundle) or 0
|
||||
end
|
||||
require("src.core.game3.link.union_plaza_map").ensure(game)
|
||||
|
||||
local NativeTileset = require("src.core.game3.tileset_native")
|
||||
if NativeTileset.install then
|
||||
|
||||
@@ -242,6 +242,65 @@ function Dex.countOwned(dex, mode)
|
||||
return Dex.countCaught(dex, mode)
|
||||
end
|
||||
|
||||
local function keyed(t, id)
|
||||
if type(t) ~= "table" then return nil end
|
||||
local v = t[id]
|
||||
if v == nil then v = t[tostring(id)] end
|
||||
if v == nil then v = t[string.format("0x%X", id)] end
|
||||
return v
|
||||
end
|
||||
|
||||
-- pokefirered/src/event_data.c:107
|
||||
function Dex.nationalEnabled(save)
|
||||
if type(save) ~= "table" then return false end
|
||||
local dex = type(save.dex) == "table" and save.dex or {}
|
||||
if dex.national == true or dex.nationalUnlocked == true or dex.isNationalUnlocked == true
|
||||
or save.national_dex_unlocked == true then
|
||||
return true
|
||||
end
|
||||
local flag = keyed(save.flags, 0x840)
|
||||
if flag == true or (type(save.flags) == "table" and save.flags.FLAG_SYS_NATIONAL_DEX == true) then
|
||||
return true
|
||||
end
|
||||
local var = keyed(save.vars, 0x404E)
|
||||
if var == nil and type(save.vars) == "table" then var = save.vars.VAR_NATIONAL_DEX end
|
||||
return tonumber(var) == 0x6258
|
||||
end
|
||||
|
||||
-- pokefirered/src/main_menu.c:643
|
||||
function Dex.summaryCount(save)
|
||||
if type(save) ~= "table" then return 0 end
|
||||
local dex = type(save.dex) == "table" and save.dex
|
||||
or type(save.pokedex) == "table" and save.pokedex or {}
|
||||
local national = Dex.nationalEnabled(save)
|
||||
local counted, n = {}, 0
|
||||
for _, key in ipairs({ "caught", "owned" }) do
|
||||
for sp, on in pairs(type(dex[key]) == "table" and dex[key] or {}) do
|
||||
local id = tonumber(sp)
|
||||
if not id and type(sp) == "string" then
|
||||
local ok, res = pcall(resolve_species_id, sp)
|
||||
id = ok and tonumber(res) or nil
|
||||
end
|
||||
if id and on and on ~= 0 and not counted[id]
|
||||
and id >= 1 and (national or id <= Dex.KANTO_MAX) then
|
||||
counted[id] = true
|
||||
n = n + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
local ci = type(save.modData) == "table" and type(save.modData.cartImport) == "table"
|
||||
and save.modData.cartImport
|
||||
local maxNat = national and Dex.NATIONAL_MAX or Dex.KANTO_MAX
|
||||
for _, nat in ipairs(ci and type(ci.dexOwned) == "table" and ci.dexOwned or {}) do
|
||||
nat = tonumber(nat)
|
||||
if nat and nat >= 1 and nat <= maxNat and not (nat <= Dex.HOST_MAX and counted[nat]) then
|
||||
if nat <= Dex.HOST_MAX then counted[nat] = true end
|
||||
n = n + 1
|
||||
end
|
||||
end
|
||||
return n
|
||||
end
|
||||
|
||||
--- Split for returnToHost: hostUpdates (1–251) + national sidecar (252+).
|
||||
function Dex.splitForHost(dex)
|
||||
local hostSeen, hostCaught = {}, {}
|
||||
|
||||
@@ -133,8 +133,8 @@ local function resolveLayout(mapId)
|
||||
end
|
||||
end
|
||||
|
||||
if Map and Map.neighbors then
|
||||
for _, n in pairs(Map.neighbors) do
|
||||
if Map and Map.neighborList then
|
||||
for _, n in ipairs(Map.neighborList) do
|
||||
if n.def and n.def.midLayout then
|
||||
if norm(n.map or n.mapId) == key then
|
||||
return n.def.midLayout, n.def.pair
|
||||
|
||||
@@ -474,6 +474,7 @@ function Field.useItemfinder(session, showOWMessage)
|
||||
width = layout and layout.width,
|
||||
height = layout and layout.height,
|
||||
neighbors = Map.neighbors,
|
||||
neighborList = Map.neighborList,
|
||||
eventsFor = function(id) return get_map_bg_events(game, id) end,
|
||||
})
|
||||
|
||||
@@ -995,14 +996,21 @@ function Field.executeFieldMove(payload)
|
||||
local Map = require("src.core.game3.map")
|
||||
local def = Map.currentDef()
|
||||
local layout = def and def.midLayout
|
||||
if layout and layout.midAt and layout.setMidAt then
|
||||
local Collision = require("src.core.game3.collision")
|
||||
if layout then
|
||||
local FieldMoves = require("src.core.game3.field_moves")
|
||||
local w, h = layout.width or 0, layout.height or 0
|
||||
local Collision = require("src.core.game3.collision")
|
||||
-- pokefirered/src/fldeff_rocksmash.c:31
|
||||
local elev = P.elevation or 0
|
||||
FieldMoves.mowGrass3x3(P.cellX, P.cellY, function(x, y) return layout:midAt(x, y) end,
|
||||
function(x, y, mid) layout:setMidAt(x, y, mid) end,
|
||||
function(x, y) return Collision.isGrass(x, y) end)
|
||||
local okFv, FieldView = pcall(require, "src.core.game3.field_view")
|
||||
if okFv and FieldView then FieldView._nativeDirty = true end
|
||||
function(x, y, mid) Field.setMetatile(x, y, mid, false) end,
|
||||
function(x, y)
|
||||
-- pokefirered/src/fldeff_cut.c:219
|
||||
return x >= 0 and y >= 0 and x < w and y < h and layout:elevAt(x, y) == elev
|
||||
and Collision.isGrass(x, y)
|
||||
end)
|
||||
-- pokefirered/src/field_effect_helpers.c:313
|
||||
if not Collision.isGrass(P.cellX, P.cellY) then FieldEffects.clearTallGrass() end
|
||||
end
|
||||
FieldEffects.startCutGrass(P.cellX, P.cellY, function()
|
||||
Field.locked = false
|
||||
|
||||
@@ -106,6 +106,8 @@ FieldMoves.CUT_GRASS_METATILES = {
|
||||
[0x300] = 0x310, -- FuchsiaCity: SafariZoneTreeTopLeft_Grass -> SafariZoneTreeTopLeft_Mowed
|
||||
[0x301] = 0x311, -- FuchsiaCity: SafariZoneTreeTopMiddle_Grass -> SafariZoneTreeTopMiddle_Mowed
|
||||
[0x302] = 0x312, -- FuchsiaCity: SafariZoneTreeTopRight_Grass -> SafariZoneTreeTopRight_Mowed
|
||||
-- pokefirered/include/constants/metatile_labels.h:295
|
||||
[0x284] = 0x281,
|
||||
}
|
||||
|
||||
-- Metatile terrain / collision behaviors
|
||||
@@ -757,7 +759,8 @@ end
|
||||
-- `getMetatileFn(x, y)`: returns numeric metatileId
|
||||
-- `setMetatileFn(x, y, newMetatileId)`: applies new metatileId
|
||||
-- Returns count of cut tiles
|
||||
function FieldMoves.mowGrass3x3(cx, cy, getMetatileFn, setMetatileFn, isGrassFn)
|
||||
-- pokefirered/src/fldeff_cut.c:200
|
||||
function FieldMoves.mowGrass3x3(cx, cy, getMetatileFn, setMetatileFn, sameElevationFn)
|
||||
if not getMetatileFn or not setMetatileFn then return 0 end
|
||||
local count = 0
|
||||
|
||||
@@ -765,13 +768,10 @@ function FieldMoves.mowGrass3x3(cx, cy, getMetatileFn, setMetatileFn, isGrassFn)
|
||||
for dx = -1, 1 do
|
||||
local x = cx + dx
|
||||
local y = cy + dy
|
||||
local mid = getMetatileFn(x, y)
|
||||
if mid then
|
||||
local newMid = FieldMoves.CUT_GRASS_METATILES[mid]
|
||||
if not newMid and isGrassFn and isGrassFn(x, y) then
|
||||
-- Default flat-ground replacement in general tileset
|
||||
newMid = 0x001
|
||||
end
|
||||
if not sameElevationFn or sameElevationFn(x, y) then
|
||||
local mid = getMetatileFn(x, y)
|
||||
-- pokefirered/src/fldeff_cut.c:237
|
||||
local newMid = mid and FieldMoves.CUT_GRASS_METATILES[mid]
|
||||
if newMid then
|
||||
setMetatileFn(x, y, newMid)
|
||||
count = count + 1
|
||||
|
||||
@@ -433,10 +433,19 @@ local function actorPriority(a)
|
||||
end
|
||||
end
|
||||
|
||||
local function sortActors(a, b)
|
||||
local ay = a.subpriority or a.sortY or a.y
|
||||
local by = b.subpriority or b.sortY or b.y
|
||||
if ay == by then return (a.i or 0) < (b.i or 0) end
|
||||
return ay < by
|
||||
end
|
||||
|
||||
-- event_object_movement.c:8379-8387, scrcmd.c:1130
|
||||
local function applyDrawOrder(actors)
|
||||
local underActors = {}
|
||||
local overActors = {}
|
||||
local function applyDrawOrder(actors, underActors, overActors)
|
||||
underActors = underActors or {}
|
||||
overActors = overActors or {}
|
||||
for i = #underActors, 1, -1 do underActors[i] = nil end
|
||||
for i = #overActors, 1, -1 do overActors[i] = nil end
|
||||
for _, a in ipairs(actors) do
|
||||
local obj = a.eventObject
|
||||
if obj and obj.fixedPriority then
|
||||
@@ -454,18 +463,14 @@ local function applyDrawOrder(actors)
|
||||
underActors[#underActors + 1] = a
|
||||
end
|
||||
end
|
||||
local function sortActors(a, b)
|
||||
local ay = a.subpriority or a.sortY or a.y
|
||||
local by = b.subpriority or b.sortY or b.y
|
||||
if ay == by then return (a.i or 0) < (b.i or 0) end
|
||||
return ay < by
|
||||
end
|
||||
table.sort(underActors, sortActors)
|
||||
table.sort(overActors, sortActors)
|
||||
return underActors, overActors
|
||||
end
|
||||
FieldView.applyDrawOrder = applyDrawOrder
|
||||
|
||||
local owOpts = {}
|
||||
|
||||
local function drawSingleActor(game, mapDef, a, camX, camY)
|
||||
local daytime = daytimeFor(game, mapDef)
|
||||
local okOw, OwSprites = pcall(require, "src.core.game3.ow_sprites")
|
||||
@@ -478,15 +483,14 @@ local function drawSingleActor(game, mapDef, a, camX, camY)
|
||||
drew = true
|
||||
end
|
||||
if not drew and useOw and a.graphicsId ~= nil then
|
||||
local opts = {
|
||||
bow = a.bow,
|
||||
fieldMove = a.fieldMove,
|
||||
fieldMoveFrame = a.fieldMoveFrame,
|
||||
fishing = a.fishing,
|
||||
fishFrame = a.fishFrame,
|
||||
frame = a.frame,
|
||||
running = a.running,
|
||||
}
|
||||
local opts = owOpts
|
||||
opts.bow = a.bow
|
||||
opts.fieldMove = a.fieldMove
|
||||
opts.fieldMoveFrame = a.fieldMoveFrame
|
||||
opts.fishing = a.fishing
|
||||
opts.fishFrame = a.fishFrame
|
||||
opts.frame = a.frame
|
||||
opts.running = a.running
|
||||
drew = OwSprites.draw(
|
||||
a.graphicsId, a.x, a.y, camX, camY, a.facing, a.walkPhase, a.stepFlip, opts)
|
||||
end
|
||||
@@ -509,11 +513,16 @@ local function drawSingleActor(game, mapDef, a, camX, camY)
|
||||
if billboarded then love.graphics.pop() end
|
||||
end
|
||||
|
||||
local frameActors, frameUnder, frameOver = {}, {}, {}
|
||||
local npcActors = setmetatable({}, { __mode = "k" })
|
||||
local playerActor = {}
|
||||
|
||||
local function collectGame3Actors(game, mapDef, camX, camY, px, py, facing, walkPhase, stepFlip, playerYOff, playerXOff)
|
||||
local okO, Objects = pcall(require, "src.core.game3.objects")
|
||||
local okOw, OwSprites = pcall(require, "src.core.game3.ow_sprites")
|
||||
local useOw = okOw and OwSprites and OwSprites.ready and OwSprites.ready()
|
||||
local actors = {}
|
||||
local actors = frameActors
|
||||
for i = #actors, 1, -1 do actors[i] = nil end
|
||||
local hasObjects = okO and Objects and Objects.hasMap and Objects.hasMap()
|
||||
|
||||
if hasObjects then
|
||||
@@ -522,23 +531,27 @@ local function collectGame3Actors(game, mapDef, camX, camY, px, py, facing, walk
|
||||
if eo.moving and eo.targetY and eo.targetY > (eo.cellY or 0) then
|
||||
sortY = math.max(sortY, eo.targetY * CELL)
|
||||
end
|
||||
actors[#actors + 1] = {
|
||||
kind = "npc",
|
||||
i = eo.localId,
|
||||
obj = eo.def,
|
||||
eventObject = eo,
|
||||
elevation = eo.elevation or (eo.def and eo.def.elevation) or 0,
|
||||
x = (eo.px or (eo.cellX * CELL)) + (eo.raiseX or 0),
|
||||
y = (eo.py or (eo.cellY * CELL)) + (eo.raiseY or 0),
|
||||
sortY = sortY,
|
||||
facing = eo.facing or "down",
|
||||
walkPhase = Objects.walkPhase(eo),
|
||||
stepFlip = eo.stepFlip and true or false,
|
||||
bow = (eo.bowFrames and eo.bowFrames > 0) or eo.raiseHand == true,
|
||||
frame = eo.customFrame,
|
||||
sprite = eo.sprite or spriteNameForObj(eo.def or {}),
|
||||
graphicsId = eo.graphicsId or (eo.def and (eo.def.graphicsId or eo.def.graphics)),
|
||||
}
|
||||
local a = npcActors[eo]
|
||||
if not a then
|
||||
a = { kind = "npc", eventObject = eo }
|
||||
npcActors[eo] = a
|
||||
end
|
||||
a.i = eo.localId
|
||||
a.obj = eo.def
|
||||
a.elevation = eo.elevation or (eo.def and eo.def.elevation) or 0
|
||||
a.x = (eo.px or (eo.cellX * CELL)) + (eo.raiseX or 0)
|
||||
a.y = (eo.py or (eo.cellY * CELL)) + (eo.raiseY or 0)
|
||||
a.sortY = sortY
|
||||
a.facing = eo.facing or "down"
|
||||
a.walkPhase = Objects.walkPhase(eo)
|
||||
a.stepFlip = eo.stepFlip and true or false
|
||||
a.bow = (eo.bowFrames and eo.bowFrames > 0) or eo.raiseHand == true
|
||||
a.frame = eo.customFrame
|
||||
a.sprite = eo.sprite or spriteNameForObj(eo.def or {})
|
||||
a.graphicsId = eo.graphicsId or (eo.def and (eo.def.graphicsId or eo.def.graphics))
|
||||
a.priority = nil
|
||||
a.subpriority = nil
|
||||
actors[#actors + 1] = a
|
||||
end
|
||||
collectNeighborActors(actors, 10000, currentMapId(game),
|
||||
resolveMapDef(game, currentMapId(game)))
|
||||
@@ -606,28 +619,30 @@ local function collectGame3Actors(game, mapDef, camX, camY, px, py, facing, walk
|
||||
if not fieldMove and FieldMod and FieldMod.fishingPose then
|
||||
fishFrame, fishX2, fishY2 = FieldMod.fishingPose()
|
||||
end
|
||||
actors[#actors + 1] = {
|
||||
kind = "player",
|
||||
elevation = PlayerMod and PlayerMod.elevation or 3,
|
||||
x = px + (playerXOff or 0) + (fishX2 or 0),
|
||||
y = py + (playerYOff or 0) + (fishY2 or 0),
|
||||
sortY = playerSortY,
|
||||
facing = facing or "down",
|
||||
walkPhase = (walkPhase == 1 or walkPhase == true) and 1 or 0,
|
||||
stepFlip = stepFlip and true or false,
|
||||
fieldMove = fieldMove,
|
||||
fieldMoveFrame = fieldMoveFrame,
|
||||
fishing = fishFrame ~= nil,
|
||||
fishFrame = fishFrame,
|
||||
running = PlayerMod and PlayerMod.runPose and PlayerMod.runPose() or nil,
|
||||
sprite = playerSpriteName(game),
|
||||
graphicsId = useOw and OwSprites.playerGraphicsId(game) or nil,
|
||||
}
|
||||
local a = playerActor
|
||||
a.kind = "player"
|
||||
a.elevation = PlayerMod and PlayerMod.elevation or 3
|
||||
a.x = px + (playerXOff or 0) + (fishX2 or 0)
|
||||
a.y = py + (playerYOff or 0) + (fishY2 or 0)
|
||||
a.sortY = playerSortY
|
||||
a.facing = facing or "down"
|
||||
a.walkPhase = (walkPhase == 1 or walkPhase == true) and 1 or 0
|
||||
a.stepFlip = stepFlip and true or false
|
||||
a.fieldMove = fieldMove
|
||||
a.fieldMoveFrame = fieldMoveFrame
|
||||
a.fishing = fishFrame ~= nil
|
||||
a.fishFrame = fishFrame
|
||||
a.running = PlayerMod and PlayerMod.runPose and PlayerMod.runPose() or nil
|
||||
a.sprite = playerSpriteName(game)
|
||||
a.graphicsId = useOw and OwSprites.playerGraphicsId(game) or nil
|
||||
a.priority = nil
|
||||
a.subpriority = nil
|
||||
actors[#actors + 1] = a
|
||||
end
|
||||
|
||||
local follower = require("src.world.game3.Follower").actor()
|
||||
if follower then actors[#actors + 1] = follower end
|
||||
return applyDrawOrder(actors)
|
||||
return applyDrawOrder(actors, frameUnder, frameOver)
|
||||
end
|
||||
|
||||
--- Collect visible tile draws grouped by palette slot for batched GbcPalette.with.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
local Connections = require("src.core.game3.connections")
|
||||
local Itemfinder = {}
|
||||
|
||||
local SE_ITEMFINDER = 65
|
||||
@@ -36,54 +37,22 @@ local function hidden_item_at_pos(events, x, y, flagSet)
|
||||
return false
|
||||
end
|
||||
|
||||
local function neighbor(neighbors, a, b)
|
||||
return neighbors and (neighbors[a] or neighbors[b])
|
||||
end
|
||||
|
||||
local function layout_size(def)
|
||||
local L = def and def.midLayout
|
||||
if L then return L.width or 0, L.height or 0 end
|
||||
return (tonumber(def and def.width) or 0) * 2, (tonumber(def and def.height) or 0) * 2
|
||||
local function neighbor_list(opts)
|
||||
if opts.neighborList then return opts.neighborList end
|
||||
local list = {}
|
||||
for k, n in pairs(opts.neighbors or {}) do
|
||||
local dir = Connections.cardinal(n.dir or k)
|
||||
if dir then
|
||||
list[#list + 1] = { dir = dir, map = n.map, mapId = n.mapId, def = n.def, offset = n.offset }
|
||||
end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
-- pokefirered/src/fieldmap.c:761, src/itemfinder.c:312
|
||||
local function connected_hidden_item(opts, lx, ly)
|
||||
local curW, curH = opts.width, opts.height
|
||||
local n = opts.neighbors
|
||||
local conn, cx, cy
|
||||
if ly < 0 then
|
||||
conn = neighbor(n, "north", "up")
|
||||
if conn then
|
||||
local w, h = layout_size(conn.def)
|
||||
local off = tonumber(conn.offset) or 0
|
||||
if lx - off >= 0 and lx - off < w then cx, cy = lx - off, h + ly end
|
||||
end
|
||||
end
|
||||
if not cx and ly >= curH then
|
||||
conn = neighbor(n, "south", "down")
|
||||
if conn then
|
||||
local w = layout_size(conn.def)
|
||||
local off = tonumber(conn.offset) or 0
|
||||
if lx - off >= 0 and lx - off < w then cx, cy = lx - off, ly - curH end
|
||||
end
|
||||
end
|
||||
if not cx and lx < 0 then
|
||||
conn = neighbor(n, "west", "left")
|
||||
if conn then
|
||||
local w, h = layout_size(conn.def)
|
||||
local off = tonumber(conn.offset) or 0
|
||||
if ly - off >= 0 and ly - off < h then cx, cy = w + lx, ly - off end
|
||||
end
|
||||
end
|
||||
if not cx and lx >= curW then
|
||||
conn = neighbor(n, "east", "right")
|
||||
if conn then
|
||||
local _, h = layout_size(conn.def)
|
||||
local off = tonumber(conn.offset) or 0
|
||||
if ly - off >= 0 and ly - off < h then cx, cy = lx - curW, ly - off end
|
||||
end
|
||||
end
|
||||
if not cx then return false end
|
||||
local conn, cx, cy = Connections.atPos(neighbor_list(opts), lx, ly, opts.width, opts.height)
|
||||
if not conn then return false end
|
||||
return hidden_item_at_pos(opts.eventsFor(conn.map or conn.mapId), cx, cy, opts.flagSet)
|
||||
end
|
||||
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
local Plaza = {}
|
||||
|
||||
Plaza.MAP_ID = "FR_UNION_ROOM_PLAZA"
|
||||
Plaza.SOURCE_ID = "FR_UNION_ROOM"
|
||||
Plaza.CAP = 40
|
||||
Plaza.WIDTH = 25
|
||||
Plaza.HEIGHT = 25
|
||||
Plaza.EXIT_X, Plaza.EXIT_Y = 12, 24
|
||||
Plaza.ATTENDANT_LOCAL_ID = 1
|
||||
|
||||
Plaza.ROLES = {
|
||||
"ABCDTTTTTTTTTTTTTTTTTTTTE",
|
||||
"abcdtttttttttttttttttttte",
|
||||
"FGHHHHHHHHHHHHHHHHHHHHHIJ",
|
||||
"fg.....................ij",
|
||||
"L.......................R",
|
||||
"L.......................R",
|
||||
"L.......................R",
|
||||
"L.......................R",
|
||||
"L.......................R",
|
||||
"L.......................R",
|
||||
"L..........123..........R",
|
||||
"L..........456..........R",
|
||||
"L..........789..........R",
|
||||
"L.......................R",
|
||||
"L.......................R",
|
||||
"L.......................R",
|
||||
"L.......................R",
|
||||
"L.......................R",
|
||||
"L.......................R",
|
||||
"L.......................R",
|
||||
"L.......................R",
|
||||
"L.......................R",
|
||||
"L.......................R",
|
||||
"Kk_________mno_________pq",
|
||||
"###########xXz###########",
|
||||
}
|
||||
|
||||
Plaza.SOURCE = {
|
||||
A = { 0, 0 }, B = { 1, 0 }, C = { 2, 0 }, D = { 3, 0 }, T = { 4, 0 }, E = { 14, 0 },
|
||||
a = { 0, 1 }, b = { 1, 1 }, c = { 2, 1 }, d = { 3, 1 }, t = { 4, 1 }, e = { 14, 1 },
|
||||
F = { 0, 2 }, G = { 1, 2 }, H = { 2, 2 }, I = { 13, 2 }, J = { 14, 2 },
|
||||
f = { 0, 3 }, g = { 1, 3 }, ["."] = { 2, 3 }, i = { 13, 3 }, j = { 14, 3 },
|
||||
L = { 0, 4 }, R = { 14, 4 },
|
||||
["1"] = { 6, 5 }, ["2"] = { 7, 5 }, ["3"] = { 8, 5 },
|
||||
["4"] = { 6, 6 }, ["5"] = { 7, 6 }, ["6"] = { 8, 6 },
|
||||
["7"] = { 6, 7 }, ["8"] = { 7, 7 }, ["9"] = { 8, 7 },
|
||||
K = { 0, 10 }, k = { 1, 10 }, _ = { 2, 10 }, m = { 6, 10 }, n = { 7, 10 }, o = { 8, 10 },
|
||||
p = { 13, 10 }, q = { 14, 10 },
|
||||
["#"] = { 0, 11 }, x = { 6, 11 }, X = { 7, 11 }, z = { 8, 11 },
|
||||
}
|
||||
|
||||
Plaza.BLOCKED = {
|
||||
A = true, B = true, C = true, D = true, T = true, E = true,
|
||||
a = true, b = true, c = true, d = true, t = true, e = true,
|
||||
["#"] = true,
|
||||
}
|
||||
|
||||
local CELL_XS = { 3, 6, 9, 12, 15, 18, 21 }
|
||||
local CELL_YS = { 5, 8, 11, 14, 17, 20 }
|
||||
local DOOR_X, DOOR_Y = 12, 23
|
||||
|
||||
local cells, bySlot, byKey = {}, {}, {}
|
||||
for _, y in ipairs(CELL_YS) do
|
||||
for _, x in ipairs(CELL_XS) do
|
||||
if not (x == DOOR_X and y == 20) then
|
||||
cells[#cells + 1] = { x = x, y = y }
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(cells, function(p, q)
|
||||
local dp = math.abs(p.x - DOOR_X) + math.abs(p.y - DOOR_Y)
|
||||
local dq = math.abs(q.x - DOOR_X) + math.abs(q.y - DOOR_Y)
|
||||
if dp ~= dq then return dp < dq end
|
||||
local ap, aq = math.abs(p.x - DOOR_X), math.abs(q.x - DOOR_X)
|
||||
if ap ~= aq then return ap < aq end
|
||||
if p.x ~= q.x then return p.x < q.x end
|
||||
return p.y > q.y
|
||||
end)
|
||||
for i, c in ipairs(cells) do
|
||||
if i <= Plaza.CAP then
|
||||
bySlot[i] = c
|
||||
byKey[c.y * 64 + c.x] = i
|
||||
end
|
||||
end
|
||||
Plaza.CELLS = cells
|
||||
|
||||
function Plaza.cellFor(slot)
|
||||
local c = bySlot[tonumber(slot) or -1]
|
||||
if not c then return nil end
|
||||
return c.x, c.y, "down"
|
||||
end
|
||||
|
||||
function Plaza.slotAt(x, y)
|
||||
x, y = tonumber(x), tonumber(y)
|
||||
if not (x and y) then return nil end
|
||||
return byKey[y * 64 + x]
|
||||
end
|
||||
|
||||
function Plaza.entry()
|
||||
return Plaza.EXIT_X, Plaza.EXIT_Y, "up"
|
||||
end
|
||||
|
||||
function Plaza.roleAt(x, y)
|
||||
local row = Plaza.ROLES[y + 1]
|
||||
if not row or x < 0 or x >= #row then return nil end
|
||||
return row:sub(x + 1, x + 1)
|
||||
end
|
||||
|
||||
local function copyRow(row)
|
||||
local out = {}
|
||||
for k, v in pairs(row) do out[k] = v end
|
||||
return out
|
||||
end
|
||||
|
||||
function Plaza.buildDecoded(src)
|
||||
local cellsOut = {}
|
||||
for y = 0, Plaza.HEIGHT - 1 do
|
||||
for x = 0, Plaza.WIDTH - 1 do
|
||||
local s = Plaza.SOURCE[Plaza.roleAt(x, y)]
|
||||
if s[1] >= (src.trueWidth or src.width or 0) or s[2] >= (src.trueHeight or src.height or 0) then
|
||||
error(("union plaza: %s layout has no cell %d,%d; re-import the ROM"):format(Plaza.SOURCE_ID, s[1], s[2]))
|
||||
end
|
||||
local c = src:cellAt(s[1], s[2])
|
||||
cellsOut[y * Plaza.WIDTH + x + 1] = { mid = c.mid, coll = c.coll, elev = c.elev }
|
||||
end
|
||||
end
|
||||
local border = {}
|
||||
for i, m in ipairs(src.borderMids or { 0 }) do border[i] = m end
|
||||
return {
|
||||
width = Plaza.WIDTH,
|
||||
height = Plaza.HEIGHT,
|
||||
trueWidth = Plaza.WIDTH,
|
||||
trueHeight = Plaza.HEIGHT,
|
||||
borderWidth = src.borderWidth or 1,
|
||||
borderHeight = src.borderHeight or 1,
|
||||
borderMids = border,
|
||||
cells = cellsOut,
|
||||
}
|
||||
end
|
||||
|
||||
local function buildEvents(ev)
|
||||
if type(ev) ~= "table" then return nil end
|
||||
local out = {}
|
||||
for k, v in pairs(ev) do out[k] = v end
|
||||
local objs = {}
|
||||
for _, row in ipairs(ev.objects or ev.objectEvents or {}) do
|
||||
if tonumber(row.localId) == Plaza.ATTENDANT_LOCAL_ID then
|
||||
objs[#objs + 1] = copyRow(row)
|
||||
end
|
||||
end
|
||||
out.objects = objs
|
||||
out.objectEvents = nil
|
||||
out.bgEvents = {}
|
||||
out.coordEvents = {}
|
||||
return out
|
||||
end
|
||||
|
||||
function Plaza.ensure(game)
|
||||
local maps = game and game.data and game.data.maps
|
||||
if type(maps) ~= "table" then error("union plaza: no map table to build " .. Plaza.MAP_ID .. " into", 2) end
|
||||
local srcDef = maps[Plaza.SOURCE_ID]
|
||||
if not srcDef then
|
||||
error("union plaza: " .. Plaza.SOURCE_ID .. " is missing from the cache; re-import the ROM", 2)
|
||||
end
|
||||
if not srcDef.midLayout then
|
||||
local okM, Map = pcall(require, "src.core.game3.map")
|
||||
if okM and Map and Map.ensureMidLayout then Map.ensureMidLayout(game, Plaza.SOURCE_ID, srcDef) end
|
||||
end
|
||||
local src = srcDef.midLayout
|
||||
if not src then
|
||||
error("union plaza: " .. Plaza.SOURCE_ID .. " has no layout in the cache; re-import the ROM", 2)
|
||||
end
|
||||
|
||||
local def = maps[Plaza.MAP_ID]
|
||||
if not (def and def._plazaSource == src) then
|
||||
local LayoutNative = require("src.core.game3.layout_native")
|
||||
local pair = src.pair or srcDef.pair
|
||||
def = {}
|
||||
for k, v in pairs(srcDef) do def[k] = v end
|
||||
def.id = Plaza.MAP_ID
|
||||
def.name = Plaza.MAP_ID
|
||||
def.width = Plaza.WIDTH
|
||||
def.height = Plaza.HEIGHT
|
||||
def.connections = {}
|
||||
def.midLayout = LayoutNative.fromDecoded(Plaza.buildDecoded(src), Plaza.MAP_ID, pair)
|
||||
def.pair = pair
|
||||
local warp = copyRow((srcDef.warps and srcDef.warps[1]) or {})
|
||||
warp.x, warp.y = Plaza.EXIT_X, Plaza.EXIT_Y
|
||||
def.warps = { warp }
|
||||
def.objects = nil
|
||||
def.objectEvents = nil
|
||||
def.bgEvents = {}
|
||||
def.coordEvents = {}
|
||||
def._plazaSource = src
|
||||
maps[Plaza.MAP_ID] = def
|
||||
end
|
||||
|
||||
local Space = package.loaded["src.core.game3.scripting.space"]
|
||||
local bundle = Space and Space.bundle
|
||||
local events = bundle and bundle.events
|
||||
if type(events) == "table" and events[Plaza.SOURCE_ID]
|
||||
and events[Plaza.MAP_ID] == nil then
|
||||
events[Plaza.MAP_ID] = buildEvents(events[Plaza.SOURCE_ID])
|
||||
end
|
||||
local ev = events and events[Plaza.MAP_ID]
|
||||
if ev and def.objects == nil and Space.attachEventsToMaps then
|
||||
Space.attachEventsToMaps({ [Plaza.MAP_ID] = def }, bundle)
|
||||
end
|
||||
return def
|
||||
end
|
||||
|
||||
return Plaza
|
||||
+281
-750
File diff suppressed because it is too large
Load Diff
+58
-65
@@ -4,11 +4,13 @@
|
||||
|
||||
local MapIds = require("src.core.game3.map_ids")
|
||||
local ModRuntime = require("src.mods.Runtime")
|
||||
local Connections = require("src.core.game3.connections")
|
||||
local Map = {}
|
||||
|
||||
Map.current = nil
|
||||
Map._announced = nil
|
||||
Map.neighbors = {}
|
||||
Map.neighborList = {}
|
||||
Map._loadedLayouts = {}
|
||||
Map._def = nil
|
||||
Map._currentDef = nil
|
||||
@@ -32,33 +34,34 @@ end
|
||||
|
||||
function Map.loadNeighborsDepth1(game, primaryDef)
|
||||
Map.neighbors = {}
|
||||
Map.neighborList = {}
|
||||
if not primaryDef or type(primaryDef.connections) ~= "table" then
|
||||
return Map.neighbors
|
||||
end
|
||||
local data = game and game.data and game.data.maps
|
||||
if not data then return Map.neighbors end
|
||||
for dir, conn in pairs(primaryDef.connections) do
|
||||
local mid = type(conn) == "table" and conn.map or conn
|
||||
if type(mid) == "string" and data[mid] then
|
||||
-- pokefirered/src/fieldmap.c:129
|
||||
for _, conn in ipairs(Connections.each(primaryDef)) do
|
||||
local mid = conn.map
|
||||
if data[mid] then
|
||||
local def = data[mid]
|
||||
Map.ensureMidLayout(game, mid, def)
|
||||
Map.neighbors[dir] = {
|
||||
local n = {
|
||||
dir = conn.dir,
|
||||
map = mid,
|
||||
mapId = mid,
|
||||
def = def,
|
||||
offset = type(conn) == "table" and (conn.offset or 0) or 0,
|
||||
offset = conn.offset,
|
||||
}
|
||||
Map.neighborList[#Map.neighborList + 1] = n
|
||||
Map.neighbors[conn.dir] = Map.neighbors[conn.dir] or n
|
||||
Map._loadedLayouts[mid] = true
|
||||
end
|
||||
end
|
||||
return Map.neighbors
|
||||
end
|
||||
|
||||
local function cell_size(def)
|
||||
local L = def and def.midLayout
|
||||
if L and L.width and L.height then return L.width or 0, L.height or 0 end
|
||||
return (tonumber(def and def.width) or 0) * 2, (tonumber(def and def.height) or 0) * 2
|
||||
end
|
||||
local cell_size = Connections.sizeOf
|
||||
|
||||
Map.world = {}
|
||||
Map._worldRoot = nil
|
||||
@@ -86,21 +89,22 @@ function Map.computeWorld(maps, rootId, hops, reachW, reachH, ensure)
|
||||
local cur = queue[qi]
|
||||
qi = qi + 1
|
||||
local curW, curH = cell_size(cur.def)
|
||||
for dir, conn in pairs(cur.def.connections or {}) do
|
||||
local destId = type(conn) == "table" and conn.map or conn
|
||||
local destDef = type(destId) == "string" and maps[destId] or nil
|
||||
for _, conn in ipairs(Connections.each(cur.def)) do
|
||||
local dir = conn.dir
|
||||
local destId = conn.map
|
||||
local destDef = maps[destId]
|
||||
if destDef and destDef ~= rootDef and not placed[destId] then
|
||||
local offset = (type(conn) == "table" and tonumber(conn.offset) or 0) or 0
|
||||
local offset = conn.offset
|
||||
ensure(destId, destDef)
|
||||
local destW, destH = cell_size(destDef)
|
||||
local ox, oy
|
||||
if dir == "north" or dir == "up" then
|
||||
if dir == "north" then
|
||||
ox, oy = offset, -destH
|
||||
elseif dir == "south" or dir == "down" then
|
||||
elseif dir == "south" then
|
||||
ox, oy = offset, curH
|
||||
elseif dir == "west" or dir == "left" then
|
||||
elseif dir == "west" then
|
||||
ox, oy = -destW, offset
|
||||
elseif dir == "east" or dir == "right" then
|
||||
elseif dir == "east" then
|
||||
ox, oy = curW, offset
|
||||
end
|
||||
if ox then
|
||||
@@ -153,28 +157,12 @@ end
|
||||
|
||||
function Map.overscanSlices()
|
||||
local slices = {}
|
||||
for dir, n in pairs(Map.neighbors) do
|
||||
slices[#slices + 1] = { dir = dir, mapId = n.map or n.mapId, offset = n.offset }
|
||||
for _, n in ipairs(Map.neighborList or {}) do
|
||||
slices[#slices + 1] = { dir = n.dir, mapId = n.map or n.mapId, offset = n.offset }
|
||||
end
|
||||
return slices
|
||||
end
|
||||
|
||||
local function neighborFor(cardinal)
|
||||
local n = Map.neighbors
|
||||
if not n then return nil end
|
||||
-- Dataset keys are north/south/west/east; some callers use up/down/left/right.
|
||||
if cardinal == "north" or cardinal == "up" then
|
||||
return n.north or n.up
|
||||
elseif cardinal == "south" or cardinal == "down" then
|
||||
return n.south or n.down
|
||||
elseif cardinal == "west" or cardinal == "left" then
|
||||
return n.west or n.left
|
||||
elseif cardinal == "east" or cardinal == "right" then
|
||||
return n.east or n.right
|
||||
end
|
||||
return n[cardinal]
|
||||
end
|
||||
|
||||
--- Resolve a cell in current-map space, sampling connected neighbors when OOB
|
||||
-- (pret VMap connection fill). Returns mid, sourcePair. OOB with no neighbor
|
||||
-- falls through to primary border tiling.
|
||||
@@ -199,40 +187,44 @@ function Map.worldMidAt(cx, cy, primaryDef)
|
||||
return layout:midAt(cx, cy), primaryPair
|
||||
end
|
||||
|
||||
if cy < 0 then
|
||||
local n = neighborFor("north")
|
||||
if n then
|
||||
local offset = tonumber(n.offset) or 0
|
||||
local L = n.def and n.def.midLayout
|
||||
local nh = L and L.height or 0
|
||||
local mid, pair = fromNeighbor(n, cx - offset, nh + cy)
|
||||
if mid ~= nil then return mid, pair end
|
||||
end
|
||||
elseif cy >= h then
|
||||
local n = neighborFor("south")
|
||||
if n then
|
||||
local offset = tonumber(n.offset) or 0
|
||||
local mid, pair = fromNeighbor(n, cx - offset, cy - h)
|
||||
if mid ~= nil then return mid, pair end
|
||||
local list = Map.neighborList or {}
|
||||
local function fromDir(dir)
|
||||
for i = #list, 1, -1 do
|
||||
local n = list[i]
|
||||
if n.dir == dir and n.def and n.def.midLayout then
|
||||
local L = n.def.midLayout
|
||||
local offset = tonumber(n.offset) or 0
|
||||
local nx, ny
|
||||
if dir == "north" then
|
||||
nx, ny = cx - offset, (L.height or 0) + cy
|
||||
elseif dir == "south" then
|
||||
nx, ny = cx - offset, cy - h
|
||||
elseif dir == "west" then
|
||||
nx, ny = (L.width or 0) + cx, cy - offset
|
||||
else
|
||||
nx, ny = cx - w, cy - offset
|
||||
end
|
||||
local mid, pair = fromNeighbor(n, nx, ny)
|
||||
if mid ~= nil then return mid, pair end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- pokefirered/src/fieldmap.c:129
|
||||
if cy < 0 then
|
||||
local mid, pair = fromDir("north")
|
||||
if mid ~= nil then return mid, pair end
|
||||
elseif cy >= h then
|
||||
local mid, pair = fromDir("south")
|
||||
if mid ~= nil then return mid, pair end
|
||||
end
|
||||
|
||||
if cx < 0 then
|
||||
local n = neighborFor("west")
|
||||
if n then
|
||||
local offset = tonumber(n.offset) or 0
|
||||
local L = n.def and n.def.midLayout
|
||||
local nw = L and L.width or 0
|
||||
local mid, pair = fromNeighbor(n, nw + cx, cy - offset)
|
||||
if mid ~= nil then return mid, pair end
|
||||
end
|
||||
local mid, pair = fromDir("west")
|
||||
if mid ~= nil then return mid, pair end
|
||||
elseif cx >= w then
|
||||
local n = neighborFor("east")
|
||||
if n then
|
||||
local offset = tonumber(n.offset) or 0
|
||||
local mid, pair = fromNeighbor(n, cx - w, cy - offset)
|
||||
if mid ~= nil then return mid, pair end
|
||||
end
|
||||
local mid, pair = fromDir("east")
|
||||
if mid ~= nil then return mid, pair end
|
||||
end
|
||||
|
||||
for _, entry in ipairs(Map.world) do
|
||||
@@ -335,6 +327,7 @@ function Map.load(mod, game, mapId, opts)
|
||||
Map.loadNeighborsDepth1(game, def)
|
||||
else
|
||||
Map.neighbors = {}
|
||||
Map.neighborList = {}
|
||||
end
|
||||
|
||||
local world = host_world(game)
|
||||
|
||||
+84
-69
@@ -420,6 +420,48 @@ local function resolveContextualMapObjects(mapId)
|
||||
end
|
||||
end
|
||||
|
||||
-- src/event_object_movement.c:1312
|
||||
local function spawnFromTemplate(def, mapId)
|
||||
local eo = newEventObject(def)
|
||||
if eo.localId > 0 then
|
||||
applyPerm(eo, mapId)
|
||||
local tmt = Objects._templateMt[eo.localId]
|
||||
if tmt then
|
||||
Objects.setTrainerMovementType(eo, tmt)
|
||||
-- src/event_object_movement.c:1569
|
||||
eo.invisible = tmt == MOVEMENT_TYPE_INVISIBLE
|
||||
local face = ({ [7] = "up", [8] = "down", [9] = "left", [10] = "right" })[tmt]
|
||||
if face then eo.facing = face end
|
||||
end
|
||||
end
|
||||
return eo
|
||||
end
|
||||
|
||||
-- src/event_object_movement.c:1651
|
||||
local function respawnFromTemplate(lid)
|
||||
local tpl
|
||||
for _, def in ipairs(Objects._defs or {}) do
|
||||
if tonumber(def.localId or def.index) == lid then tpl = def end
|
||||
end
|
||||
if not tpl then return nil end
|
||||
local eo = spawnFromTemplate(tpl, Objects._mapId)
|
||||
eo.hidden, eo.visible = false, true
|
||||
if eo.def then eo.def.hidden = false end
|
||||
local present = Objects._byId[lid] ~= nil
|
||||
if not present then
|
||||
for _, id in ipairs(Objects._order) do
|
||||
if id == lid then present = true break end
|
||||
end
|
||||
end
|
||||
Objects._byId[lid] = eo
|
||||
Objects._tracks[lid] = nil
|
||||
if not present then Objects._order[#Objects._order + 1] = lid end
|
||||
if ModRuntime.wants("world.npc_spawned") then
|
||||
ModRuntime.emit("world.npc_spawned", { mapId = Objects._mapId, npcId = lid, runtime = eo })
|
||||
end
|
||||
return eo
|
||||
end
|
||||
|
||||
--- Spawn EventObjects from mapDef.objects (extract / content).
|
||||
function Objects.loadMap(game, mapId, mapDef)
|
||||
local sameMap = Objects._mapId == mapId
|
||||
@@ -456,17 +498,8 @@ function Objects.loadMap(game, mapId, mapDef)
|
||||
resolveContextualMapObjects(mapId)
|
||||
local announce = ModRuntime.wants("world.npc_spawned")
|
||||
for _, def in ipairs(Objects._defs) do
|
||||
local eo = newEventObject(def)
|
||||
local eo = spawnFromTemplate(def, mapId)
|
||||
if eo.localId > 0 then
|
||||
applyPerm(eo, mapId)
|
||||
local tmt = Objects._templateMt[eo.localId]
|
||||
if tmt then
|
||||
Objects.setTrainerMovementType(eo, tmt)
|
||||
-- src/event_object_movement.c:1569
|
||||
eo.invisible = tmt == MOVEMENT_TYPE_INVISIBLE
|
||||
local face = ({ [7] = "up", [8] = "down", [9] = "left", [10] = "right" })[tmt]
|
||||
if face then eo.facing = face end
|
||||
end
|
||||
Objects._byId[eo.localId] = eo
|
||||
Objects._order[#Objects._order + 1] = eo.localId
|
||||
if announce then
|
||||
@@ -537,35 +570,45 @@ end
|
||||
|
||||
local VIRT_DIR_FACE = { [1] = "down", [2] = "up", [3] = "left", [4] = "right" }
|
||||
|
||||
local drawList = {}
|
||||
local vrecs = {}
|
||||
|
||||
function Objects.forDraw()
|
||||
local list = {}
|
||||
local list = drawList
|
||||
local n = 0
|
||||
for _, lid in ipairs(Objects._order) do
|
||||
local eo = Objects._byId[lid]
|
||||
-- src/event_object_movement.c:8014
|
||||
if eo and eo.visible and not eo.hidden and not eo.invisible
|
||||
and not offMap(Objects._bounds, eo) then
|
||||
list[#list + 1] = eo
|
||||
n = n + 1
|
||||
list[n] = eo
|
||||
end
|
||||
end
|
||||
-- src/event_object_movement.c:1719
|
||||
if VirtualObjects.count() > 0 then
|
||||
for _, vo in ipairs(VirtualObjects.list()) do
|
||||
for i = 1, VirtualObjects.slots() do
|
||||
local vo = VirtualObjects.nth(i)
|
||||
if vo then
|
||||
local vrec = vrecs[vo.id]
|
||||
if not vrec then
|
||||
vrec = { virtualId = vo.id, visible = true, hidden = false }
|
||||
vrecs[vo.id] = vrec
|
||||
end
|
||||
local gid = tonumber(vo.graphicsId) or 0
|
||||
local vrec = {
|
||||
virtualId = vo.id,
|
||||
cellX = tonumber(vo.x) or 0,
|
||||
cellY = tonumber(vo.y) or 0,
|
||||
elevation = tonumber(vo.elevation) or 3,
|
||||
facing = VIRT_DIR_FACE[tonumber(vo.direction)] or "down",
|
||||
sprite = GfxIds.spriteFor(gid),
|
||||
graphicsId = gid,
|
||||
raiseY = tonumber(vo.y2) or 0,
|
||||
visible = true,
|
||||
hidden = false,
|
||||
}
|
||||
if not offMap(Objects._bounds, vrec) then list[#list + 1] = vrec end
|
||||
vrec.cellX = tonumber(vo.x) or 0
|
||||
vrec.cellY = tonumber(vo.y) or 0
|
||||
vrec.elevation = tonumber(vo.elevation) or 3
|
||||
vrec.facing = VIRT_DIR_FACE[tonumber(vo.direction)] or "down"
|
||||
vrec.sprite = GfxIds.spriteFor(gid)
|
||||
vrec.graphicsId = gid
|
||||
vrec.raiseY = tonumber(vo.y2) or 0
|
||||
if not offMap(Objects._bounds, vrec) then
|
||||
n = n + 1
|
||||
list[n] = vrec
|
||||
end
|
||||
end
|
||||
end
|
||||
for i = #list, n + 1, -1 do list[i] = nil end
|
||||
return list
|
||||
end
|
||||
|
||||
@@ -609,10 +652,9 @@ function Objects.blocks(tx, ty, exceptLocalId, elevation)
|
||||
end
|
||||
end
|
||||
-- pokefirered/src/union_room_player_avatar.c:475
|
||||
if VirtualObjects.count() > 0 then
|
||||
for _, vo in ipairs(VirtualObjects.list()) do
|
||||
if vo.solid == true and tonumber(vo.x) == tx and tonumber(vo.y) == ty then return true end
|
||||
end
|
||||
for i = 1, VirtualObjects.slots() do
|
||||
local vo = VirtualObjects.nth(i)
|
||||
if vo and vo.solid == true and tonumber(vo.x) == tx and tonumber(vo.y) == ty then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
@@ -662,7 +704,7 @@ local function beginStep(eo, tx, ty)
|
||||
eo.animClock = 0
|
||||
end
|
||||
|
||||
local function finishStep(eo, game)
|
||||
local function finishStep(eo, game, ctx)
|
||||
eo.cellX = eo.targetX
|
||||
eo.cellY = eo.targetY
|
||||
eo.px = eo.cellX * CELL
|
||||
@@ -673,11 +715,13 @@ local function finishStep(eo, game)
|
||||
if eo.def then
|
||||
eo.def.x, eo.def.y = eo.cellX, eo.cellY
|
||||
end
|
||||
if ctx then return end
|
||||
local Coll = Collision()
|
||||
local curElev = Coll and Coll.elevationAt and Coll.elevationAt(eo.cellX, eo.cellY)
|
||||
if curElev and curElev ~= 0 and curElev ~= 15 then
|
||||
eo.elevation = curElev
|
||||
end
|
||||
-- src/trainer_see.c:94
|
||||
if eo.sight and eo.sight > 0 and not eo.scriptBusy and not eo.frozen then
|
||||
local okTs, TrainerSight = pcall(require, "src.core.game3.trainer_sight")
|
||||
if okTs and TrainerSight and TrainerSight.check then
|
||||
@@ -716,7 +760,7 @@ local function stepOffset(frames, progress, cells)
|
||||
return math.floor(cells * CELL * math.min(progress, frames) / frames)
|
||||
end
|
||||
|
||||
local function tickMotion(eo, game)
|
||||
local function tickMotion(eo, game, ctx)
|
||||
Objects.updateElevation(eo)
|
||||
if not eo.moving then return false end
|
||||
eo.progress = eo.progress + 1
|
||||
@@ -728,7 +772,7 @@ local function tickMotion(eo, game)
|
||||
eo.px = eo.cellX * CELL + (dx > 0 and off or dx < 0 and -off or 0)
|
||||
eo.py = eo.cellY * CELL + (dy > 0 and off or dy < 0 and -off or 0)
|
||||
if eo.progress >= frames then
|
||||
finishStep(eo, game)
|
||||
finishStep(eo, game, ctx)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
@@ -1339,7 +1383,7 @@ function Objects.tickPool(pool, game, ctx)
|
||||
eo.bowFrames = eo.bowFrames - 1
|
||||
if eo.bowFrames <= 0 then eo.bowFrames = nil end
|
||||
end
|
||||
tickMotion(eo, game)
|
||||
tickMotion(eo, game, ctx or pool)
|
||||
idleTick(eo, game, ctx)
|
||||
end
|
||||
end
|
||||
@@ -1394,30 +1438,8 @@ end
|
||||
function Objects.addObject(localId)
|
||||
localId = tonumber(localId) or 0
|
||||
local eo = Objects._byId[localId]
|
||||
if eo then
|
||||
if eo.hidden then
|
||||
-- src/event_object_movement.c:1569
|
||||
eo.invisible = Objects.templateMovementType(localId) == MOVEMENT_TYPE_INVISIBLE
|
||||
end
|
||||
applyPerm(eo, Objects._mapId)
|
||||
eo.hidden = false
|
||||
eo.visible = true
|
||||
if eo.def then eo.def.hidden = false end
|
||||
return true
|
||||
end
|
||||
-- Respawn from template defs.
|
||||
for _, def in ipairs(Objects._defs or {}) do
|
||||
if tonumber(def.localId or def.index) == localId then
|
||||
eo = newEventObject(def)
|
||||
applyPerm(eo, Objects._mapId)
|
||||
eo.hidden = false
|
||||
eo.visible = true
|
||||
Objects._byId[localId] = eo
|
||||
Objects._order[#Objects._order + 1] = localId
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
if eo and not eo.hidden then return true end
|
||||
return respawnFromTemplate(localId) ~= nil
|
||||
end
|
||||
|
||||
--- Re-evaluate hide flags after sidecar load / setflag mid-map.
|
||||
@@ -1449,7 +1471,6 @@ end
|
||||
Objects.inCameraView = inCameraView
|
||||
|
||||
--- pret FlagClear/FlagSet on an object template hide flag.
|
||||
-- clearflag after removeobject must bring the NPC back at perm coords.
|
||||
-- src/scrcmd.c:558
|
||||
function Objects.syncFlagVisibility(flagId, hidden, force)
|
||||
flagId = tonumber(flagId) or 0
|
||||
@@ -1465,15 +1486,9 @@ function Objects.syncFlagVisibility(flagId, hidden, force)
|
||||
eo.visible = false
|
||||
if eo.def then eo.def.hidden = true end
|
||||
end
|
||||
else
|
||||
if eo.hidden then
|
||||
-- src/event_object_movement.c:1569
|
||||
eo.invisible = Objects.templateMovementType(lid) == MOVEMENT_TYPE_INVISIBLE
|
||||
end
|
||||
applyPerm(eo, Objects._mapId)
|
||||
eo.hidden = false
|
||||
eo.visible = true
|
||||
if eo.def then eo.def.hidden = false end
|
||||
elseif eo.hidden then
|
||||
-- src/event_object_movement.c:1792
|
||||
respawnFromTemplate(lid)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -76,6 +76,19 @@ local CONTINUE_GAME_WARP = 0x01
|
||||
-- pokefirered/data/maps/PokemonLeague_HallOfFame/scripts.inc:40
|
||||
local HALL_OF_FAME_MAP = "FR_POKEMON_LEAGUE_HALL_OF_FAME"
|
||||
|
||||
local UNION_ROOMS = { FR_UNION_ROOM = true, FR_UNION_ROOM_PLAZA = true }
|
||||
-- pokefirered/data/maps/ViridianCity_PokemonCenter_2F/map.json:84
|
||||
local UNION_DOOR_X, UNION_DOOR_Y = 5, 1
|
||||
local FIRST_CENTER_2F = "FR_VIRIDIAN_CITY_POKEMON_CENTER_2F"
|
||||
|
||||
local function union_room_door_map(session)
|
||||
local heal = type(session.healMap) == "string" and session.healMap or ""
|
||||
local city = heal:match("^(.+_POKEMON_CENTER)_1F$")
|
||||
if city then return city .. "_2F" end
|
||||
if heal:match("_POKECENTER$") then return heal .. "_2F" end
|
||||
return FIRST_CENTER_2F
|
||||
end
|
||||
|
||||
-- pokefirered/src/overworld.c:1706 CB2_ContinueSavedGame
|
||||
local function use_continue_game_warp(session, mounted)
|
||||
local Bit = require("bit")
|
||||
@@ -87,6 +100,11 @@ local function use_continue_game_warp(session, mounted)
|
||||
return
|
||||
end
|
||||
session._continueWarpDeferred = nil
|
||||
if UNION_ROOMS[session.map] then
|
||||
session.map, session.x, session.y, session.facing =
|
||||
union_room_door_map(session), UNION_DOOR_X, UNION_DOOR_Y, "down"
|
||||
return
|
||||
end
|
||||
if session.map == HALL_OF_FAME_MAP then
|
||||
local Field = require("src.core.game3.field")
|
||||
if not mounted and not Field.flyDestinationsMounted() then
|
||||
@@ -111,6 +129,7 @@ local LINK_ROOMS = {
|
||||
FR_RECORD_CORNER = true,
|
||||
FR_BATTLE_COLOSSEUM_4P = true,
|
||||
FR_UNION_ROOM = true,
|
||||
FR_UNION_ROOM_PLAZA = true,
|
||||
}
|
||||
|
||||
-- pokefirered/src/load_save.c:149 SetContinueGameWarpStatusToDynamicWarp
|
||||
|
||||
@@ -1228,6 +1228,12 @@ function Adapters.host(mod, game, world)
|
||||
return v
|
||||
end
|
||||
local cx, cy = as_coord(x), as_coord(y)
|
||||
if mapId == "FR_UNION_ROOM" then
|
||||
local Plaza = require("src.core.game3.link.union_plaza_map")
|
||||
Plaza.ensure(resolveGame())
|
||||
mapId = Plaza.MAP_ID
|
||||
cx, cy = Plaza.entry()
|
||||
end
|
||||
local wid = tonumber(warpId)
|
||||
if (not cx or not cy) and wid and wid ~= 0xFF and wid >= 0 then
|
||||
local def = w and w.data and w.data.maps and w.data.maps[mapId]
|
||||
|
||||
@@ -379,6 +379,8 @@ function TrainerSight.check(game, specificTrainer)
|
||||
|
||||
if specificTrainer then
|
||||
local eo = specificTrainer
|
||||
-- src/trainer_see.c:94
|
||||
if Objs.find(eo.localId) ~= eo then return false end
|
||||
if eo.visible and not eo.hidden and not eo.scriptBusy and not eo.frozen then
|
||||
local sight = tonumber(eo.sight or (eo.def and (eo.def.sight or eo.def.trainerRange))) or 0
|
||||
if sight > 0 and TrainerSight.isTrainerType(eo)
|
||||
|
||||
@@ -64,6 +64,15 @@ function VirtualObjects.list()
|
||||
return out
|
||||
end
|
||||
|
||||
function VirtualObjects.slots()
|
||||
return #order
|
||||
end
|
||||
|
||||
function VirtualObjects.nth(i)
|
||||
local id = order[i]
|
||||
return id and byId[id] or nil
|
||||
end
|
||||
|
||||
function VirtualObjects.count()
|
||||
local n = 0
|
||||
for _ in pairs(byId) do n = n + 1 end
|
||||
|
||||
@@ -198,6 +198,14 @@ function SwitchDiagnostics.onPadReconcile()
|
||||
SwitchDiagnostics.maybeFlush(true)
|
||||
end
|
||||
|
||||
local function osName()
|
||||
local sys = love and love.system
|
||||
if not (sys and sys.getOS) then return nil end
|
||||
local ok, name = pcall(sys.getOS)
|
||||
if ok then return name end
|
||||
return nil
|
||||
end
|
||||
|
||||
function SwitchDiagnostics.onJoystickEvent(kind, joystick, button, extra)
|
||||
if not SwitchDiagnostics.isEnabled() then return end
|
||||
local payload = { button = button }
|
||||
@@ -209,9 +217,27 @@ function SwitchDiagnostics.onJoystickEvent(kind, joystick, button, extra)
|
||||
if extra then
|
||||
for k, v in pairs(extra) do payload[k] = v end
|
||||
end
|
||||
local PadHints = require("src.core.PadHints")
|
||||
payload.hf = tostring(PadHints.hasFocus())
|
||||
local fg = PadHints.foreground(osName())
|
||||
if fg then payload.fg = fg end
|
||||
SwitchDiagnostics.onEvent(kind, payload)
|
||||
end
|
||||
|
||||
function SwitchDiagnostics.onFocus(f)
|
||||
if not SwitchDiagnostics.isEnabled() then return end
|
||||
local PadHints = require("src.core.PadHints")
|
||||
local payload = {
|
||||
f = tostring(f),
|
||||
hasFocus = tostring(PadHints.hasFocus()),
|
||||
visible = tostring(not PadHints.windowMinimized()),
|
||||
}
|
||||
local fg = PadHints.foreground(osName())
|
||||
if fg then payload.fg = fg end
|
||||
SwitchDiagnostics.onEvent("focus", payload)
|
||||
SwitchDiagnostics.maybeFlush(true)
|
||||
end
|
||||
|
||||
function SwitchDiagnostics.logLuaError(msg)
|
||||
local filesystem = fs()
|
||||
if not filesystem then return nil end
|
||||
|
||||
@@ -19,10 +19,11 @@ CacheContract.VERSION_FORMAT = {
|
||||
silver = "rom-cache-v12:",
|
||||
crystal = "rom-cache-v12-crystal4:",
|
||||
-- engine/overworld/map_sprites.asm:181, engine/battle/animations.asm:2600
|
||||
yellow = "rom-cache-v12-yellow1:",
|
||||
-- data/pikachu/pikachu_pic_animation.asm:340
|
||||
yellow = "rom-cache-v12-yellow2:",
|
||||
-- v8: M4A tracks retain reachable patterns and explicit entry offsets.
|
||||
firered = "rom-cache-v16-firered:",
|
||||
leafgreen = "rom-cache-v1-leafgreen:",
|
||||
firered = "rom-cache-v17-firered:",
|
||||
leafgreen = "rom-cache-v2-leafgreen:",
|
||||
}
|
||||
CacheContract.MARKER_PATH = "rom-cache.complete"
|
||||
|
||||
@@ -49,6 +50,63 @@ CacheContract.VERSION_REQUIRED_FILES = {
|
||||
"assets/generated/battle/trainers/jessie_james.png",
|
||||
"assets/generated/battle/profoakb.png",
|
||||
"assets/generated/pikachu/pikapic_1.png",
|
||||
"assets/generated/pikachu/pikapic_2.png",
|
||||
"assets/generated/pikachu/pikapic_3.png",
|
||||
"assets/generated/pikachu/pikapic_4.png",
|
||||
"assets/generated/pikachu/pikapic_5.png",
|
||||
"assets/generated/pikachu/pikapic_6.png",
|
||||
"assets/generated/pikachu/pikapic_7.png",
|
||||
"assets/generated/pikachu/pikapic_8.png",
|
||||
"assets/generated/pikachu/pikapic_9.png",
|
||||
"assets/generated/pikachu/pikapic_10.png",
|
||||
"assets/generated/pikachu/pikapic_11.png",
|
||||
"assets/generated/pikachu/pikapic_12.png",
|
||||
"assets/generated/pikachu/pikapic_13.png",
|
||||
"assets/generated/pikachu/pikapic_14.png",
|
||||
"assets/generated/pikachu/pikapic_15.png",
|
||||
"assets/generated/pikachu/pikapic_16.png",
|
||||
"assets/generated/pikachu/pikapic_17.png",
|
||||
"assets/generated/pikachu/pikapic_18.png",
|
||||
"assets/generated/pikachu/pikapic_19.png",
|
||||
"assets/generated/pikachu/pikapic_20.png",
|
||||
"assets/generated/pikachu/pikapic_21.png",
|
||||
"assets/generated/pikachu/pikapic_22.png",
|
||||
"assets/generated/pikachu/pikapic_23.png",
|
||||
"assets/generated/pikachu/pikapic_24.png",
|
||||
"assets/generated/pikachu/pikapic_25.png",
|
||||
"assets/generated/pikachu/pikapic_26.png",
|
||||
"assets/generated/pikachu/pikapic_27.png",
|
||||
"assets/generated/pikachu/pikapic_28.png",
|
||||
"assets/generated/pikachu/gfx_e4841.png",
|
||||
"assets/generated/pikachu/gfx_e4ce0.png",
|
||||
"assets/generated/pikachu/gfx_e4e70.png",
|
||||
"assets/generated/pikachu/gfx_e50af.png",
|
||||
"assets/generated/pikachu/gfx_e52fe.png",
|
||||
"assets/generated/pikachu/gfx_e5541.png",
|
||||
"assets/generated/pikachu/gfx_e5794.png",
|
||||
"assets/generated/pikachu/gfx_e59ed.png",
|
||||
"assets/generated/pikachu/gfx_e5c4d.png",
|
||||
"assets/generated/pikachu/gfx_e5e90.png",
|
||||
"assets/generated/pikachu/gfx_e6020.png",
|
||||
"assets/generated/pikachu/gfx_e61b0.png",
|
||||
"assets/generated/pikachu/gfx_e63f7.png",
|
||||
"assets/generated/pikachu/gfx_e6646.png",
|
||||
"assets/generated/pikachu/gfx_e682f.png",
|
||||
"assets/generated/pikachu/gfx_e69bf.png",
|
||||
"assets/generated/pikachu/gfx_e6b4f.png",
|
||||
"assets/generated/pikachu/gfx_e6cdf.png",
|
||||
"assets/generated/pikachu/gfx_e6e6f.png",
|
||||
"assets/generated/pikachu/gfx_e6fff.png",
|
||||
"assets/generated/pikachu/gfx_e718f.png",
|
||||
"assets/generated/pikachu/gfx_e731f.png",
|
||||
"assets/generated/pikachu/gfx_e74af.png",
|
||||
"assets/generated/pikachu/gfx_e763f.png",
|
||||
"assets/generated/pikachu/gfx_e7863.png",
|
||||
"assets/generated/pikachu/gfx_e79f3.png",
|
||||
"assets/generated/pikachu/gfx_e7b83.png",
|
||||
"assets/generated/pikachu/gfx_e7d13.png",
|
||||
"assets/generated/pikachu/gfx_f0b64.png",
|
||||
"assets/generated/pikachu/gfx_f0d82.png",
|
||||
"assets/generated/minigame/surf_1a.png",
|
||||
"assets/generated/minigame/surf_1b.png",
|
||||
"assets/generated/minigame/surf_1c.png",
|
||||
|
||||
@@ -344,20 +344,12 @@ local function coreRows(opts, hooks)
|
||||
|
||||
local okSpd, GameSpeed = pcall(require, "src.core.GameSpeed")
|
||||
if okSpd then
|
||||
-- Per-category (RFC 0007): overworld/battle/menu each cycle their own
|
||||
-- multiplier, mirroring OptionsMenu.lua's three rows.
|
||||
add(Strings("OVERWORLD SPEED"),
|
||||
function() return GameSpeed.levelLabel(opts.speedOverworld) end,
|
||||
function(dir)
|
||||
opts.speedOverworld = GameSpeed.cycle(opts.speedOverworld, dir)
|
||||
return true
|
||||
end)
|
||||
add(Strings("BATTLE SPEED"),
|
||||
function() return GameSpeed.levelLabel(opts.speedBattle) end,
|
||||
function(dir)
|
||||
opts.speedBattle = GameSpeed.cycle(opts.speedBattle, dir)
|
||||
return true
|
||||
end)
|
||||
add(Strings("MENU SPEED"),
|
||||
function() return GameSpeed.levelLabel(opts.speedMenu) end,
|
||||
function(dir)
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
local LauncherSplash = {}
|
||||
LauncherSplash.__index = LauncherSplash
|
||||
|
||||
local DURATION = 127 / 30
|
||||
local FADE_SECONDS = 0.75
|
||||
local REVEAL_SECONDS = 0.65
|
||||
|
||||
local function smoothstep(value)
|
||||
local t = math.max(0, math.min(1, value))
|
||||
return t * t * (3 - 2 * t)
|
||||
end
|
||||
|
||||
function LauncherSplash.new()
|
||||
local self = setmetatable({ elapsed = 0, age = 0, opacity = 1 }, LauncherSplash)
|
||||
local ok, err = pcall(function()
|
||||
self.video = love.graphics.newVideo("assets/launcher/splash.ogv", {
|
||||
audio = love.audio ~= nil,
|
||||
})
|
||||
self.video:setFilter("linear", "linear")
|
||||
local width, height = self.video:getDimensions()
|
||||
self.canvas = love.graphics.newCanvas(width, height)
|
||||
self.canvas:setFilter("linear", "linear")
|
||||
self.shader = love.graphics.newShader([[
|
||||
extern number opacity;
|
||||
vec4 effect(vec4 color, Image texture, vec2 uv, vec2 screen) {
|
||||
vec4 pixel = Texel(texture, uv);
|
||||
float brightness = max(pixel.r, max(pixel.g, pixel.b));
|
||||
float coverage = smoothstep(0.015, 0.10, brightness);
|
||||
return vec4(pixel.rgb, pixel.a * coverage * opacity) * color;
|
||||
}
|
||||
]])
|
||||
local options = require("src.core.SaveData").loadOptions()
|
||||
self.volume = math.max(0, math.min(7, tonumber(options.sfxVol) or 7)) / 7
|
||||
local source = self.video:getSource()
|
||||
if source then source:setVolume(self.volume) end
|
||||
self.video:play()
|
||||
end)
|
||||
if not ok then
|
||||
self:release()
|
||||
print("[launcher splash] unavailable: " .. tostring(err))
|
||||
return nil
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function LauncherSplash:update(dt)
|
||||
self.age = self.age + dt
|
||||
self.elapsed = self.video:tell()
|
||||
self.opacity = 1 - smoothstep((self.elapsed - (DURATION - FADE_SECONDS)) / FADE_SECONDS)
|
||||
local source = self.video:getSource()
|
||||
if source then source:setVolume(self.volume * self.opacity) end
|
||||
-- A decoder that stalls must not prevent access to the launcher.
|
||||
return self.elapsed >= DURATION - 0.035 or not self.video:isPlaying()
|
||||
or self.age > DURATION + 2
|
||||
end
|
||||
|
||||
function LauncherSplash:draw()
|
||||
local g = love.graphics
|
||||
local width, height = g.getDimensions()
|
||||
local vw, vh = self.video:getDimensions()
|
||||
local scale = width / vw
|
||||
local backdrop = 1 - smoothstep((self.elapsed - DURATION / 2) / REVEAL_SECONDS)
|
||||
|
||||
g.push("all")
|
||||
g.origin()
|
||||
g.setScissor()
|
||||
g.setShader()
|
||||
g.setBlendMode("alpha", "alphamultiply")
|
||||
g.setColor(0, 0, 0, backdrop)
|
||||
g.rectangle("fill", 0, 0, width, height)
|
||||
|
||||
-- Video supplies YUV planes, so convert through a canvas before keying RGB.
|
||||
local target = g.getCanvas()
|
||||
g.setCanvas(self.canvas)
|
||||
g.clear(0, 0, 0, 0)
|
||||
g.setColor(1, 1, 1, 1)
|
||||
g.draw(self.video, 0, 0)
|
||||
g.setCanvas(target)
|
||||
self.shader:send("opacity", self.opacity)
|
||||
g.setShader(self.shader)
|
||||
g.draw(self.canvas, 0, (height - vh * scale) / 2, 0, scale, scale)
|
||||
g.pop()
|
||||
end
|
||||
|
||||
function LauncherSplash:release()
|
||||
if self.video then
|
||||
self.video:pause()
|
||||
local source = self.video:getSource()
|
||||
if source then source:stop() end
|
||||
end
|
||||
for _, key in ipairs({ "video", "canvas", "shader" }) do
|
||||
local resource = self[key]
|
||||
if resource then resource:release(); self[key] = nil end
|
||||
end
|
||||
end
|
||||
|
||||
return LauncherSplash
|
||||
@@ -2137,10 +2137,7 @@ function RomExtractor:extractField()
|
||||
-- (PikaAnimTilemap_1, column order) paints. Scripts 18/22/23/24 have no
|
||||
-- compressed base and take PikaPicAnimBGFrames_5 -> PikaAnimTilemap_9,
|
||||
-- which is ROW order over a raw 25-tile sheet, hence no columns flag.
|
||||
-- Script 26 shares script 11's base. The pikaframe overlays each script
|
||||
-- draws ON TOP of the base are a second full pose out of the same blob and
|
||||
-- stay unripped: PikachuFollower.picLift stands in for their motion
|
||||
-- (#561, still on #407's stand-in).
|
||||
-- Script 26 shares script 11's base.
|
||||
local PIKAPIC_BASE = {
|
||||
"Pic_e4000", "Pic_e411c", "Pic_e4272", "Pic_e4383", "Pic_e458b",
|
||||
"Pic_e467b", "Pic_e476e", "Pic_e49d1", "Pic_e4b39", "Pic_e4c3e",
|
||||
@@ -2158,6 +2155,17 @@ function RomExtractor:extractField()
|
||||
self:writeCompressedPic(label, path)
|
||||
end
|
||||
end
|
||||
-- data/pikachu/pikachu_pic_animation.asm:340
|
||||
for _, label in ipairs({
|
||||
"GFX_e4841", "GFX_e4ce0", "GFX_e4e70", "GFX_e50af", "GFX_e52fe",
|
||||
"GFX_e5541", "GFX_e5794", "GFX_e59ed", "GFX_e5c4d", "GFX_e5e90",
|
||||
"GFX_e6020", "GFX_e61b0", "GFX_e63f7", "GFX_e6646", "GFX_e682f",
|
||||
"GFX_e69bf", "GFX_e6b4f", "GFX_e6cdf", "GFX_e6e6f", "GFX_e6fff",
|
||||
"GFX_e718f", "GFX_e731f", "GFX_e74af", "GFX_e763f", "GFX_e7863",
|
||||
"GFX_e79f3", "GFX_e7b83", "GFX_e7d13", "GFX_f0b64", "GFX_f0d82",
|
||||
}) do
|
||||
self:raw2bpp(label, 40, 40, "pikachu/" .. label:lower() .. ".png")
|
||||
end
|
||||
end
|
||||
|
||||
self:raw1bpp("LedgeHoppingShadow", 8, 8,
|
||||
|
||||
@@ -443,7 +443,7 @@ function Extract.run(imports, cache, progressCb)
|
||||
for _, mapId in ipairs(mapOrder) do
|
||||
local conns = romConns[mapId] or {}
|
||||
local fixed = {}
|
||||
for dir, c in pairs(conns) do
|
||||
for _, c in ipairs(conns) do
|
||||
local dest = c.map
|
||||
if (not dest or dest:match("^g%d+_m%d+$")) and c.mapGroup ~= nil then
|
||||
dest = MapCatalog.mapIdFor(c.mapGroup, c.mapNum) or dest
|
||||
@@ -451,7 +451,7 @@ function Extract.run(imports, cache, progressCb)
|
||||
dest = MapCatalog.resolve(dest) or dest
|
||||
end
|
||||
if dest then
|
||||
fixed[dir] = { map = dest, offset = tonumber(c.offset) or 0 }
|
||||
fixed[#fixed + 1] = { dir = c.dir, map = dest, offset = tonumber(c.offset) or 0 }
|
||||
end
|
||||
end
|
||||
connections[mapId] = fixed
|
||||
@@ -533,13 +533,9 @@ function Extract.run(imports, cache, progressCb)
|
||||
for _, mapId in ipairs(mapOrder) do
|
||||
local conns = connections[mapId] or {}
|
||||
cl[#cl + 1] = (" %s = {\n"):format(mapId)
|
||||
local dirs = {}
|
||||
for dir in pairs(conns) do dirs[#dirs + 1] = dir end
|
||||
table.sort(dirs)
|
||||
for _, dir in ipairs(dirs) do
|
||||
local c = conns[dir]
|
||||
cl[#cl + 1] = (" %s = { map = %q, offset = %d },\n"):format(
|
||||
dir, c.map, tonumber(c.offset) or 0)
|
||||
for _, c in ipairs(conns) do
|
||||
cl[#cl + 1] = (" { dir = %q, map = %q, offset = %d },\n"):format(
|
||||
c.dir, c.map, tonumber(c.offset) or 0)
|
||||
end
|
||||
cl[#cl + 1] = " },\n"
|
||||
end
|
||||
@@ -1844,7 +1840,7 @@ local function _dormant_quantize_run(imports, cache, progressCb)
|
||||
for _, mapId in ipairs(mapOrder) do
|
||||
local conns = romConns[mapId] or {}
|
||||
local fixed = {}
|
||||
for dir, c in pairs(conns) do
|
||||
for _, c in ipairs(conns) do
|
||||
local dest = c.map
|
||||
if (not dest or dest:match("^g%d+_m%d+$")) and c.mapGroup ~= nil then
|
||||
dest = MapCatalog.mapIdFor(c.mapGroup, c.mapNum) or dest
|
||||
@@ -1852,7 +1848,7 @@ local function _dormant_quantize_run(imports, cache, progressCb)
|
||||
dest = MapCatalog.resolve(dest) or dest
|
||||
end
|
||||
if dest then
|
||||
fixed[dir] = { map = dest, offset = tonumber(c.offset) or 0 }
|
||||
fixed[#fixed + 1] = { dir = c.dir, map = dest, offset = tonumber(c.offset) or 0 }
|
||||
end
|
||||
end
|
||||
connections[mapId] = fixed
|
||||
@@ -1885,13 +1881,9 @@ local function _dormant_quantize_run(imports, cache, progressCb)
|
||||
for _, mapId in ipairs(mapOrder) do
|
||||
local conns = connections[mapId] or {}
|
||||
cl[#cl + 1] = (" %s = {\n"):format(mapId)
|
||||
local dirs = {}
|
||||
for dir in pairs(conns) do dirs[#dirs + 1] = dir end
|
||||
table.sort(dirs)
|
||||
for _, dir in ipairs(dirs) do
|
||||
local c = conns[dir]
|
||||
cl[#cl + 1] = (" %s = { map = %q, offset = %d },\n"):format(
|
||||
dir, c.map, tonumber(c.offset) or 0)
|
||||
for _, c in ipairs(conns) do
|
||||
cl[#cl + 1] = (" { dir = %q, map = %q, offset = %d },\n"):format(
|
||||
c.dir, c.map, tonumber(c.offset) or 0)
|
||||
end
|
||||
cl[#cl + 1] = " },\n"
|
||||
end
|
||||
|
||||
@@ -351,7 +351,7 @@ function ExtractMapEvents.parseConnections(rom, connectionsPtr)
|
||||
local dirName = CONN_DIR[direction]
|
||||
local destMap = Versions.frMapFor(mapGroup, mapNum)
|
||||
if dirName and destMap then
|
||||
out[dirName] = { map = destMap, offset = offset }
|
||||
out[#out + 1] = { dir = dirName, map = destMap, offset = offset }
|
||||
end
|
||||
end
|
||||
return out
|
||||
|
||||
@@ -149,7 +149,8 @@ function MapTree.parseConnections(rom, connectionsPtr, groupsData)
|
||||
local maps = groupsData.groups[mapGroup].maps
|
||||
pretName = maps and maps[mapNum + 1]
|
||||
end
|
||||
out[dirName] = {
|
||||
out[#out + 1] = {
|
||||
dir = dirName,
|
||||
mapGroup = mapGroup,
|
||||
mapNum = mapNum,
|
||||
offset = offset,
|
||||
|
||||
@@ -43,7 +43,7 @@ Versions.ROM_SIZE = 16777216
|
||||
-- v115: LeafGreen profiles, edition-specific title assets and Deoxys stats.
|
||||
-- v121: chrome/fonts/japanese_{normal,small}_* and japanese_widths.lua, the
|
||||
-- cart's Japanese fonts, for text a Japanese translation mod prints.
|
||||
Versions.CACHE_VERSION = 125
|
||||
Versions.CACHE_VERSION = 126
|
||||
Versions.NATIVE_VERSION = 6
|
||||
Versions.OW_VERSION = 3
|
||||
Versions.ANIM_VERSION = 1
|
||||
|
||||
+5
-1
@@ -327,7 +327,7 @@ local MAX_TEAM = 6
|
||||
local MAX_DEADLINES = 8
|
||||
local MAX_U32 = 4294967295
|
||||
local MAX_SEATS = 5
|
||||
local MAX_PLAZA = 9
|
||||
local MAX_PLAZA = 40
|
||||
local MAX_DIRECT = 64
|
||||
local MAX_GROUPS = 64
|
||||
local MAX_AVATAR_NAME = 16
|
||||
@@ -979,6 +979,7 @@ end
|
||||
SCHEMAS.plaza_join = function(m)
|
||||
return {
|
||||
kind = Wire.str(m.kind, nil, MAX_NAME),
|
||||
cap = Wire.num(m.cap, nil, 1, MAX_PLAZA),
|
||||
profile = profile(m.profile),
|
||||
avatar = Wire.avatar(m.avatar),
|
||||
}
|
||||
@@ -992,6 +993,8 @@ SCHEMAS.plaza_state = function(m)
|
||||
return {
|
||||
kind = Wire.str(m.kind, nil, MAX_NAME),
|
||||
instance = Wire.num(m.instance, nil, 0, MAX_INT),
|
||||
cap = Wire.num(m.cap, nil, 1, MAX_PLAZA),
|
||||
rev = Wire.num(m.rev, nil, 0, MAX_INT),
|
||||
you = Wire.num(m.you, nil, 1, MAX_PLAZA),
|
||||
members = type(m.members) == "table" and Wire.list(m.members, MAX_PLAZA, Wire.member)
|
||||
or nil,
|
||||
@@ -1002,6 +1005,7 @@ SCHEMAS.plaza_delta = function(m)
|
||||
return {
|
||||
kind = Wire.str(m.kind, nil, MAX_NAME),
|
||||
instance = Wire.num(m.instance, nil, 0, MAX_INT),
|
||||
rev = Wire.num(m.rev, nil, 0, MAX_INT),
|
||||
joined = Wire.list(m.joined, MAX_PLAZA, Wire.member),
|
||||
left = idList(m.left, MAX_PLAZA),
|
||||
changed = Wire.list(m.changed, MAX_PLAZA, Wire.member),
|
||||
|
||||
+16
-1
@@ -903,6 +903,7 @@ COVERAGE["src.core.Game"] = {
|
||||
-- ------- src.world.Map
|
||||
|
||||
local MapView = {}
|
||||
local connViews = setmetatable({}, { __mode = "k" })
|
||||
|
||||
local function collision()
|
||||
return package.loaded["src.core.game3.collision"]
|
||||
@@ -919,7 +920,21 @@ function MapView.__index(self, key)
|
||||
if key == "widthCells" then return def and def.width end
|
||||
if key == "heightCells" then return def and def.height end
|
||||
if key == "warps" then return def and def.warps end
|
||||
if key == "connections" then return def and def.connections end
|
||||
if key == "connections" then
|
||||
if not def then return nil end
|
||||
local hit = connViews[def]
|
||||
if hit and hit.src == def.connections then return hit.view end
|
||||
local list = require("src.core.game3.connections").each(def)
|
||||
local view = setmetatable(list, { __index = function(t, dir)
|
||||
if type(dir) ~= "string" then return nil end
|
||||
for i = 1, #t do
|
||||
if t[i].dir == dir then return t[i] end
|
||||
end
|
||||
return nil
|
||||
end })
|
||||
connViews[def] = { src = def.connections, view = view }
|
||||
return view
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
|
||||
+4
-1
@@ -258,11 +258,14 @@ function Loader:_installDevShim()
|
||||
-- doing it is the hole this closes, and any future path that runs mod
|
||||
-- code without a sandbox env still lands here.
|
||||
local owner = Runtime.currentMod or Runtime.modRequire
|
||||
if owner or callerIsMod(3) then
|
||||
if owner then
|
||||
local id = type(owner) == "string" and owner or nil
|
||||
local denial = Sandbox.moduleDenial(name, devShim.permissions[id])
|
||||
or (id and crossGenerationDenial(name, devShim.generation))
|
||||
if denial then error(("[%s] %s"):format(id or "mod", denial), 0) end
|
||||
else
|
||||
local denial = Sandbox.moduleDenial(name, nil)
|
||||
if denial and callerIsMod(3) then error(("[mod] %s"):format(denial), 0) end
|
||||
end
|
||||
if devShim.dev or devShim.generation ~= 1 then scanRequire(name) end
|
||||
-- The Gen 1 name a mod asked for, answered by this generation's compat arm.
|
||||
|
||||
+12
-5
@@ -143,6 +143,11 @@ function Client.upgradeRequired() return S.upgrade end
|
||||
function Client.invites() return S.invites end
|
||||
function Client.outgoing() return S.outgoing end
|
||||
function Client.plaza() return S.plaza end
|
||||
function Client.plazaRev()
|
||||
local p = S.plaza
|
||||
if not p then return nil end
|
||||
return p.instance, p.rev
|
||||
end
|
||||
function Client.plazaCounts() return S.plazaCounts end
|
||||
function Client.group() return S.group end
|
||||
function Client.direct() return S.direct end
|
||||
@@ -747,6 +752,7 @@ local function applyPlazaDelta(msg)
|
||||
and msg.instance ~= plaza.instance then
|
||||
return false
|
||||
end
|
||||
if type(msg.rev) == "number" then plaza.rev = msg.rev end
|
||||
local gone = {}
|
||||
for _, id in ipairs(msg.left or {}) do gone[id] = true end
|
||||
local members = {}
|
||||
@@ -831,7 +837,7 @@ local function resendState()
|
||||
if S.presence then sendRaw(Protocol2.presence(S.presence)) end
|
||||
for _, kind in ipairs({ "union", "wireless" }) do
|
||||
local j = S.plazaJoins[kind]
|
||||
if j then sendRaw(Protocol2.plazaJoin(kind, j.profile, j.avatar)) end
|
||||
if j then sendRaw(Protocol2.plazaJoin(kind, j.profile, j.avatar, j.cap)) end
|
||||
end
|
||||
local board = S.presence and S.presence.board
|
||||
if S.plazaJoins.union and type(board) == "table" then
|
||||
@@ -1051,7 +1057,7 @@ local function handle(msg)
|
||||
emit("invite_closed", msg)
|
||||
elseif kind == "plaza_state" then
|
||||
S.plaza = { kind = msg.kind, instance = msg.instance, you = msg.you,
|
||||
members = sortMembers(msg.members) }
|
||||
cap = msg.cap, rev = msg.rev, members = sortMembers(msg.members) }
|
||||
emit("plaza", S.plaza)
|
||||
elseif kind == "plaza_delta" then
|
||||
if applyPlazaDelta(msg) then emit("plaza", S.plaza) end
|
||||
@@ -1398,10 +1404,11 @@ function Client.replyInvite(id, accept)
|
||||
return sendRaw(Protocol2.inviteReply(id, accept == true))
|
||||
end
|
||||
|
||||
function Client.joinPlaza(kind, profile, avatar)
|
||||
function Client.joinPlaza(kind, profile, avatar, cap)
|
||||
profile = profile or defaultProfile(3)
|
||||
S.plazaJoins[kind] = { profile = profile, avatar = avatar }
|
||||
return sendRaw(Protocol2.plazaJoin(kind, profile, avatar))
|
||||
if cap == nil and kind == "union" then cap = Protocol2.PLAZA_CAP end
|
||||
S.plazaJoins[kind] = { profile = profile, avatar = avatar, cap = cap }
|
||||
return sendRaw(Protocol2.plazaJoin(kind, profile, avatar, cap))
|
||||
end
|
||||
|
||||
function Client.leavePlaza(kind)
|
||||
|
||||
@@ -98,9 +98,11 @@ function Protocol2.inviteReply(id, accept)
|
||||
return build({ type = "invite_reply", id = id, accept = accept == true })
|
||||
end
|
||||
|
||||
function Protocol2.plazaJoin(kind, profile, avatar)
|
||||
Protocol2.PLAZA_CAP = 40
|
||||
|
||||
function Protocol2.plazaJoin(kind, profile, avatar, cap)
|
||||
return build({ type = "plaza_join", kind = kind, profile = profile,
|
||||
avatar = avatar })
|
||||
avatar = avatar, cap = cap })
|
||||
end
|
||||
|
||||
function Protocol2.plazaLeave(kind)
|
||||
@@ -399,11 +401,14 @@ end
|
||||
VALIDATORS.plaza_state = function(m)
|
||||
if type(m.members) ~= "table" then return nil, "plaza_state without members" end
|
||||
if type(m.kind) ~= "string" then return nil, "plaza_state without a kind" end
|
||||
if m.rev ~= nil and type(m.rev) ~= "number" then return nil, "plaza_state rev is not a number" end
|
||||
if m.cap ~= nil and type(m.cap) ~= "number" then return nil, "plaza_state cap is not a number" end
|
||||
return m
|
||||
end
|
||||
|
||||
VALIDATORS.plaza_delta = function(m)
|
||||
if type(m.kind) ~= "string" then return nil, "plaza_delta without a kind" end
|
||||
if m.rev ~= nil and type(m.rev) ~= "number" then return nil, "plaza_delta rev is not a number" end
|
||||
return m
|
||||
end
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ local Runtime = require("src.mods.Runtime")
|
||||
local BattleTransition = {}
|
||||
BattleTransition.__index = BattleTransition
|
||||
BattleTransition.isOpaque = false -- draws over the frozen overworld
|
||||
BattleTransition.isFixedSpeed = true
|
||||
|
||||
-- BattleTransition_FlashScreenPalettes: fade to black and back, then to
|
||||
-- white and back; each palette held 2 frames, whole sequence played 3
|
||||
|
||||
@@ -73,8 +73,8 @@ function PaletteFX.darkObp(colors, group)
|
||||
end
|
||||
|
||||
-- the same shift folded into a baked 8-group world palette array (ADVANCED)
|
||||
local function darkGroups(groups)
|
||||
if not (groups and darkWorld) then return groups end
|
||||
local function darkGroups(groups, lit)
|
||||
if lit or not (groups and darkWorld) then return groups end
|
||||
local out = {}
|
||||
for i = 1, #groups do
|
||||
out[i] = PaletteFX.permute(groups[i], PaletteFX.DARK_BGP)
|
||||
@@ -716,11 +716,11 @@ end
|
||||
-- slot swapped to the current town/route (Route 6's north end uses
|
||||
-- Saffron's roof colors while the player stands in its top 2 cell rows,
|
||||
-- like pokered's wYCoord check -- data is Game.data, for the map lookup)
|
||||
function PaletteFX.worldGroupColors(data, tileset, mapId, playerCellY)
|
||||
function PaletteFX.worldGroupColors(data, tileset, mapId, playerCellY, lit)
|
||||
local w = PaletteFX.worldPack()
|
||||
local base = w and w.groupColors[tileset]
|
||||
if not base then return nil end
|
||||
if not w.roofGroup[tileset] then return darkGroups(base) end
|
||||
if not w.roofGroup[tileset] then return darkGroups(base, lit) end
|
||||
local roofMapId = mapId
|
||||
if mapId == ROUTE_6_SAFFRON.mapId and playerCellY
|
||||
and playerCellY < ROUTE_6_SAFFRON.cellYBelow then
|
||||
@@ -728,7 +728,7 @@ function PaletteFX.worldGroupColors(data, tileset, mapId, playerCellY)
|
||||
end
|
||||
local roofMap = data and data.maps and data.maps[roofMapId]
|
||||
local roof = roofMap and w.roofByMapIndex[roofMap.index]
|
||||
if not roof then return darkGroups(base) end
|
||||
if not roof then return darkGroups(base, lit) end
|
||||
local out = {}
|
||||
for i = 1, 8 do out[i] = base[i] end
|
||||
-- LoadTownPalette only overwrites W2_BgPaletteData + $32, i.e. colors 1
|
||||
@@ -738,7 +738,7 @@ function PaletteFX.worldGroupColors(data, tileset, mapId, playerCellY)
|
||||
-- material's 2 middle shades are town-specific
|
||||
local base4 = base[ROOF_GROUP + 1]
|
||||
out[ROOF_GROUP + 1] = { base4[1], roof[1], roof[2], base4[4] }
|
||||
return darkGroups(out)
|
||||
return darkGroups(out, lit)
|
||||
end
|
||||
|
||||
-- an overworld sprite's resolved 4-color OBJ palette (ColorOverworldSprite),
|
||||
|
||||
+121
-13
@@ -185,11 +185,11 @@ TileRenderer.recolorSample = recolorSample
|
||||
-- the 8 shifted variants of one tile (built once per sheet + tile id [+
|
||||
-- gbcKey, when `colors` recolors it for RED++ -- see buildAnim])
|
||||
local shiftVariants = {}
|
||||
local function getShiftVariants(tilesetImagePath, perRow, tile, colors, gbcKey)
|
||||
local function getShiftVariants(tilesetImagePath, perRow, tile, colors, gbcKey, own)
|
||||
local key = tilesetImagePath .. "#" .. tile .. (gbcKey or "")
|
||||
if shiftVariants[key] ~= nil then return shiftVariants[key] end
|
||||
if not own and shiftVariants[key] ~= nil then return shiftVariants[key] end
|
||||
if not (love.image and love.image.newImageData) then
|
||||
shiftVariants[key] = false
|
||||
if not own then shiftVariants[key] = false end
|
||||
return false
|
||||
end
|
||||
local id = Assets.imageData(tilesetImagePath)
|
||||
@@ -207,14 +207,14 @@ local function getShiftVariants(tilesetImagePath, perRow, tile, colors, gbcKey)
|
||||
end
|
||||
out[o + 1] = love.graphics.newImage(v)
|
||||
end
|
||||
shiftVariants[key] = out
|
||||
if not own then shiftVariants[key] = out end
|
||||
return out
|
||||
end
|
||||
|
||||
local frameImages = {}
|
||||
local function getFrameImages(paths, colors, gbcKey)
|
||||
local function getFrameImages(paths, colors, gbcKey, own)
|
||||
local key = table.concat(paths, "|") .. (gbcKey or "")
|
||||
if frameImages[key] ~= nil then return frameImages[key] end
|
||||
if not own and frameImages[key] ~= nil then return frameImages[key] end
|
||||
local out = {}
|
||||
for i, path in ipairs(paths) do
|
||||
local ok, img = pcall(function()
|
||||
@@ -234,12 +234,12 @@ local function getFrameImages(paths, colors, gbcKey)
|
||||
return love.graphics.newImage(out2)
|
||||
end)
|
||||
if not ok then
|
||||
frameImages[key] = false
|
||||
if not own then frameImages[key] = false end
|
||||
return false
|
||||
end
|
||||
out[i] = img
|
||||
end
|
||||
frameImages[key] = out
|
||||
if not own then frameImages[key] = out end
|
||||
return out
|
||||
end
|
||||
|
||||
@@ -359,14 +359,14 @@ local function buildAnim(spec, tilesetImagePath, perRow, quads, gbc)
|
||||
local sequence = {}
|
||||
for i, offset in ipairs(offsets) do sequence[i] = offset + 1 end
|
||||
return { tiles = tiles, textures = textures, sequence = sequence,
|
||||
period = period }
|
||||
period = period, spec = gbc and spec or nil }
|
||||
elseif spec.kind == "frames" then
|
||||
local sequence = spec.sequence
|
||||
if not (spec.images and sequence and #sequence > 0) then return nil end
|
||||
local textures = getFrameImages(spec.images, colors, gbc and gbc.key)
|
||||
if not textures then return nil end
|
||||
return { tiles = tiles, textures = textures, sequence = sequence,
|
||||
period = period }
|
||||
period = period, spec = gbc and spec or nil }
|
||||
elseif spec.kind == "toggle" then
|
||||
if gbc then return nil end
|
||||
local image = getToggleImage(spec, tilesetImagePath, perRow)
|
||||
@@ -407,12 +407,24 @@ local function gbcKeyFor(mapId)
|
||||
return "#gbc:" .. mapId .. PaletteFX.darkKey()
|
||||
end
|
||||
|
||||
local function getGbcAtlas(imagePath, tilesetId, mapId, perRow, data)
|
||||
local function bgpGroupColors(data, tilesetId, mapId, bgp)
|
||||
local groupColors = PaletteFX.worldGroupColors(data, tilesetId, mapId, nil, bgp ~= nil)
|
||||
if groupColors and bgp then
|
||||
local shade = {}
|
||||
for i = 0, 3 do shade[i] = math.floor(bgp / (4 ^ i)) % 4 end
|
||||
local shaded = {}
|
||||
for i = 1, #groupColors do shaded[i] = PaletteFX.permute(groupColors[i], shade) end
|
||||
groupColors = shaded
|
||||
end
|
||||
return groupColors
|
||||
end
|
||||
|
||||
local function getGbcAtlas(imagePath, tilesetId, mapId, perRow, data, bgp)
|
||||
local key = imagePath .. gbcKeyFor(mapId)
|
||||
if gbcAtlasCache[key] ~= nil then return gbcAtlasCache[key] or nil end
|
||||
if not bgp and gbcAtlasCache[key] ~= nil then return gbcAtlasCache[key] or nil end
|
||||
local img = false
|
||||
if love.image and love.image.newImageData then
|
||||
local groupColors = PaletteFX.worldGroupColors(data, tilesetId, mapId, nil)
|
||||
local groupColors = bgpGroupColors(data, tilesetId, mapId, bgp)
|
||||
if groupColors then
|
||||
local src = Assets.imageData(imagePath)
|
||||
local iw, ih = src:getDimensions()
|
||||
@@ -458,6 +470,7 @@ local function getGbcAtlas(imagePath, tilesetId, mapId, perRow, data)
|
||||
img = love.graphics.newImage(out)
|
||||
end
|
||||
end
|
||||
if bgp then return img or nil end
|
||||
gbcAtlasCache[key] = img
|
||||
return img or nil
|
||||
end
|
||||
@@ -881,6 +894,90 @@ end
|
||||
local function safeRelease(o)
|
||||
if o and o.release then pcall(o.release, o) end
|
||||
end
|
||||
local EMPTY = {}
|
||||
|
||||
-- engine/pikachu/pikachu_pic_animation.asm:845
|
||||
function TileRenderer:bgpImage(byte)
|
||||
if not (byte and self.gbcAtlas and self.data) then return nil end
|
||||
local base = self.baseImage or self.image
|
||||
if byte == 0xE4 and not PaletteFX.darkWorld() then return base end
|
||||
self.bgpImages = self.bgpImages or {}
|
||||
local img = self.bgpImages[byte]
|
||||
if img == nil then
|
||||
local ts = self.map.tileset
|
||||
local ok, made = pcall(getGbcAtlas, ts.image, ts.id, self.map.id,
|
||||
ts.tilesPerRow, self.data, byte)
|
||||
img = ok and made or false
|
||||
self.bgpImages[byte] = img
|
||||
if img then pcall(self.bakeBgpAnims, self, byte) end
|
||||
end
|
||||
return img or nil
|
||||
end
|
||||
|
||||
function TileRenderer:bakeBgpAnims(byte)
|
||||
local ctx = self.gbcCtx
|
||||
if not (ctx and self.anims) then return end
|
||||
local groupColors = bgpGroupColors(self.data, ctx.tilesetId, ctx.mapId, byte)
|
||||
if not groupColors then return end
|
||||
for _, anim in ipairs(self.anims) do
|
||||
local spec = anim.spec
|
||||
local group = spec and PaletteFX.worldGroupAt(ctx.tilesetId, ctx.mapId, anim.tiles[1])
|
||||
local colors = group and groupColors[group + 1]
|
||||
if colors then
|
||||
local textures
|
||||
if spec.kind == "hshift" then
|
||||
textures = getShiftVariants(ctx.imagePath, ctx.perRow, anim.tiles[1],
|
||||
colors, nil, true)
|
||||
else
|
||||
textures = getFrameImages(spec.images, colors, nil, true)
|
||||
end
|
||||
if textures then
|
||||
anim.bgpTextures = anim.bgpTextures or {}
|
||||
anim.bgpTextures[byte] = textures
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function TileRenderer:setBgp(byte)
|
||||
if byte == self.curBgp and byte ~= nil then return true end
|
||||
local before = self.image
|
||||
if self.curBgp then
|
||||
self.bgpBorders[self.curBgp] = self.borderFill
|
||||
self.image = self.baseImage
|
||||
self.borderFill, self.borderFillMode = self.baseBorder, self.baseBorderMode
|
||||
self.baseImage, self.baseBorder, self.baseBorderMode = nil, nil, nil
|
||||
self.curBgp = nil
|
||||
for _, anim in ipairs(self.anims or EMPTY) do
|
||||
if anim.baseTextures then
|
||||
anim.textures, anim.baseTextures = anim.baseTextures, nil
|
||||
end
|
||||
end
|
||||
end
|
||||
local shown = false
|
||||
local img = self:bgpImage(byte)
|
||||
if img and img == self.image then
|
||||
shown = true
|
||||
elseif img then
|
||||
self.bgpBorders = self.bgpBorders or {}
|
||||
self.baseImage = self.image
|
||||
self.baseBorder, self.baseBorderMode = self.borderFill, self.borderFillMode
|
||||
self.image = img
|
||||
self.borderFill = self.bgpBorders[byte]
|
||||
self.borderFillMode = self.borderFill and self.baseBorderMode or nil
|
||||
self.bgpBorders[byte] = nil
|
||||
self.curBgp = byte
|
||||
for _, anim in ipairs(self.anims or EMPTY) do
|
||||
local textures = anim.bgpTextures and anim.bgpTextures[byte]
|
||||
if textures then
|
||||
anim.baseTextures, anim.textures = anim.textures, textures
|
||||
end
|
||||
end
|
||||
shown = true
|
||||
end
|
||||
if self.winBatch and self.image ~= before then self.winBatch:setTexture(self.image) end
|
||||
return shown
|
||||
end
|
||||
|
||||
-- Release only the GPU objects this instance built and uniquely owns: the
|
||||
-- two SpriteBatches, the border-fill image and its quad, the per-tile
|
||||
@@ -890,6 +987,9 @@ end
|
||||
-- them. Used by :rebuild before it swaps in fresh batches, and by
|
||||
-- :release on eviction.
|
||||
function TileRenderer:releaseBatches()
|
||||
if self.curBgp then self:setBgp(nil) end
|
||||
for _, img in pairs(self.bgpBorders or {}) do safeRelease(img) end
|
||||
self.bgpBorders = nil
|
||||
safeRelease(self.winBatch); self.winBatch = nil
|
||||
safeRelease(self.borderFill); self.borderFill = nil
|
||||
safeRelease(self.borderQuad); self.borderQuad = nil
|
||||
@@ -928,6 +1028,14 @@ function TileRenderer:release()
|
||||
self.image = nil
|
||||
self.gbcAtlas = nil
|
||||
end
|
||||
for _, img in pairs(self.bgpImages or {}) do safeRelease(img) end
|
||||
self.bgpImages = nil
|
||||
for _, anim in ipairs(self.anims or {}) do
|
||||
for _, textures in pairs(anim.bgpTextures or {}) do
|
||||
for _, img in ipairs(textures) do safeRelease(img) end
|
||||
end
|
||||
anim.bgpTextures = nil
|
||||
end
|
||||
self.anims = nil
|
||||
end
|
||||
|
||||
|
||||
@@ -1123,10 +1123,12 @@ function GenSave.encode(save, data, template)
|
||||
for b = 7, 12 do encodeBoxRegion(O.box7 + (b - 7) * BOX_REGION_SIZE, boxes[b] or {}) end
|
||||
|
||||
-- map + position
|
||||
local px = math.floor(tonumber(save.player and save.player.x) or 0)
|
||||
local py = math.floor(tonumber(save.player and save.player.y) or 0)
|
||||
if save.player and save.player.map then
|
||||
setByte(buf, O.curMap, cw.mapsIndex[save.player.map] or 0)
|
||||
setByte(buf, O.yCoord, save.player.y or 0)
|
||||
setByte(buf, O.xCoord, save.player.x or 0)
|
||||
setByte(buf, O.yCoord, py)
|
||||
setByte(buf, O.xCoord, px)
|
||||
end
|
||||
if save.lastOutdoor and save.lastOutdoor.id then
|
||||
setByte(buf, O.lastMap, cw.mapsIndex[save.lastOutdoor.id] or 0)
|
||||
@@ -1137,10 +1139,10 @@ function GenSave.encode(save, data, template)
|
||||
-- zero-filled one boots into a garbled map on a silent hang (#889).
|
||||
--
|
||||
-- Rebuilt when there is no template at all (a save that began as a New Game
|
||||
-- in this port), and when the template was saved on a DIFFERENT map than the
|
||||
-- one the player is standing on now -- an imported save that has since been
|
||||
-- played carries the old map's header, which is just as unbootable. A
|
||||
-- template still on its own map keeps its bytes untouched: they are the
|
||||
-- in this port), and when the template was saved on a DIFFERENT map or cell
|
||||
-- than the one the player is standing on now -- an imported save that has
|
||||
-- since been played carries a stale map window, which is just as unbootable.
|
||||
-- A template still on its own cell keeps its bytes untouched: they are the
|
||||
-- game's own, including live NPC positions, and preserving them is what
|
||||
-- makes import -> export byte-identical.
|
||||
local mapId = save.player and save.player.map
|
||||
@@ -1151,10 +1153,11 @@ function GenSave.encode(save, data, template)
|
||||
-- map has no index at all rather than trusting a stale template
|
||||
local index = cw.mapsIndex[mapId]
|
||||
rebuild = index == nil or u8(src, O.curMap) ~= bit.band(index, 0xFF)
|
||||
or u8(src, O.yCoord) ~= bit.band(py, 0xFF)
|
||||
or u8(src, O.xCoord) ~= bit.band(px, 0xFF)
|
||||
end
|
||||
if rebuild then
|
||||
local ctx, why = MapContext.build(data, mapId,
|
||||
(save.player and save.player.x) or 0, (save.player and save.player.y) or 0)
|
||||
local ctx, why = MapContext.build(data, mapId, px, py)
|
||||
-- home/overworld.asm:2016 (#1691)
|
||||
if not ctx then
|
||||
error(("this save cannot be exported: %s"):format(tostring(why)), 0)
|
||||
|
||||
@@ -57,6 +57,10 @@ local O = {
|
||||
currentMapHeight2 = 557,
|
||||
currentMapWidth2 = 558,
|
||||
tilesetHeader = 564, -- wTilesetBank .. wGrassTile (11)
|
||||
grassRate = 1424, -- ram/wram.asm:2408 wGrassRate
|
||||
grassMons = 1425,
|
||||
waterRate = 1453,
|
||||
waterMons = 1454,
|
||||
}
|
||||
MapContext.OFFSETS = O
|
||||
|
||||
@@ -66,6 +70,7 @@ local MAX_OBJECT_EVENTS = 16
|
||||
local CONNECTION_STRUCT = 11 -- map_connection_struct (macros/ram.asm)
|
||||
local SPRITE_STRUCT = 16 -- one wSpriteStateData1/2 entry
|
||||
local NUM_SPRITE_STRUCTS = 16
|
||||
local NUM_WILDMONS = 10
|
||||
|
||||
-- wOverworldMap, the tile-block map the view pointer indexes. Recovered from
|
||||
-- the event_displacement formula below applied to a real save's coordinates,
|
||||
@@ -227,6 +232,36 @@ function MapContext.build(data, mapId, x, y)
|
||||
writes[O.mapMusicSoundID] = { id }
|
||||
writes[O.mapMusicROMBank] = { song.bank % 256 }
|
||||
|
||||
-- engine/overworld/wild_mons.asm:1
|
||||
if type(data.encounters) ~= "table" then
|
||||
return nil, "no wild encounter data (re-import the ROM)"
|
||||
end
|
||||
local enc = data.encounters[mapId]
|
||||
local pokemon = data.pokemon or {}
|
||||
local function wildRows(t)
|
||||
local out = {}
|
||||
for i = 1, NUM_WILDMONS do
|
||||
local slot = t.slots and t.slots[i]
|
||||
local def = slot and pokemon[slot.species]
|
||||
if not (slot and def and def.index) then return nil end
|
||||
out[#out + 1] = (tonumber(slot.level) or 0) % 256
|
||||
out[#out + 1] = def.index % 256
|
||||
end
|
||||
return out
|
||||
end
|
||||
for _, kind in ipairs({ "grass", "water" }) do
|
||||
local t = type(enc) == "table" and enc[kind] or nil
|
||||
local rate = t and tonumber(t.rate) or 0
|
||||
writes[O[kind .. "Rate"]] = { rate % 256 }
|
||||
if rate ~= 0 then
|
||||
local rows = wildRows(t)
|
||||
if not rows then
|
||||
return nil, ("bad %s wild data for %s (re-import the ROM)"):format(kind, tostring(mapId))
|
||||
end
|
||||
writes[O[kind .. "Mons"]] = rows
|
||||
end
|
||||
end
|
||||
|
||||
-- Player position within its block, and the upper-left corner of the view.
|
||||
-- The pointer is the same expression the warp_to tables are assembled with
|
||||
-- (macros/coords.asm event_displacement).
|
||||
|
||||
@@ -94,6 +94,7 @@ local DATA_MODULES = {
|
||||
-- rebuild the current map's engine state on export (#889)
|
||||
tilesets = { "data.generated.tilesets", "data/generated/tilesets.lua" },
|
||||
audio = { "data.generated.audio", "data/generated/audio.lua" },
|
||||
encounters = { "data.generated.encounters", "data/generated/encounters.lua" },
|
||||
charmap = { "src.save_convert.data.charmap", "src/save_convert/data/charmap.lua" },
|
||||
eventFlags = { "src.save_convert.data.event_flags", "src/save_convert/data/event_flags.lua" },
|
||||
toggleObjects = { "src.save_convert.data.toggle_objects", "src/save_convert/data/toggle_objects.lua" },
|
||||
@@ -253,14 +254,6 @@ local function defaultsSave()
|
||||
defeatedTrainers = {},
|
||||
repelSteps = 0,
|
||||
modData = {},
|
||||
options = {
|
||||
textSpeed = 3, animations = true, battleStyle = "shift",
|
||||
battleLayout = "og",
|
||||
ruleset = "gen1_faithful", musicVol = 7, sfxVol = 7, pikaVol = 7,
|
||||
musicFilter = 0,
|
||||
speed = 1, colors = "gbc", tilt = 0,
|
||||
videoMode = "windowed", mods = {},
|
||||
},
|
||||
}
|
||||
end
|
||||
|
||||
|
||||
@@ -207,6 +207,9 @@ function SyncEngine.defaultSaves()
|
||||
playthroughId = id,
|
||||
format = meta.format,
|
||||
engine = meta.engine,
|
||||
-- Derive from the saved set, including older saves that
|
||||
-- predate modCount, rather than this device's current mods.
|
||||
modCount = #(type(meta.mods) == "table" and meta.mods or {}),
|
||||
playTime = tonumber(save.playTime),
|
||||
summary = {
|
||||
name = name,
|
||||
|
||||
+2
-14
@@ -529,10 +529,7 @@ local function buildRows(game)
|
||||
return true
|
||||
end },
|
||||
-- fast-forward the logic clock only; music and sfx keep their tempo
|
||||
-- (src/core/GameSpeed.lua), so this is safe to leave on. Per-category
|
||||
-- (RFC 0007): overworld walking, battle turns and menu navigation each
|
||||
-- cycle their own multiplier -- GameSpeed.CATEGORIES is the single
|
||||
-- source of truth for which three rows exist.
|
||||
-- (src/core/GameSpeed.lua), so this is safe to leave on.
|
||||
{ id = "speedOverworld", label = Strings("OVERWORLD SPEED"),
|
||||
value = function(g)
|
||||
return gameSpeedLabel(g.save.options.speedOverworld)
|
||||
@@ -542,15 +539,6 @@ local function buildRows(game)
|
||||
o.speedOverworld = GameSpeed.cycle(o.speedOverworld, dir)
|
||||
return true
|
||||
end },
|
||||
{ id = "speedBattle", label = Strings("BATTLE SPEED"),
|
||||
value = function(g)
|
||||
return gameSpeedLabel(g.save.options.speedBattle)
|
||||
end,
|
||||
step = function(g, dir)
|
||||
local o = g.save.options
|
||||
o.speedBattle = GameSpeed.cycle(o.speedBattle, dir)
|
||||
return true
|
||||
end },
|
||||
{ id = "speedMenu", label = Strings("MENU SPEED"),
|
||||
value = function(g)
|
||||
return gameSpeedLabel(g.save.options.speedMenu)
|
||||
@@ -726,7 +714,7 @@ local GROUPS = {
|
||||
members = { "uiLayout", "videoMode", "orientation", "faithfulRes",
|
||||
"screenPos", "fpsCap", "vsync", "logicClock" } },
|
||||
{ id = "group.speed", label = "SPEED",
|
||||
members = { "textSpeed", "speedOverworld", "speedBattle", "speedMenu" } },
|
||||
members = { "textSpeed", "speedOverworld", "speedMenu" } },
|
||||
{ id = "group.graphics", label = "GRAPHICS",
|
||||
members = { "colors", "uiLetterbox", "shaderfx", "shaderfx2" } },
|
||||
-- A mod's Pipelines row splices in after TILT and is in no group, so it
|
||||
|
||||
+54
-15
@@ -28,12 +28,31 @@ local function anythingElse(game)
|
||||
Strings("Is there anything\nelse I can do?"))
|
||||
end
|
||||
|
||||
-- engine/events/pokemart.asm:131-133
|
||||
local function say(game, menu, text, after)
|
||||
local box
|
||||
box = TextBox.new(game, text, nil, { stay = { onShown = function()
|
||||
if game.stack:top() == box then game.stack:pop() end
|
||||
menu.footer = text
|
||||
if after then after() end
|
||||
end } })
|
||||
game.stack:push(box)
|
||||
end
|
||||
|
||||
-- engine/events/pokemart.asm:199-206
|
||||
local function backToMenu(game, menu)
|
||||
say(game, menu, anythingElse(game), function()
|
||||
-- engine/events/pokemart.asm:10
|
||||
menu.index, menu.hollowIndex = 1, nil
|
||||
end)
|
||||
end
|
||||
|
||||
-- .returnToMainPokemartMenu -- pokered engine/events/pokemart.asm:199-206
|
||||
local function refuse(game, menu, list, text, under)
|
||||
game.stack:push(TextBox.new(game, text, function()
|
||||
if under and game.stack:top() == under then game.stack:pop() end
|
||||
if list then list:close() end
|
||||
menu.footer = anythingElse(game)
|
||||
backToMenu(game, menu)
|
||||
end))
|
||||
end
|
||||
|
||||
@@ -62,12 +81,12 @@ local function buy(game, stock, menu)
|
||||
itemBox = true,
|
||||
money = function() return game.save.money end,
|
||||
footer = greet,
|
||||
onCancel = function() menu.footer = anythingElse(game) end,
|
||||
onCancel = function() backToMenu(game, menu) end,
|
||||
onChoose = function(item)
|
||||
-- home/list_menu.asm:105-110, 523-528
|
||||
if item.cancel then
|
||||
list:close()
|
||||
menu.footer = anythingElse(game)
|
||||
backToMenu(game, menu)
|
||||
return
|
||||
end
|
||||
-- home/list_menu.asm:89-91
|
||||
@@ -126,7 +145,8 @@ local function buy(game, stock, menu)
|
||||
game.stack:push(qtyBox)
|
||||
end,
|
||||
})
|
||||
game.stack:push(list)
|
||||
-- engine/events/pokemart.asm:131-150
|
||||
say(game, menu, greet, function() game.stack:push(list) end)
|
||||
end
|
||||
|
||||
-- home/list_menu.asm:472-477
|
||||
@@ -168,7 +188,7 @@ local function sell(game, menu)
|
||||
itemBox = true,
|
||||
money = function() return game.save.money end,
|
||||
footer = greet,
|
||||
onCancel = function() menu.footer = anythingElse(game) end,
|
||||
onCancel = function() backToMenu(game, menu) end,
|
||||
onSelectKey = function(item, l)
|
||||
-- swap_items.asm:19-22
|
||||
if not item or item.cancel then return end
|
||||
@@ -186,7 +206,7 @@ local function sell(game, menu)
|
||||
-- home/list_menu.asm:105-110, 523-528
|
||||
if item.cancel then
|
||||
list:close()
|
||||
menu.footer = anythingElse(game)
|
||||
backToMenu(game, menu)
|
||||
return
|
||||
end
|
||||
-- home/list_menu.asm:89-91
|
||||
@@ -245,7 +265,8 @@ local function sell(game, menu)
|
||||
game.stack:push(qtyBox)
|
||||
end,
|
||||
})
|
||||
game.stack:push(list)
|
||||
-- engine/events/pokemart.asm:50-52
|
||||
say(game, menu, greet, function() game.stack:push(list) end)
|
||||
end
|
||||
|
||||
-- MONEY_BOX 11,0 (data/text_boxes.asm:35) over the greeting PrintText left
|
||||
@@ -281,21 +302,39 @@ function ShopMenu.new(game, stock, onQuit)
|
||||
-- keepOpen: the mart menu stays underneath its list so closing the
|
||||
-- list lands back here; only QUIT (or B) leaves and fires onQuit
|
||||
local menu
|
||||
-- engine/menus/text_box.asm:176
|
||||
local function chosen(fn)
|
||||
return function()
|
||||
menu.hollowIndex = menu.index
|
||||
fn()
|
||||
end
|
||||
end
|
||||
-- engine/events/pokemart.asm:220 .done, reached by QUIT and by B alike
|
||||
local function farewell()
|
||||
game.stack:push(TextBox.new(game,
|
||||
txt(game, "_PokemartThankYouText", "Thank you!"), onQuit))
|
||||
txt(game, "_PokemartThankYouText", "Thank you!"), function()
|
||||
if game.stack:top() == menu then game.stack:pop() end
|
||||
if onQuit then onQuit() end
|
||||
end))
|
||||
end
|
||||
menu = Menu.new(game, {
|
||||
{ label = Strings("BUY"), keepOpen = true, onSelect = function() buy(game, stock, menu) end },
|
||||
{ label = Strings("SELL"), keepOpen = true, onSelect = function() sell(game, menu) end },
|
||||
{ label = Strings("QUIT"), onSelect = farewell },
|
||||
{ label = Strings("BUY"), keepOpen = true,
|
||||
onSelect = chosen(function() buy(game, stock, menu) end) },
|
||||
{ label = Strings("SELL"), keepOpen = true,
|
||||
onSelect = chosen(function() sell(game, menu) end) },
|
||||
{ label = Strings("QUIT"), keepOpen = true, onSelect = chosen(farewell) },
|
||||
-- data/text_boxes.asm:34
|
||||
}, { tx = 0, ty = 0, tw = 11, th = 7 })
|
||||
menu.onCancel = farewell
|
||||
menu.footer = txt(game, "_PokemartGreetingText",
|
||||
Strings("Hi there!\nMay I help you?"))
|
||||
}, { tx = 0, ty = 0, tw = 11, th = 7, keepOnCancel = true })
|
||||
menu.onCancel = chosen(farewell)
|
||||
-- home/text_script.asm:141-150
|
||||
function menu:enter()
|
||||
self.hidden = true
|
||||
say(game, self, txt(game, "_PokemartGreetingText",
|
||||
Strings("Hi there!\nMay I help you?")),
|
||||
function() self.hidden = false end)
|
||||
end
|
||||
menu.draw = function(self)
|
||||
if self.hidden then return end
|
||||
drawClerk(self)
|
||||
Menu.draw(self)
|
||||
end
|
||||
|
||||
@@ -61,7 +61,7 @@ end
|
||||
-- src/union_room.c:4558 InUnionRoom
|
||||
local function link_menus_active()
|
||||
local Map = package.loaded["src.core.game3.map"]
|
||||
if Map and Map.current == "FR_UNION_ROOM" then return true end
|
||||
if Map and require("src.core.game3.link.union_room").isUnionMap(Map.current) then return true end
|
||||
local Link = package.loaded["src.core.game3.link"]
|
||||
return type(Link) == "table" and Link.link ~= nil and Link.inLinkRoom() == true
|
||||
end
|
||||
|
||||
+1
-10
@@ -159,16 +159,7 @@ function Boot.continueInfoFromSave(save)
|
||||
local store = { flags = type(save.flags) == "table" and save.flags or {} }
|
||||
local pt = type(save.playTime) == "table" and save.playTime
|
||||
or type(save.playtime) == "table" and save.playtime or {}
|
||||
local dex = type(save.dex) == "table" and save.dex or {}
|
||||
local caught = dex.caught or dex.owned or {}
|
||||
local counted, n = {}, 0
|
||||
for sp, on in pairs(caught) do
|
||||
local id = tonumber(sp)
|
||||
if id and on and on ~= 0 and not counted[id] and (dex.national or id <= 151) then
|
||||
counted[id] = true
|
||||
n = n + 1
|
||||
end
|
||||
end
|
||||
local n = require("src.core.game3.dex").summaryCount(save)
|
||||
local name = tostring(save.name or save.playerName or "")
|
||||
return {
|
||||
name = FrlgFont.truncate(name, 7),
|
||||
|
||||
@@ -70,7 +70,6 @@ function Rows.build(ctx)
|
||||
step = function(c, dir) return cartCycle(c, "textSpeed", 3, dir) end,
|
||||
})
|
||||
add(speedRow("speedOverworld", "OVERWORLD SPEED", "speedOverworld"))
|
||||
add(speedRow("speedBattle", "BATTLE SPEED", "speedBattle"))
|
||||
add(speedRow("speedMenu", "MENU SPEED", "speedMenu"))
|
||||
|
||||
add({
|
||||
@@ -397,7 +396,7 @@ end
|
||||
|
||||
Rows.GROUPS = {
|
||||
{ id = "group.speed", label = "SPEED",
|
||||
members = { "textSpeed", "speedOverworld", "speedBattle", "speedMenu" } },
|
||||
members = { "textSpeed", "speedOverworld", "speedMenu" } },
|
||||
{ id = "group.video", label = "VIDEO",
|
||||
members = { "uiLayout", "videoMode", "orientation", "faithfulRes",
|
||||
"screenPos", "fpsCap", "vsync", "logicClock" } },
|
||||
|
||||
@@ -1657,7 +1657,19 @@ function PartyMenu.handleInput(input)
|
||||
local fx, fy = P.cellX + d[1], P.cellY + d[2]
|
||||
local facingObj = Objects.at(fx, fy)
|
||||
local isWater = Collision.isWater and Collision.isWater(fx, fy)
|
||||
local isGrass = Collision.isGrass and Collision.isGrass(fx, fy)
|
||||
-- pokefirered/src/fldeff_cut.c:140
|
||||
local function hasCuttableGrass3x3(px, py)
|
||||
if not Collision.isGrass then return false end
|
||||
-- pokefirered/src/fldeff_rocksmash.c:31
|
||||
local elev = P.elevation
|
||||
for y = py - 1, py + 1 do
|
||||
for x = px - 1, px + 1 do
|
||||
local e = Collision.elevationAt and Collision.elevationAt(x, y)
|
||||
if (elev == nil or e == elev) and Collision.isGrass(x, y) then return true end
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
local mapDef = Map.currentDef()
|
||||
-- pokefirered/src/party_menu.c:4118
|
||||
local facingBeh = Collision.behavior and Collision.behavior(fx, fy)
|
||||
@@ -1671,7 +1683,7 @@ function PartyMenu.handleInput(input)
|
||||
isFacingWaterfall = FieldMoves.isWaterfallBehavior(facingBeh),
|
||||
facing = P.facing,
|
||||
isSurfing = P.surfing == true,
|
||||
hasCuttableGrass = isGrass or (Collision.isGrass and Collision.isGrass(P.cellX, P.cellY)),
|
||||
hasCuttableGrass = hasCuttableGrass3x3(P.cellX, P.cellY),
|
||||
mapType = mapDef and mapDef.mapType,
|
||||
isCave = mapHeaderFlag(mapDef, "cave"),
|
||||
canEscapeRope = mapHeaderFlag(mapDef, "allowEscaping"),
|
||||
|
||||
@@ -40,7 +40,7 @@ end
|
||||
local function in_union_room(session)
|
||||
local Map = package.loaded["src.core.game3.map"]
|
||||
local cur = (Map and type(Map.current) == "string" and Map.current) or (session and session.map)
|
||||
return cur == "FR_UNION_ROOM"
|
||||
return require("src.core.game3.link.union_room").isUnionMap(cur)
|
||||
end
|
||||
|
||||
local function safari_active(session)
|
||||
|
||||
@@ -21,6 +21,9 @@ UnionRoomScreen.TYPES_TEMPLATE = Window.template(20, 2, 9, 11)
|
||||
UnionRoomScreen.BOARD_HEADER_TEMPLATE = Window.template(1, 1, 28, 2)
|
||||
-- pokefirered/src/data/union_room.h:302
|
||||
UnionRoomScreen.BOARD_TEMPLATE = Window.template(1, 5, 28, 10)
|
||||
-- pokefirered/src/union_room.c:4400
|
||||
UnionRoomScreen.BOARD_ROWS = 8
|
||||
UnionRoomScreen.BOARD_EXIT = "exit"
|
||||
|
||||
-- pokefirered/src/data/union_room.h:197
|
||||
UnionRoomScreen.ROW_HEIGHT = 14
|
||||
@@ -202,14 +205,15 @@ function UnionRoomScreen.showBoard(opts)
|
||||
if item.entry then UnionRoomScreen.printBoardRow(item.entry, px, py, UnionRoomScreen.BOARD_SELF) end
|
||||
end }
|
||||
local offers = opts.offers or {}
|
||||
for i = 0, 7 do
|
||||
for i = 0, math.max(UnionRoomScreen.BOARD_ROWS, #offers) - 1 do
|
||||
local entry = offers[i + 1]
|
||||
rows[#rows + 1] = { label = "", id = i, disabled = entry == nil, entry = entry,
|
||||
print = function(item, px, py)
|
||||
if item.entry then UnionRoomScreen.printBoardRow(item.entry, px, py, UnionRoomScreen.BOARD_OTHER) end
|
||||
end }
|
||||
end
|
||||
rows[#rows + 1] = { label = RomText.plain("gText_UR_Exit2"), id = 8, colors = UnionRoomScreen.BOARD_OTHER }
|
||||
rows[#rows + 1] = { label = RomText.plain("gText_UR_Exit2"), id = UnionRoomScreen.BOARD_EXIT,
|
||||
colors = UnionRoomScreen.BOARD_OTHER }
|
||||
local shown = UnionRoomScreen.showList("board", {
|
||||
template = UnionRoomScreen.BOARD_TEMPLATE,
|
||||
items = rows,
|
||||
|
||||
@@ -150,6 +150,7 @@ end
|
||||
function WirelessIcon.onLinkMap(mapId)
|
||||
if type(mapId) ~= "string" then return false end
|
||||
if WirelessIcon.LINK_MAPS[mapId] then return true end
|
||||
if require("src.core.game3.link.union_room").isUnionMap(mapId) then return true end
|
||||
return mapId:match("_POKEMON_CENTER_2F$") ~= nil
|
||||
end
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ local Unown = require("src.core.gen2.Unown")
|
||||
local BattleState = {}
|
||||
BattleState.__index = BattleState
|
||||
BattleState.isOpaque = true
|
||||
BattleState.isBattle = true
|
||||
|
||||
function BattleState:moveGridNavigation()
|
||||
if not Runtime.wantsHook("battle.move_grid_navigation") then return false end
|
||||
|
||||
@@ -35,6 +35,7 @@ local Tilt = require("src.render.Tilt")
|
||||
local BattleTransition = {}
|
||||
BattleTransition.__index = BattleTransition
|
||||
BattleTransition.isOpaque = false
|
||||
BattleTransition.isFixedSpeed = true
|
||||
|
||||
local COLS, ROWS = 20, 18 -- SCREEN_WIDTH x SCREEN_HEIGHT, in tiles
|
||||
|
||||
|
||||
@@ -5868,6 +5868,30 @@ function OverworldState:drawWorld()
|
||||
PaletteFX.setShadeMap((self.dark and not battleOverWorld)
|
||||
and PaletteFX.DARK_BGP or self:poisonShadeMap())
|
||||
end
|
||||
local tiles = self.map and self.map.renderer
|
||||
self.boltBaked = nil
|
||||
if tiles and tiles.setBgp then
|
||||
local e = self.emote
|
||||
-- engine/pikachu/pikachu_pic_animation.asm:790
|
||||
local prebake = e and e.boltAt and not e.boltDone and (e.boltT or 0) > e.boltAt
|
||||
local bolt = e and not battleOverWorld and e.bgp or nil
|
||||
local neighbors = self.neighbors
|
||||
for i = 1, neighbors and #neighbors or 0 do
|
||||
local r = neighbors[i].map.renderer
|
||||
if r and r.setBgp then
|
||||
if prebake then
|
||||
r:bgpImage(0xC0)
|
||||
r:bgpImage(0xE4)
|
||||
end
|
||||
r:setBgp(bolt)
|
||||
end
|
||||
end
|
||||
if prebake then
|
||||
tiles:bgpImage(0xC0)
|
||||
tiles:bgpImage(0xE4)
|
||||
end
|
||||
self.boltBaked = tiles:setBgp(bolt) and bolt or nil
|
||||
end
|
||||
-- advance the water/flower tile animation (runs under dialogs too).
|
||||
-- TileRenderer.tick uses wall-clock 60Hz steps so display refresh rate
|
||||
-- does not speed or slow the cycle (issue #4).
|
||||
@@ -6439,28 +6463,27 @@ function OverworldState:drawUI()
|
||||
-- TalkToPikachu's picture box (engine/pikachu/pikachu_pic_animation.asm
|
||||
-- PlacePikapicTextBoxBorder: TextBoxBorder at (6,5) with b,c = 5,5, so a
|
||||
-- 7x7 box holding the 5x5 pic at (7,6) -- PikaAnimTilemap_1). The
|
||||
-- script's base frame is ripped as pikachu/pikapic_N.png (#561) but the
|
||||
-- pikaframe overlays on top of it are not, so PikachuFollower
|
||||
-- .picLift lifts the base on the runs that draw the alternate pose, and the
|
||||
-- script's own duration times the beat (#407, #424). Palette zone
|
||||
-- PAL_PIKACHU_PORTRAIT covers (7,6)-(11,10) via sgbPalettes above.
|
||||
if self.emote and self.emote.pikaPic then
|
||||
require("src.render.Font").drawBox(6, 5, 7, 7)
|
||||
-- one image per path, cached: this draws every frame of the hold, and
|
||||
-- a mod skin can move the path between talks
|
||||
if self.pikaPicPath ~= self.emote.pikaPic then
|
||||
local ok, loaded = pcall(love.graphics.newImage, self.emote.pikaPic)
|
||||
self.pikaPicImg = ok and loaded or nil
|
||||
self.pikaPicPath = self.emote.pikaPic
|
||||
local path, lift = require("src.world.PikachuFollower").picFrame(self.emote)
|
||||
self.pikaPicImgs = self.pikaPicImgs or {}
|
||||
local img = path and self.pikaPicImgs[path]
|
||||
if path and img == nil then
|
||||
local ok, loaded = pcall(love.graphics.newImage, path)
|
||||
img = ok and loaded or false
|
||||
self.pikaPicImgs[path] = img
|
||||
end
|
||||
local img = self.pikaPicImg
|
||||
self.pikaPicDrawn = img and path or nil
|
||||
if img then
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
local w, h = img:getDimensions()
|
||||
local lift = require("src.world.PikachuFollower").picLift(self.emote)
|
||||
love.graphics.draw(img, math.floor(56 + (40 - w) / 2),
|
||||
math.floor(48 + (40 - h) / 2) - lift)
|
||||
math.floor(48 + (40 - h) / 2) - (lift or 0))
|
||||
end
|
||||
else
|
||||
self.pikaPicDrawn = nil
|
||||
end
|
||||
|
||||
-- engine/gfx/screen_effects.asm:1-12
|
||||
@@ -6483,7 +6506,7 @@ function OverworldState:drawUI()
|
||||
|
||||
-- engine/pikachu/pikachu_pic_animation.asm:847
|
||||
local bolt = self.emote and self.emote.bgp
|
||||
if bolt and bolt ~= 0xE4 and Game and Game.renderer
|
||||
if bolt and bolt ~= 0xE4 and Game and Game.renderer and self.boltBaked ~= bolt
|
||||
and (self:bakedWorldColors() or not PaletteFX.shader()) then
|
||||
Game.renderer.screenVeil = BOLT_WHITE_VEIL
|
||||
end
|
||||
|
||||
@@ -735,11 +735,7 @@ end
|
||||
-- and voiced PCM clip, and raise the framed Pikachu picture the original
|
||||
-- puts over the map (pikaemotion_pikapic -> pikachu_pic_animation.asm
|
||||
-- PlacePikapicTextBoxBorder), drawn by OverworldController:drawUI. Each
|
||||
-- script's BASE 5x5 frame is ripped as pikachu/pikapic_N.png (#561); the
|
||||
-- pikaframe overlays it alternates with are a second full-body pose out of
|
||||
-- the same blob and are still unported, so picLift below stands in for
|
||||
-- their motion and the battle front pic covers caches built before the
|
||||
-- rip (#407).
|
||||
-- script's BASE 5x5 frame is ripped as pikachu/pikapic_N.png (#561).
|
||||
-- ---------------------------------------------------------------------
|
||||
|
||||
-- PikachuEmotionTable, reduced to each entry's bubble + pikaemotion_pcm
|
||||
@@ -804,6 +800,7 @@ local MODIFIER_EMOTIONS = { 18, 21, 23, 24, 25 }
|
||||
-- third of even the shortest script (#424).
|
||||
local PIKAPIC_TICK = 3
|
||||
local PIKAPIC_LIFT = 4 -- px the stand-in pic rises on an overlay run
|
||||
local PIKAPIC_DIR = "assets/generated/pikachu/"
|
||||
|
||||
-- pikaemotion_pikapic's script id per emotion: emotion N takes
|
||||
-- PikaPicAnimScript N, except the four listed here (data/pikachu/
|
||||
@@ -821,11 +818,9 @@ PikachuFollower.THUNDERBOLT_ROW = THUNDERBOLT_ROW
|
||||
-- Per script: pikapic_setduration's tick count, and for the scripts whose
|
||||
-- overlay is a whole second pose, that frameset's run lengths in ticks
|
||||
-- (data/pikachu/pikachu_pic_objects.asm PikaPicAnimBGFrames_*, which script
|
||||
-- N reaches as frameset N+5, or N+6 from script 10 up). The list alternates
|
||||
-- pikaframedelay (the base pic alone) and pikaframe (the overlay) starting
|
||||
-- with a delay, so a frameset that opens on a pikaframe opens with a zero
|
||||
-- here; the frameset restarts until pikapic_looptofinish runs the duration
|
||||
-- out. Scripts 1, 2, 3, 5, 6, 8 and 9 are left without a list on purpose:
|
||||
-- N reaches as frameset N+5, or N+6 from script 10 up). The frameset
|
||||
-- restarts until pikapic_looptofinish runs the duration out.
|
||||
-- Scripts 1, 2, 3, 5, 6, 8 and 9 are left without a list on purpose:
|
||||
-- their overlays (PikaAnimTilemap_14 to _22) only paint a few tiles over a
|
||||
-- pic that otherwise stands still, so with no tiles to paint the port has
|
||||
-- nothing to show for them and must not bob the whole picture instead.
|
||||
@@ -836,29 +831,50 @@ local PIKAPIC = {
|
||||
[4] = { dur = 70, seq = { 8, 8, 20, 8 } },
|
||||
[5] = { dur = 32 },
|
||||
[6] = { dur = 50 },
|
||||
[7] = { dur = 58, seq = { 0, 8, 2, 8, 2, 8 } },
|
||||
[7] = { dur = 58, seq = { 8, 2, 8, 2, 8 },
|
||||
poses = { "e4841", false, "e4841", false, "e4841" } },
|
||||
[8] = { dur = 44 },
|
||||
[9] = { dur = 56 },
|
||||
[10] = { dur = 56, seq = { 8, 11, 5 } },
|
||||
[11] = { dur = 100, seq = { 20, 8, 20, 8 } },
|
||||
[12] = { dur = 50, seq = { 13, 12, 100, 8 } },
|
||||
[13] = { dur = 50, seq = { 5, 5, 5, 5, 100 } },
|
||||
[14] = { dur = 40, seq = { 2, 2, 2, 2 } },
|
||||
[15] = { dur = 50, seq = { 5, 5, 5, 5 } },
|
||||
[16] = { dur = 32, seq = { 0, 8, 100 } },
|
||||
[17] = { dur = 100, seq = { 10, 3, 3, 3, 100 } },
|
||||
[18] = { dur = 32, seq = { 3, 100, 8, 8 } },
|
||||
[19] = { dur = 44, seq = { 0, 6, 6, 6, 6 } },
|
||||
[20] = { dur = 50, seq = { 8, 12, 8, 12 } },
|
||||
[21] = { dur = 40, seq = { 8, 104 } },
|
||||
[22] = { dur = 40, seq = { 8, 100 } },
|
||||
[23] = { dur = 70, seq = { 16, 16, 16, 16 } },
|
||||
[24] = { dur = 60, seq = { 6, 6, 6, 6, 100 } },
|
||||
[25] = { dur = 50, seq = { 6, 106 }, bolt = 13 },
|
||||
[26] = { dur = 100, seq = { 20, 8, 20, 116 } },
|
||||
[27] = { dur = 30, seq = { 4, 100 } },
|
||||
[28] = { dur = 64, seq = { 12, 12, 12, 100 } },
|
||||
[10] = { dur = 56, seq = { 8, 3, 5, 3, 5 },
|
||||
poses = { false, "e4ce0", "e4e70", "e4ce0", false } },
|
||||
[11] = { dur = 100, seq = { 20, 8, 20, 8 },
|
||||
poses = { false, "e50af", false, "e50af" } },
|
||||
[12] = { dur = 50, seq = { 13, 12, 100, 8 },
|
||||
poses = { false, "e52fe", false, "e52fe" } },
|
||||
[13] = { dur = 50, seq = { 5, 5, 5, 5, 100 },
|
||||
poses = { false, "e5541", false, "e5541", false } },
|
||||
[14] = { dur = 40, seq = { 2, 2, 2, 2 },
|
||||
poses = { false, "e5794", false, "e5794" } },
|
||||
[15] = { dur = 50, seq = { 5, 5, 5, 5 },
|
||||
poses = { false, "e59ed", false, "e59ed" } },
|
||||
[16] = { dur = 32, seq = { 8, 100 },
|
||||
poses = { "e5c4d", false } },
|
||||
[17] = { dur = 100, seq = { 10, 3, 3, 3, 100 },
|
||||
poses = { false, "e5e90", false, "e5e90", false } },
|
||||
[18] = { dur = 32, seq = { 3, 100, 8, 8 },
|
||||
poses = { false, "e61b0", false, "e61b0" } },
|
||||
[19] = { dur = 44, seq = { 6, 6, 6, 6 },
|
||||
poses = { "e63f7", false, "e63f7", false } },
|
||||
[20] = { dur = 50, seq = { 8, 12, 8, 12 },
|
||||
poses = { false, "e6646", false, "e6646" } },
|
||||
[21] = { dur = 40, seq = { 8, 2, 1, 1, 100 },
|
||||
poses = { false, "e682f", "e69bf", "e6b4f", "e6cdf" } },
|
||||
[22] = { dur = 40, seq = { 8, 100 },
|
||||
poses = { false, "e6fff" } },
|
||||
[23] = { dur = 70, seq = { 16, 16, 16, 16 },
|
||||
poses = { false, "e731f", false, "e731f" } },
|
||||
[24] = { dur = 60, seq = { 6, 6, 6, 6, 100 },
|
||||
poses = { false, "e763f", false, "e763f", false } },
|
||||
[25] = { dur = 50, seq = { 6, 6, 100 },
|
||||
poses = { false, "e7863", "e79f3" }, bolt = 13 },
|
||||
[26] = { dur = 100, seq = { 20, 8, 20, 8, 8, 100 },
|
||||
poses = { false, "e50af", false, "e50af", "e7b83", "e7d13" } },
|
||||
[27] = { dur = 30, seq = { 4, 100 },
|
||||
poses = { false, "f0b64" } },
|
||||
[28] = { dur = 64, seq = { 12, 12, 12, 100 },
|
||||
poses = { false, "f0d82", false, "f0d82" } },
|
||||
}
|
||||
PikachuFollower.PIKAPIC = PIKAPIC
|
||||
|
||||
local function moodEmotion(save)
|
||||
local mood = save.pikachuMood or 128
|
||||
@@ -953,19 +969,22 @@ function playEmotion(game, ow, npc, emotion, opts)
|
||||
|
||||
-- data/pikachu/pikachu_emotions.asm
|
||||
local function pikapic()
|
||||
local Sprites = require("src.pokemon.Sprites")
|
||||
local script = PIKAPIC_SCRIPT[emotion] or emotion
|
||||
local pic = "assets/generated/pikachu/pikapic_" .. script .. ".png"
|
||||
if not require("src.render.Assets").exists(pic) then
|
||||
pic = Sprites.path(game.data, "PIKACHU", "front",
|
||||
{ kind = "overworld" })
|
||||
end
|
||||
local pic = PIKAPIC_DIR .. "pikapic_" .. script .. ".png"
|
||||
local anim = PIKAPIC[script] or PIKAPIC[1]
|
||||
local hold = anim.dur * PIKAPIC_TICK
|
||||
local poses
|
||||
if anim.poses then
|
||||
poses = {}
|
||||
for i = 1, #anim.seq do
|
||||
local id = anim.poses[i]
|
||||
poses[i] = id and (PIKAPIC_DIR .. "gfx_" .. id .. ".png") or false
|
||||
end
|
||||
end
|
||||
ow.emote = {
|
||||
npc = npc, frames = hold, bubble = false, pikaPic = pic,
|
||||
pikaSeq = anim.seq, pikaTotal = hold, skippable = opts.skippable,
|
||||
onDone = done,
|
||||
pikaSeq = anim.seq, pikaPoses = poses, pikaTotal = hold,
|
||||
skippable = opts.skippable, onDone = done,
|
||||
}
|
||||
if anim.bolt then
|
||||
-- engine/pikachu/pikachu_pic_animation.asm:520
|
||||
@@ -1016,24 +1035,37 @@ function playEmotion(game, ow, npc, emotion, opts)
|
||||
return cry()
|
||||
end
|
||||
|
||||
-- Where the framed pic sits this frame. The overlay a pikaframe run draws
|
||||
-- is a second full-body pose (PikaAnimTilemap_23 and up replace all 5x5
|
||||
-- tiles) out of gfx/pikachu/unknown_*, which the cache does not carry, so
|
||||
-- the port lifts the one pic it has for the length of those runs -- the jump
|
||||
-- the happy emotions make inside the box (#424, still on #407's stand-in).
|
||||
function PikachuFollower.picLift(emote)
|
||||
local function frameRun(emote)
|
||||
local seq = emote and emote.pikaSeq
|
||||
if not seq then return 0 end
|
||||
if not seq then return nil end
|
||||
local loop = 0
|
||||
for _, run in ipairs(seq) do loop = loop + run end
|
||||
if loop <= 0 then return 0 end
|
||||
if loop <= 0 then return nil end
|
||||
local elapsed = math.max(0, (emote.pikaTotal or 0) - (emote.frames or 0))
|
||||
local tick = math.floor(elapsed / PIKAPIC_TICK) % loop
|
||||
for i, run in ipairs(seq) do
|
||||
if tick < run then return i % 2 == 0 and PIKAPIC_LIFT or 0 end
|
||||
if tick < run then return i end
|
||||
tick = tick - run
|
||||
end
|
||||
return 0
|
||||
return nil
|
||||
end
|
||||
|
||||
function PikachuFollower.picLift(emote)
|
||||
if emote and emote.pikaPoses then return 0 end
|
||||
local i = frameRun(emote)
|
||||
return (i and i % 2 == 0) and PIKAPIC_LIFT or 0
|
||||
end
|
||||
|
||||
-- engine/pikachu/pikachu_pic_animation.asm:359
|
||||
function PikachuFollower.picFrame(emote)
|
||||
if not emote then return nil, 0 end
|
||||
if emote.pikaBlankAt and (emote.frames or 0) <= emote.pikaBlankAt then
|
||||
return nil, 0
|
||||
end
|
||||
local poses = emote.pikaPoses
|
||||
if not poses then return emote.pikaPic, PikachuFollower.picLift(emote) end
|
||||
local i = frameRun(emote)
|
||||
return (i and poses[i]) or emote.pikaPic, 0
|
||||
end
|
||||
|
||||
-- engine/pikachu/pikachu_pic_animation.asm:790
|
||||
@@ -1066,7 +1098,9 @@ function PikachuFollower.tickBolt(game, ow, emote)
|
||||
-- engine/pikachu/pikachu_pic_animation.asm:802
|
||||
if Sound.moveSfxBusy() and k < strobe + THUNDERBOLT_SOUND_CAP then return end
|
||||
emote.boltDone = true
|
||||
emote.frames = PIKAPIC_TICK + 1
|
||||
-- engine/pikachu/pikachu_pic_animation.asm:136-147
|
||||
emote.pikaBlankAt = PIKAPIC_TICK
|
||||
emote.frames = 2 * PIKAPIC_TICK + 1
|
||||
end
|
||||
|
||||
-- Bill's House has three map-scripted Yellow companion beats
|
||||
|
||||
@@ -25,6 +25,19 @@ return function(game)
|
||||
ow:pushBattle(battle)
|
||||
|
||||
U.log("logic speed", game:logicSpeed(), "sfx rate", Sound.rate())
|
||||
local function inBattle()
|
||||
for _, st in ipairs(game.stack.states) do
|
||||
if st == battle then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
local function assertLocked(where)
|
||||
if inBattle() and game:logicSpeed() ~= 1 then
|
||||
error(("speed lock: %s ran at %sX with speedOverride 4")
|
||||
:format(where, tostring(game:logicSpeed())))
|
||||
end
|
||||
end
|
||||
assertLocked("battle start")
|
||||
if Sound.rate() ~= 1 then
|
||||
error(("bug2087: Game:update pitched SFX off battle speed (rate %s at 4X)")
|
||||
:format(tostring(Sound.rate())))
|
||||
@@ -58,8 +71,11 @@ return function(game)
|
||||
end
|
||||
end
|
||||
|
||||
local lockedFrames = 0
|
||||
local shot = false
|
||||
for _ = 1, 2400 do
|
||||
assertLocked("entrance")
|
||||
if inBattle() then lockedFrames = lockedFrames + 1 end
|
||||
poll("entrance cry")
|
||||
if battle.phase == "menu" then break end
|
||||
if battle.waitingSound and not shot then
|
||||
@@ -87,6 +103,7 @@ return function(game)
|
||||
for _ = 1, 2400 do
|
||||
poll("post-move sfx")
|
||||
if game.stack:top() ~= battle then break end
|
||||
assertLocked("post-move")
|
||||
if #gates > before and cur and not shotLevel then
|
||||
shotLevel = U.shot(game, DIR .. "/bug2087_levelup.png")
|
||||
end
|
||||
@@ -105,11 +122,12 @@ return function(game)
|
||||
fail = ("bug2087: %s was pitched with battle speed (%s)"):format(
|
||||
g.label, tostring(g.pitch))
|
||||
elseif g.dur and g.held < g.dur * 0.9 then
|
||||
fail = ("bug2087: %s cut short at 4X (%.3fs of %.3fs)"):format(
|
||||
fail = ("bug2087: %s cut short (%.3fs of %.3fs)"):format(
|
||||
g.label, g.held, g.dur)
|
||||
end
|
||||
end
|
||||
if fail then error(fail) end
|
||||
U.log("PASS every battle sfx gate held for its real length at 4X")
|
||||
U.log("input is yours now")
|
||||
if lockedFrames == 0 then error("speed lock: the battle never reached the stack") end
|
||||
U.log("PASS every battle sfx gate held for its real length, battle locked to 1X under a 4X override")
|
||||
love.event.quit(0)
|
||||
end
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
-- Wait gates hold for the sound's real length at high GAME SPEED
|
||||
-- (#1952/#2087) without pitching one-shot SFX (#1990/#1991/#1997).
|
||||
-- home/text.asm:506 (#1952/#2087, #1990/#1991/#1997)
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local DIR = os.getenv("SHOT_DIR") or os.getenv("POKEPORT_SHOT_DIR") or "/tmp/shots"
|
||||
@@ -26,12 +25,26 @@ return function(game)
|
||||
ow:pushBattle(battle)
|
||||
|
||||
U.log("logic speed", game:logicSpeed(), "sfx rate", Sound.rate())
|
||||
local function inBattle()
|
||||
for _, st in ipairs(game.stack.states) do
|
||||
if st == battle then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
local function assertLocked(where)
|
||||
if inBattle() and game:logicSpeed() ~= 1 then
|
||||
error(("speed lock: %s ran at %sX with speedOverride 4")
|
||||
:format(where, tostring(game:logicSpeed())))
|
||||
end
|
||||
end
|
||||
assertLocked("battle start")
|
||||
if Sound.rate() ~= 1 then
|
||||
error(("bug1952: Game:update pitched SFX off GAME SPEED (rate %s at 4X)")
|
||||
:format(tostring(Sound.rate())))
|
||||
end
|
||||
|
||||
for _ = 1, 240 do
|
||||
assertLocked("battle intro")
|
||||
if battle.phase == "menu" then break end
|
||||
U.tap(game, "a")
|
||||
U.wait(3)
|
||||
@@ -50,6 +63,7 @@ return function(game)
|
||||
local shot = false
|
||||
for _ = 1, 1200 do
|
||||
U.wait(1)
|
||||
assertLocked("level-up")
|
||||
if battle.waitingSound and not t0 then
|
||||
local src = battle.waitingSound
|
||||
t0 = love.timer.getTime()
|
||||
@@ -80,13 +94,25 @@ return function(game)
|
||||
|
||||
if not dur then error("bug1952: no duration for the level-up fanfare") end
|
||||
if held < dur * 0.9 then
|
||||
error(("bug2087: the 4X battle cut the fanfare short (%.3fs of %.3fs)")
|
||||
error(("bug2087: the battle cut the fanfare short (%.3fs of %.3fs)")
|
||||
:format(held, dur))
|
||||
end
|
||||
if held > dur + 1 then
|
||||
error(("bug1952: the fanfare dragged past its length (%.3fs of %.3fs)")
|
||||
:format(held, dur))
|
||||
end
|
||||
U.log("PASS the fanfare kept natural pitch and played to completion at 4X")
|
||||
love.event.quit()
|
||||
for _ = 1, 2400 do
|
||||
if not inBattle() then break end
|
||||
assertLocked("battle end")
|
||||
U.tap(game, "a")
|
||||
U.wait(1)
|
||||
end
|
||||
if inBattle() then error("speed lock: the battle never ended") end
|
||||
U.wait(2)
|
||||
if game:logicSpeed() ~= 4 then
|
||||
error(("speed lock: after the battle logic speed is %s, want 4")
|
||||
:format(tostring(game:logicSpeed())))
|
||||
end
|
||||
U.log("PASS the fanfare kept natural pitch at 1X in a 4X-override battle, 4X back after")
|
||||
love.event.quit(0)
|
||||
end
|
||||
|
||||
@@ -69,13 +69,27 @@ return function(game)
|
||||
check(game.save.inventory["0"] == 1, "seeded unknown item '0' in bag")
|
||||
|
||||
-- open the clerk's BUY/SELL/QUIT menu exactly as the mart interaction does
|
||||
local Menu = require("src.ui.Menu")
|
||||
local ListMenu = require("src.ui.ListMenu")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local function waitTop(pred)
|
||||
for _ = 1, 600 do
|
||||
local top = game.stack:top()
|
||||
if top and pred(top) then return top end
|
||||
U.wait(1)
|
||||
end
|
||||
end
|
||||
Screens.push(game, "ShopMenu", {})
|
||||
U.wait(4)
|
||||
local shop = waitTop(function(t)
|
||||
return getmetatable(t) == Menu and not t.hidden
|
||||
end)
|
||||
check(shop ~= nil, "clerk menu up after the greeting")
|
||||
U.wait(2)
|
||||
U.tap(game, "down") -- BUY -> SELL
|
||||
U.wait(4)
|
||||
U.tap(game, "a") -- open the SELL list
|
||||
U.wait(6)
|
||||
local sellList = game.stack:top()
|
||||
local sellList = waitTop(function(t) return getmetatable(t) == ListMenu end)
|
||||
U.wait(2)
|
||||
check(sellList and sellList.items ~= nil, "SELL list opened")
|
||||
U.shot(game, DIR .. "/bh_2_sell_list.png")
|
||||
|
||||
@@ -88,7 +102,6 @@ return function(game)
|
||||
check(item ~= nil, "bogus '0' row present in sell list")
|
||||
if item then
|
||||
sellList.index = idx
|
||||
local footerBefore = sellList.footer
|
||||
-- Invoke the real onChoose the ListMenu would call on A. Before the
|
||||
-- fix this throws at math.floor(def.price/2) with def=nil; we catch
|
||||
-- it so the run reports the crash instead of hanging on love's error
|
||||
@@ -101,9 +114,15 @@ return function(game)
|
||||
check(true, "selling '0' did not crash")
|
||||
-- nothing may be sold: the guard returns before any QuantityBox
|
||||
check(game.save.inventory["0"] == 1, "unknown item still in bag (not sold)")
|
||||
check(sellList.footer ~= footerBefore
|
||||
and tostring(sellList.footer):find("price") ~= nil,
|
||||
"sell footer shows the unsellable message")
|
||||
local box = game.stack:top()
|
||||
local said = {}
|
||||
if getmetatable(box) == TextBox then
|
||||
for _, page in ipairs(box.pages) do
|
||||
for _, line in ipairs(page) do said[#said + 1] = line end
|
||||
end
|
||||
end
|
||||
check(table.concat(said, " "):find("price") ~= nil,
|
||||
"clerk says the unsellable line")
|
||||
end
|
||||
U.wait(4)
|
||||
U.shot(game, DIR .. "/bh_3_sell_choose.png")
|
||||
@@ -112,4 +131,5 @@ return function(game)
|
||||
|
||||
U.log("RESULT", ("pass=%d fail=%d"):format(pass, fail))
|
||||
if fail == 0 then U.log("RESULT", "ALL PASS") else U.log("RESULT", "HAS FAILURES") end
|
||||
love.event.quit(fail == 0 and 0 or 1)
|
||||
end
|
||||
|
||||
@@ -2,7 +2,9 @@ local U = require("tests.drivers.util")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/g3link"
|
||||
|
||||
local CENTER_2F = "FR_VIRIDIAN_CITY_POKEMON_CENTER_2F"
|
||||
local UNION_ROOM = "FR_UNION_ROOM"
|
||||
local Plaza = require("src.core.game3.link.union_plaza_map")
|
||||
local UNION_ROOM = Plaza.MAP_ID
|
||||
local OTHERS = Plaza.CAP - 1
|
||||
-- pokefirered/include/constants/flags.h:1375
|
||||
local FLAG_SYS_POKEDEX_GET = 0x829
|
||||
-- pokefirered/include/constants/metatile_behaviors.h:104
|
||||
@@ -135,13 +137,82 @@ return function(game)
|
||||
me.presence.where = "union"
|
||||
|
||||
local blue = peer("b0000002", "BLUE", 0x2222, 0, "leafgreen")
|
||||
local leaf = peer("c0000003", "LEAF", 0x3131, 1, "firered")
|
||||
relay:handle(blue, { type = "plaza_join", kind = "union", profile = live, avatar = blue.avatar })
|
||||
relay:handle(leaf, { type = "plaza_join", kind = "union", profile = live, avatar = leaf.avatar })
|
||||
result(waitFor(function() return Union.playerCount() == 2 end, 10, Union.REFRESH_FRAMES + 30),
|
||||
"both plaza members appear as union room avatars")
|
||||
relay:handle(blue, { type = "plaza_join", kind = "union", cap = 40, profile = live, avatar = blue.avatar })
|
||||
local crowd = { blue }
|
||||
for i = 2, OTHERS do
|
||||
local s = peer(string.format("c%07x", i), "T" .. i, 0x100 + i * 13, i % 2, i % 3 == 0 and "leafgreen" or "firered")
|
||||
relay:handle(s, { type = "plaza_join", kind = "union", cap = 40, profile = live, avatar = s.avatar })
|
||||
crowd[#crowd + 1] = s
|
||||
end
|
||||
result(waitFor(function() return Union.playerCount() == OTHERS end, 10, 120),
|
||||
"all 39 other plaza members appear as avatars")
|
||||
result(waitFor(function()
|
||||
local VirtualObjects = require("src.core.game3.virtual_objects")
|
||||
return VirtualObjects.count() == OTHERS
|
||||
end, 5, 60), "39 virtual objects, no cart map objects")
|
||||
local mySlot = Client.plaza() and Client.plaza().you
|
||||
result(mySlot ~= nil and Union.players[mySlot] == nil, "our own slot is not drawn")
|
||||
local cellsOk = true
|
||||
for slot = 1, Plaza.CAP do
|
||||
if slot ~= mySlot then
|
||||
local v = Union.vobj(slot)
|
||||
local x, y = Plaza.cellFor(slot)
|
||||
if not (v and v.visible and v.x == x and v.y == y) then cellsOk = false end
|
||||
end
|
||||
end
|
||||
result(cellsOk, "every avatar stands on its slot's cell")
|
||||
wait(60)
|
||||
U.shot(game, DIR .. "/g3link_union_plaza_avatars.png")
|
||||
U.still(game, DIR .. "/g3link_union_plaza_entrance.png")
|
||||
place(12, 12, "down")
|
||||
wait(20)
|
||||
U.still(game, DIR .. "/g3link_union_plaza_full_room.png")
|
||||
place(22, 3, "left")
|
||||
wait(20)
|
||||
U.still(game, DIR .. "/g3link_union_plaza_far_corner.png")
|
||||
|
||||
local far = crowd[#crowd]
|
||||
local farSlot = Union.slotForId(far.id)
|
||||
if farSlot ~= Plaza.CAP then
|
||||
for _, s in ipairs(crowd) do
|
||||
if Union.slotForId(s.id) == Plaza.CAP then far, farSlot = s, Plaza.CAP end
|
||||
end
|
||||
end
|
||||
local fx, fy = Plaza.cellFor(Plaza.CAP)
|
||||
result(farSlot == Plaza.CAP, "someone holds slot 40 at " .. tostring(fx) .. "," .. tostring(fy))
|
||||
place(12, 22, "up")
|
||||
wait(10)
|
||||
local function walk(dir, axis, target)
|
||||
for _ = 1, 600 do
|
||||
local v = axis == "x" and Player.cellX or Player.cellY
|
||||
if v == target then
|
||||
waitFor(function() return not Player.moving end, 2, 60)
|
||||
return (axis == "x" and Player.cellX or Player.cellY) == target
|
||||
end
|
||||
if Player.moving then wait(1) else U.hold(game, dir, 1) end
|
||||
end
|
||||
return false
|
||||
end
|
||||
local walked = walk("up", "y", 18) and walk("left", "x", fx + 1) and walk("up", "y", fy)
|
||||
if walked and Player.facing ~= "left" then
|
||||
U.tap(game, "left")
|
||||
wait(6)
|
||||
end
|
||||
print("[driver] walked to " .. tostring(Player.cellX) .. "," .. tostring(Player.cellY) .. " " .. tostring(Player.facing))
|
||||
result(walked and Player.cellX == fx + 1 and Player.cellY == fy, "walked through the crowd to slot 40's cell")
|
||||
U.tap(game, "a")
|
||||
local prompt = waitFor(function()
|
||||
if Message.isOpen() and Message.isWaiting() and (Message._page or 1) < #(Message._pages or {}) then
|
||||
U.tap(game, "a")
|
||||
end
|
||||
local Screen = require("src.ui.game3.union_room")
|
||||
return Screen.isOpen() and Screen.mode == "activity"
|
||||
end, 8, 600)
|
||||
result(prompt, "talking to slot 40 opens the do-something prompt")
|
||||
result(Union.vobj(Plaza.CAP).dir == Union.DIR.EAST, "slot 40 turned to face the player")
|
||||
wait(10)
|
||||
U.still(game, DIR .. "/g3link_union_plaza_slot40_prompt.png")
|
||||
U.tap(game, "b")
|
||||
drive(function() return Union.state == "main" and not Message.isOpen() end, 8)
|
||||
|
||||
relay:handle(blue, { type = "invite", to = "a0000001", activity = "chat", detail = {}, profile = live })
|
||||
result(waitFor(function() return Union.state == "player_contacted_you" or Union.state == "handle_activity_request" end,
|
||||
@@ -170,8 +241,16 @@ return function(game)
|
||||
"leaving the chat ends it")
|
||||
result(Client.room() == nil, "and leaves the private room")
|
||||
|
||||
Map.load(nil, game, CENTER_2F, { x = 7, y = 5, facing = "down" })
|
||||
wait(90)
|
||||
place(12, 22, "down")
|
||||
wait(10)
|
||||
for _ = 1, 20 do
|
||||
if Map.current == CENTER_2F then break end
|
||||
U.hold(game, "down", 8)
|
||||
relay:pump()
|
||||
end
|
||||
result(waitFor(function() return Map.current == CENTER_2F end, 8, 300), "the exit pad warps back to the PC 2F")
|
||||
wait(60)
|
||||
U.still(game, DIR .. "/g3link_union_plaza_exit_2f.png")
|
||||
result(Union.state == "off", "leaving the Union Room stops it")
|
||||
result(Client.plaza() == nil, "and leaves the plaza")
|
||||
|
||||
@@ -194,9 +273,9 @@ return function(game)
|
||||
local rows = LinkMenu.rows
|
||||
print("[driver] rows trade=" .. tostring(rows[1] and rows[1].count) .. " battle=" .. tostring(rows[2] and rows[2].count)
|
||||
.. " union=" .. tostring(rows[3] and rows[3].count) .. " total=" .. tostring(rows[4] and rows[4].count))
|
||||
result(rows[Status.GROUPTYPE.UNION] and rows[Status.GROUPTYPE.UNION].count == 2,
|
||||
"UNION ROOM counts the two plaza members")
|
||||
result(rows[Status.GROUPTYPE.TOTAL] and rows[Status.GROUPTYPE.TOTAL].count == 2, "TOTAL = trade + battle + union")
|
||||
result(rows[Status.GROUPTYPE.UNION] and rows[Status.GROUPTYPE.UNION].count == OTHERS,
|
||||
"UNION ROOM counts the 39 plaza members")
|
||||
result(rows[Status.GROUPTYPE.TOTAL] and rows[Status.GROUPTYPE.TOTAL].count == OTHERS, "TOTAL = trade + battle + union")
|
||||
result(relay.wireless["a0000001"] == true, "the monitor subscribed to wireless counts")
|
||||
U.shot(game, DIR .. "/g3link_wireless_monitor_counts.png")
|
||||
U.tap(game, "a")
|
||||
|
||||
@@ -0,0 +1,228 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/g3link"
|
||||
|
||||
local CENTER_2F = "FR_VIRIDIAN_CITY_POKEMON_CENTER_2F"
|
||||
local Plaza = require("src.core.game3.link.union_plaza_map")
|
||||
-- pokefirered/include/constants/flags.h:1375
|
||||
local FLAG_SYS_POKEDEX_GET = 0x829
|
||||
local ROLE = os.getenv("B4_ROLE") or "A"
|
||||
local NAMES = { A = "ALICE", B = "BOB", C = "CAROL" }
|
||||
local NAME = NAMES[ROLE]
|
||||
-- pokefirered/include/constants/species.h:10
|
||||
local CHARIZARD, CATERPIE, BULBASAUR = 6, 10, 1
|
||||
|
||||
local failures = 0
|
||||
local function result(ok, label)
|
||||
print((ok and "PASS " or "FAIL ") .. "[" .. ROLE .. "] " .. label)
|
||||
if not ok then failures = failures + 1 end
|
||||
return ok
|
||||
end
|
||||
|
||||
local function finish()
|
||||
local Link = package.loaded["src.core.game3.link"]
|
||||
if Link then pcall(Link.reset) end
|
||||
local Client = package.loaded["src.online.Client"]
|
||||
if Client then pcall(Client.disconnect) end
|
||||
print((failures == 0 and "PASS" or "FAIL") .. " g3link_union_plaza_live role=" .. ROLE .. " failures=" .. failures)
|
||||
love.event.quit(failures == 0 and 0 or 1)
|
||||
coroutine.yield()
|
||||
end
|
||||
|
||||
local function now() return love.timer.getTime() end
|
||||
|
||||
return function(game)
|
||||
print("PASS driver_started")
|
||||
for _ = 1, 900 do
|
||||
if game.phase == "boot" and game.boot then break end
|
||||
U.wait(1)
|
||||
end
|
||||
game:_handleBootAction({ action = "new_game", name = NAME })
|
||||
U.wait(240)
|
||||
|
||||
local Runtime = require("src.core.game3.runtime")
|
||||
local Map = require("src.core.game3.map")
|
||||
local Space = require("src.core.game3.scripting.space")
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
local Player = require("src.core.game3.player")
|
||||
local Message = require("src.ui.game3.message")
|
||||
local Choice = require("src.ui.game3.choice")
|
||||
local Party = require("src.core.game3.party")
|
||||
local Link = require("src.core.game3.link")
|
||||
local Union = require("src.core.game3.link.union_room")
|
||||
local LB = require("src.core.game3.link.battle")
|
||||
local Chat = require("src.core.game3.link.chat")
|
||||
local Screen = require("src.ui.game3.union_room")
|
||||
local Client = require("src.online.Client")
|
||||
local Natives = require("src.core.game3.scripting.natives")
|
||||
local MapCatalog = require("src.import.gba.map_catalog")
|
||||
|
||||
local session = Runtime.getSession()
|
||||
session.name = NAME
|
||||
if ROLE == "A" then Party.giveMon(session, CHARIZARD, 70)
|
||||
elseif ROLE == "B" then Party.giveMon(session, CATERPIE, 3)
|
||||
else Party.giveMon(session, BULBASAUR, 5) end
|
||||
|
||||
local function ctx() return Space.vm and Space.vm.ctx end
|
||||
local function place(x, y, facing)
|
||||
Player.cellX, Player.cellY = x, y
|
||||
Player.px, Player.py = x * 16, y * 16
|
||||
Player.targetX, Player.targetY = x, y
|
||||
Player.facing = facing
|
||||
game.session.x, game.session.y, game.session.facing = x, y, facing
|
||||
end
|
||||
local function waitFor(cond, seconds, onFrame)
|
||||
local t0 = now()
|
||||
while not cond() do
|
||||
if onFrame then onFrame() end
|
||||
U.wait(1)
|
||||
if now() - t0 > (seconds or 5) then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
local function pageThrough()
|
||||
if Message.isOpen() and Message.isWaiting() and (Message._page or 1) < #(Message._pages or {}) then
|
||||
U.tap(game, "a")
|
||||
end
|
||||
end
|
||||
local function findByName(name)
|
||||
for slot = 1, Plaza.CAP do
|
||||
local p = Union.players[slot]
|
||||
if p and not p.gone and p.name == name then return slot, p end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
local function talkTo(slot)
|
||||
local x, y = Plaza.cellFor(slot)
|
||||
place(x, y + 1, "up")
|
||||
U.wait(4)
|
||||
U.tap(game, "a")
|
||||
end
|
||||
local function mashBattle(seconds)
|
||||
local sawBattle = waitFor(function() return LB.state == "battle" end, 30, pageThrough)
|
||||
if not sawBattle then return false, false end
|
||||
local t0, k = now(), 0
|
||||
while LB.state == "battle" or LB.state == "setup" do
|
||||
k = k + 1
|
||||
if k % 8 == 0 then U.tap(game, "a") else U.wait(1) end
|
||||
if now() - t0 > seconds then return true, false end
|
||||
end
|
||||
return true, waitFor(function() return Union.state == "main" end, 30, function()
|
||||
if Message.isOpen() then U.tap(game, "a") end
|
||||
end)
|
||||
end
|
||||
|
||||
local live = Link.liveProfile()
|
||||
if not result(live ~= nil, "live g3 profile (vanilla)") then return finish() end
|
||||
Link.connect()
|
||||
if not result(waitFor(function() return Client.state() == "online" end, 20), "online on the local relay") then
|
||||
print("[driver] client state=" .. tostring(Client.state()) .. " err=" .. tostring(Client.error()))
|
||||
return finish()
|
||||
end
|
||||
|
||||
Map.load(nil, game, CENTER_2F, { x = 5, y = 1, facing = "up" })
|
||||
U.wait(30)
|
||||
Flags.setFlag(Space.store, ctx(), FLAG_SYS_POKEDEX_GET, true)
|
||||
place(5, 1, "up")
|
||||
U.wait(5)
|
||||
-- pokefirered/data/specials.inc:12
|
||||
Natives.special(ctx(), 0x01, Space.vm.adapters)
|
||||
local slotKey = MapCatalog.slotKeyFor("FR_UNION_ROOM") or ""
|
||||
local group, num = slotKey:match("(%d+)%D+(%d+)")
|
||||
-- pokefirered/data/scripts/cable_club.inc:797
|
||||
Space.vm.adapters.warp(tonumber(group), tonumber(num), 0xFF, 7, 11, function() end, "warpspinenter")
|
||||
if not result(waitFor(function() return Space.mapId == Plaza.MAP_ID and Union.state == "main" end, 30),
|
||||
"entered the plaza over the real relay") then
|
||||
return finish()
|
||||
end
|
||||
result(waitFor(function() return Client.plaza() ~= nil end, 10), "plaza_state from the relay")
|
||||
local mySlot = Client.plaza() and Client.plaza().you
|
||||
print("[driver] " .. ROLE .. " slot=" .. tostring(mySlot) .. " instance=" .. tostring(Client.plaza() and Client.plaza().instance)
|
||||
.. " cap=" .. tostring(Client.plaza() and Client.plaza().cap) .. " rev=" .. tostring(Client.plaza() and Client.plaza().rev))
|
||||
result(Client.plaza() and Client.plaza().cap == 40, "the relay reports a 40-cap plaza")
|
||||
|
||||
if ROLE == "A" then
|
||||
local slotB
|
||||
result(waitFor(function() slotB = findByName("BOB") return slotB ~= nil end, 60), "BOB appears in the plaza")
|
||||
if not slotB then return finish() end
|
||||
U.wait(60)
|
||||
local bx, by = Plaza.cellFor(slotB)
|
||||
print("[driver] A sees BOB on slot " .. slotB .. " cell " .. bx .. "," .. by)
|
||||
U.still(game, DIR .. "/live_a_sees_bob.png")
|
||||
talkTo(slotB)
|
||||
result(waitFor(function() return Screen.isOpen() and Screen.mode == "activity" end, 10, pageThrough),
|
||||
"talking to BOB opens the do-something prompt")
|
||||
Screen.cursor = 2
|
||||
U.tap(game, "a")
|
||||
local started, back = mashBattle(180)
|
||||
result(started, "the battle with BOB started")
|
||||
result(back, "the battle ended and ALICE is back in the Union Room")
|
||||
result(waitFor(function() return Space.mapId == Plaza.MAP_ID end, 10), "still on the plaza map")
|
||||
U.wait(90)
|
||||
local slotB2 = findByName("BOB")
|
||||
result(Client.plaza() and Client.plaza().you == mySlot, "ALICE kept slot " .. tostring(mySlot))
|
||||
result(slotB2 == slotB, "BOB is back on slot " .. tostring(slotB))
|
||||
local v = Union.vobj(slotB)
|
||||
result(v and v.visible and v.x == bx and v.y == by, "BOB's avatar stands on the same cell")
|
||||
U.still(game, DIR .. "/live_a_after_battle.png")
|
||||
U.wait(120)
|
||||
talkTo(slotB)
|
||||
result(waitFor(function() return Screen.isOpen() and Screen.mode == "activity" end, 10, pageThrough),
|
||||
"BOB's menu again")
|
||||
Screen.cursor = 3
|
||||
U.tap(game, "a")
|
||||
result(waitFor(function() return Chat.isActive() end, 40, pageThrough), "ALICE and BOB are chatting")
|
||||
local three = waitFor(function()
|
||||
local room = Client.room()
|
||||
return room and room.players and #room.players >= 3
|
||||
end, 120)
|
||||
result(three, "CAROL joined the chat")
|
||||
U.wait(120)
|
||||
U.still(game, DIR .. "/live_a_chat_three.png")
|
||||
Chat.stop("left")
|
||||
waitFor(function() return Union.state == "main" end, 10)
|
||||
elseif ROLE == "B" then
|
||||
result(waitFor(function() return Choice.active end, 120, pageThrough), "ALICE's battle request rings BOB")
|
||||
U.wait(10)
|
||||
U.tap(game, "a")
|
||||
local started, back = mashBattle(180)
|
||||
result(started, "the battle with ALICE started")
|
||||
result(back, "BOB is back in the Union Room")
|
||||
U.wait(90)
|
||||
result(Client.plaza() and Client.plaza().you == mySlot, "BOB kept slot " .. tostring(mySlot))
|
||||
local slotA = findByName("ALICE")
|
||||
local ax, ay = Plaza.cellFor(slotA or 0)
|
||||
local v = slotA and Union.vobj(slotA)
|
||||
result(v and v.visible and v.x == ax and v.y == ay, "ALICE's avatar stands on her cell")
|
||||
U.still(game, DIR .. "/live_b_after_battle.png")
|
||||
result(waitFor(function() return Choice.active end, 120, pageThrough), "ALICE's chat request rings BOB")
|
||||
U.wait(10)
|
||||
U.tap(game, "a")
|
||||
result(waitFor(function() return Chat.isActive() end, 40, pageThrough), "BOB is chatting")
|
||||
waitFor(function()
|
||||
local room = Client.room()
|
||||
return room and room.players and #room.players >= 3
|
||||
end, 120)
|
||||
waitFor(function() return not Chat.isActive() end, 60)
|
||||
Chat.stop("left")
|
||||
waitFor(function() return Union.state == "main" end, 10)
|
||||
else
|
||||
local slotB
|
||||
result(waitFor(function()
|
||||
slotB = findByName("BOB")
|
||||
local p = slotB and Union.players[slotB]
|
||||
return p and (p.activity % Union.IN_UNION_ROOM) == Union.ACTIVITY.CHAT
|
||||
end, 300), "CAROL sees BOB chatting")
|
||||
if not slotB then return finish() end
|
||||
U.wait(30)
|
||||
talkTo(slotB)
|
||||
result(waitFor(function() return Choice.active end, 15, pageThrough), "the join-chat prompt")
|
||||
U.still(game, DIR .. "/live_c_join_prompt.png")
|
||||
U.tap(game, "a")
|
||||
result(waitFor(function() return Chat.isActive() end, 40, pageThrough), "CAROL joined the chat")
|
||||
U.wait(120)
|
||||
U.still(game, DIR .. "/live_c_chat.png")
|
||||
Chat.stop("left")
|
||||
waitFor(function() return Union.state == "main" end, 10)
|
||||
end
|
||||
finish()
|
||||
end
|
||||
@@ -0,0 +1,196 @@
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
local CENTER_2F = "FR_VIRIDIAN_CITY_POKEMON_CENTER_2F"
|
||||
local Plaza = require("src.core.game3.link.union_plaza_map")
|
||||
-- pokefirered/include/constants/flags.h:1375
|
||||
local FLAG_SYS_POKEDEX_GET = 0x829
|
||||
local SECONDS = tonumber(os.getenv("PERF_SECONDS")) or 20
|
||||
local LABEL = os.getenv("POKEPORT_TOUCH") == "1" and "touch" or "desktop"
|
||||
|
||||
local failures = 0
|
||||
local function result(ok, label)
|
||||
print((ok and "PASS " or "FAIL ") .. label)
|
||||
if not ok then failures = failures + 1 end
|
||||
return ok
|
||||
end
|
||||
|
||||
local function finish()
|
||||
local Link = package.loaded["src.core.game3.link"]
|
||||
if Link then pcall(Link.reset) end
|
||||
local Client = package.loaded["src.online.Client"]
|
||||
if Client then pcall(Client.disconnect) end
|
||||
if failures == 0 then
|
||||
print("PASS g3link_union_plaza_perf")
|
||||
love.event.quit(0)
|
||||
else
|
||||
print("FAIL g3link_union_plaza_perf failures=" .. failures)
|
||||
love.event.quit(1)
|
||||
end
|
||||
end
|
||||
|
||||
local function now() return love.timer.getTime() end
|
||||
|
||||
return function(game)
|
||||
print("PASS driver_started")
|
||||
for _ = 1, 900 do
|
||||
if game.phase == "boot" and game.boot then break end
|
||||
U.wait(1)
|
||||
end
|
||||
game:_handleBootAction({ action = "new_game", name = "RED" })
|
||||
U.wait(240)
|
||||
|
||||
local Map = require("src.core.game3.map")
|
||||
local Space = require("src.core.game3.scripting.space")
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
local Player = require("src.core.game3.player")
|
||||
local Message = require("src.ui.game3.message")
|
||||
local Choice = require("src.ui.game3.choice")
|
||||
local Link = require("src.core.game3.link")
|
||||
local Union = require("src.core.game3.link.union_room")
|
||||
local Client = require("src.online.Client")
|
||||
local Relay = require("tests.support.fake_relay")
|
||||
local Natives = require("src.core.game3.scripting.natives")
|
||||
local MapCatalog = require("src.import.gba.map_catalog")
|
||||
|
||||
local relay = Relay.new({ clock = function() return now() end })
|
||||
local me = relay:seat("a0000001", "RED")
|
||||
Client.configure({ relayAddress = "fake:1", connect = function() return me.transport end })
|
||||
|
||||
local function ctx() return Space.vm and Space.vm.ctx end
|
||||
local function wait(n)
|
||||
for _ = 1, n do
|
||||
relay:pump()
|
||||
U.wait(1)
|
||||
end
|
||||
end
|
||||
local function waitFor(cond, seconds)
|
||||
local t0 = now()
|
||||
while not cond() do
|
||||
relay:pump()
|
||||
U.wait(1)
|
||||
if now() - t0 > (seconds or 5) then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
local function drive(cond, seconds)
|
||||
local t0 = now()
|
||||
while not cond() and now() - t0 < (seconds or 10) do
|
||||
if Choice.active or Message.isOpen() then U.tap(game, "a") end
|
||||
wait(6)
|
||||
end
|
||||
return cond()
|
||||
end
|
||||
|
||||
local live = Link.liveProfile()
|
||||
if not result(live ~= nil, "the live g3 profile computes (vanilla game)") then return finish() end
|
||||
Link.connect()
|
||||
result(waitFor(function() return Client.state() == "online" end, 5), "online on the relay")
|
||||
Map.load(nil, game, CENTER_2F, { x = 5, y = 1, facing = "up" })
|
||||
wait(30)
|
||||
Flags.setFlag(Space.store, ctx(), FLAG_SYS_POKEDEX_GET, true)
|
||||
Player.cellX, Player.cellY, Player.targetX, Player.targetY = 5, 1, 5, 1
|
||||
Player.px, Player.py, Player.facing = 5 * 16, 1 * 16, "up"
|
||||
wait(5)
|
||||
-- pokefirered/data/specials.inc:12
|
||||
Natives.special(ctx(), 0x01, Space.vm.adapters)
|
||||
local slot = MapCatalog.slotKeyFor("FR_UNION_ROOM") or ""
|
||||
local group, num = slot:match("(%d+)%D+(%d+)")
|
||||
-- pokefirered/data/scripts/cable_club.inc:797
|
||||
Space.vm.adapters.warp(tonumber(group), tonumber(num), 0xFF, 7, 11, function() end, "warpspinenter")
|
||||
if not result(drive(function() return Space.mapId == Plaza.MAP_ID and Union.state == "main" end, 60),
|
||||
"entered the plaza") then
|
||||
return finish()
|
||||
end
|
||||
|
||||
local peers = {}
|
||||
local function addPeers(n)
|
||||
while #peers < n do
|
||||
local i = #peers + 1
|
||||
local id = string.format("d%07x", i)
|
||||
local s = relay:seat(id, "P" .. i)
|
||||
relay:handle(s, { type = "lobby_hello", protocol = 3, name = "P" .. i, profiles = { live },
|
||||
presence = { where = "launcher", status = "idle", version = "firered" } })
|
||||
s.avatar = { name = "P" .. i, trainerId = i * 37, gender = i % 2, version = "firered" }
|
||||
relay:handle(s, { type = "plaza_join", kind = "union", cap = 40, profile = live, avatar = s.avatar })
|
||||
peers[i] = s
|
||||
end
|
||||
end
|
||||
|
||||
local ownUpdate, ownDraw = rawget(game, "update"), rawget(game, "draw")
|
||||
local rawUpdate, rawDraw = game.update, game.draw
|
||||
local upd, drw = { sum = 0, max = 0, n = 0 }, { sum = 0, max = 0, n = 0 }
|
||||
local function timed(fn, acc)
|
||||
return function(self, ...)
|
||||
local t = love.timer.getTime()
|
||||
local a, b = fn(self, ...)
|
||||
local d = (love.timer.getTime() - t) * 1000
|
||||
acc.sum, acc.n = acc.sum + d, acc.n + 1
|
||||
if d > acc.max then acc.max = d end
|
||||
return a, b
|
||||
end
|
||||
end
|
||||
game.update = timed(rawUpdate, upd)
|
||||
game.draw = timed(rawDraw, drw)
|
||||
|
||||
local STATUSES = { "chatting", "battling", "idle" }
|
||||
local function measure(n)
|
||||
addPeers(n)
|
||||
result(waitFor(function() return Union.playerCount() == n end, 10), n .. " avatars in the room")
|
||||
Player.cellX, Player.cellY, Player.targetX, Player.targetY = 12, 12, 12, 12
|
||||
Player.px, Player.py = 12 * 16, 12 * 16
|
||||
wait(90)
|
||||
upd.sum, upd.max, upd.n = 0, 0, 0
|
||||
drw.sum, drw.max, drw.n = 0, 0, 0
|
||||
collectgarbage("collect")
|
||||
local kb0 = collectgarbage("count")
|
||||
local last = kb0
|
||||
local allocated = 0
|
||||
local frame = { sum = 0, max = 0, n = 0 }
|
||||
local builds0 = Union.plazaBuilds or 0
|
||||
local t0, tPrev, nextChange, k = now(), now(), now() + 1, 0
|
||||
while now() - t0 < SECONDS do
|
||||
relay:pump()
|
||||
for i = 1, #peers do
|
||||
local box = peers[i].transport.inbox
|
||||
for j = #box, 1, -1 do box[j] = nil end
|
||||
end
|
||||
if now() >= nextChange then
|
||||
k = k + 1
|
||||
local s = peers[(k - 1) % n + 1]
|
||||
s.status = STATUSES[(k - 1) % #STATUSES + 1]
|
||||
relay:plazaChanged(s)
|
||||
nextChange = nextChange + 1
|
||||
end
|
||||
U.wait(1)
|
||||
local t = now()
|
||||
local d = (t - tPrev) * 1000
|
||||
tPrev = t
|
||||
frame.sum, frame.n = frame.sum + d, frame.n + 1
|
||||
if d > frame.max then frame.max = d end
|
||||
local kb = collectgarbage("count")
|
||||
if kb > last then allocated = allocated + (kb - last) end
|
||||
last = kb
|
||||
end
|
||||
local elapsed = now() - t0
|
||||
collectgarbage("collect")
|
||||
local growth = collectgarbage("count") - kb0
|
||||
local line = string.format(
|
||||
"PERF %s members=%d seconds=%.1f frames=%d frame_ms mean=%.2f max=%.2f update_ms mean=%.3f max=%.3f"
|
||||
.. " draw_ms mean=%.3f max=%.3f alloc_kb_per_s=%.1f gc_growth_kb=%.1f status_changes=%d rebuilds=%d",
|
||||
LABEL, n, elapsed, frame.n, frame.sum / math.max(1, frame.n), frame.max,
|
||||
upd.sum / math.max(1, upd.n), upd.max, drw.sum / math.max(1, drw.n), drw.max,
|
||||
allocated / elapsed, growth, k, (Union.plazaBuilds or 0) - builds0)
|
||||
print(line)
|
||||
local f = io.open(os.getenv("PERF_OUT") or "/tmp/g3link_union_plaza_perf.log", "a")
|
||||
if f then
|
||||
f:write(line, "\n")
|
||||
f:close()
|
||||
end
|
||||
result((Union.plazaBuilds or 0) - builds0 <= k + 1, "one rebuild per status change at " .. n)
|
||||
end
|
||||
|
||||
measure(8)
|
||||
measure(Plaza.CAP - 1)
|
||||
game.update, game.draw = ownUpdate, ownDraw
|
||||
finish()
|
||||
end
|
||||
@@ -2,7 +2,8 @@ local U = require("tests.drivers.util")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/g3link"
|
||||
|
||||
local CENTER_2F = "FR_VIRIDIAN_CITY_POKEMON_CENTER_2F"
|
||||
local UNION_ROOM = "FR_UNION_ROOM"
|
||||
local Plaza = require("src.core.game3.link.union_plaza_map")
|
||||
local UNION_ROOM = Plaza.MAP_ID
|
||||
-- pokefirered/include/constants/flags.h:1375
|
||||
local FLAG_SYS_POKEDEX_GET = 0x829
|
||||
-- pokefirered/include/constants/species.h:5
|
||||
@@ -69,16 +70,6 @@ return function(game)
|
||||
Party.giveMon(session, PIDGEY, 9)
|
||||
|
||||
local relay = Relay.new({ clock = function() return now() end })
|
||||
local plazaState = relay.plazaStateMsg
|
||||
relay.plazaStateMsg = function(self, s)
|
||||
local msg = plazaState(self, s)
|
||||
for _, m in ipairs(msg.members or {}) do
|
||||
local ss = self.sessions[m.id]
|
||||
if ss and ss.group then m.group = ss.group end
|
||||
if ss and ss.status then m.status = ss.status end
|
||||
end
|
||||
return msg
|
||||
end
|
||||
local me = relay:seat("a0000001", "RED")
|
||||
Client.configure({ relayAddress = "fake:1", connect = function() return me.transport end })
|
||||
|
||||
@@ -125,9 +116,6 @@ return function(game)
|
||||
return cond()
|
||||
end
|
||||
end
|
||||
local function pushPlaza()
|
||||
relay:to(me, relay:plazaStateMsg(me))
|
||||
end
|
||||
|
||||
local live = Link.liveProfile()
|
||||
if not result(live ~= nil, "the live g3 profile computes (vanilla game)") then return finish() end
|
||||
@@ -163,32 +151,34 @@ return function(game)
|
||||
local yellow = peer("d0000004", "YELLOW", 6, 0, "firered")
|
||||
for _, s in ipairs({ green, pink, yellow }) do
|
||||
s.presence.where = "union"
|
||||
relay:handle(s, { type = "plaza_join", kind = "union", profile = live, avatar = s.avatar })
|
||||
relay:handle(s, { type = "plaza_join", kind = "union", cap = 40, profile = live, avatar = s.avatar })
|
||||
end
|
||||
local group = { leader = pink.id, members = { pink.id, yellow.id }, activity = "chat" }
|
||||
pink.group, pink.status = group, "chatting"
|
||||
yellow.group, yellow.status = group, "chatting"
|
||||
pushPlaza()
|
||||
result(waitFor(function() return Union.playerCount() == 2 end, 10, 60),
|
||||
"GREEN takes a leader slot and PINK's chat group another")
|
||||
relay:plazaChanged(pink)
|
||||
relay:plazaChanged(yellow)
|
||||
result(waitFor(function() return Union.playerCount() == 3 end, 10, 60),
|
||||
"GREEN, PINK and YELLOW each take their own plaza slot")
|
||||
local slotG = Union.slotForId(green.id)
|
||||
local slotP = Union.slotForId(pink.id)
|
||||
result(slotG ~= nil and slotP ~= nil, "both have slots")
|
||||
result(waitFor(function() return slotG and Union.leaderObj(slotG).state == 1 end, 5, 60),
|
||||
"GREEN flew down onto her spot")
|
||||
result(slotP and Union.vobjVisible(slotP, 0) and Union.vobjVisible(slotP, 1),
|
||||
"PINK and YELLOW stand at the group offsets")
|
||||
result(slotP and Union.vobj(slotP, 0).dir == Union.DIR.SOUTH, "the chat leader faces south")
|
||||
place(7, 9, "up")
|
||||
local slotY = Union.slotForId(yellow.id)
|
||||
result(slotG ~= nil and slotP ~= nil and slotY ~= nil, "all three have slots")
|
||||
result(waitFor(function() return slotG and Union.vobjVisible(slotG) and Union.vobj(slotG).anim == nil end, 5, 60),
|
||||
"GREEN flew down onto her cell")
|
||||
local gx, gy = Plaza.cellFor(slotG)
|
||||
result(Union.vobj(slotG).x == gx and Union.vobj(slotG).y == gy, "on her slot's cell")
|
||||
result(slotP and Union.vobjVisible(slotP) and Union.vobjVisible(slotY), "PINK and YELLOW stand on their own cells")
|
||||
result(slotP and Union.vobj(slotP).dir == Union.DIR.SOUTH, "a chatter faces south")
|
||||
place(12, 22, "up")
|
||||
wait(40)
|
||||
U.still(game, DIR .. "/g3link_union_room_avatars.png")
|
||||
|
||||
print("[driver] 2. talking to a trainer opens the invite menu")
|
||||
local Objects = require("src.core.game3.objects")
|
||||
local opened = false
|
||||
for _ = 1, 20 do
|
||||
local eo = Objects.find(Union.LOCAL_IDS[slotG])
|
||||
if eo and not eo.moving then
|
||||
local eo = { cellX = gx, cellY = gy }
|
||||
do
|
||||
place(eo.cellX, eo.cellY + 1, "up")
|
||||
wait(2)
|
||||
U.tap(game, "a")
|
||||
@@ -217,7 +207,7 @@ return function(game)
|
||||
drive(function() return Union.state == "main" and not Message.isOpen() end, 8)
|
||||
|
||||
print("[driver] 3. an incoming invite rings")
|
||||
place(7, 9, "up")
|
||||
place(12, 22, "up")
|
||||
wait(6)
|
||||
relay:handle(green, { type = "invite", to = me.id, activity = "chat", detail = {}, profile = live })
|
||||
result(waitFor(function() return Union.state == "player_contacted_you" or Union.state == "handle_activity_request" end,
|
||||
@@ -319,8 +309,8 @@ return function(game)
|
||||
end
|
||||
local asked = false
|
||||
for _ = 1, 20 do
|
||||
local eo = Objects.find(Union.LOCAL_IDS[slotG])
|
||||
if eo and not eo.moving then
|
||||
local eo = { cellX = gx, cellY = gy }
|
||||
do
|
||||
place(eo.cellX, eo.cellY + 1, "up")
|
||||
wait(2)
|
||||
U.tap(game, "a")
|
||||
@@ -351,7 +341,7 @@ return function(game)
|
||||
U.still(game, DIR .. "/g3link_union_request_declined.png")
|
||||
drive(function() return Union.state == "main" and not Message.isOpen() end, 8)
|
||||
|
||||
place(7, 9, "up")
|
||||
place(12, 22, "up")
|
||||
wait(6)
|
||||
relay:handle(green, { type = "invite", to = me.id, activity = "chat", detail = {}, profile = live })
|
||||
result(waitFor(pageThrough(function() return Choice.active end), 8, 600), "GREEN asks to chat again")
|
||||
@@ -368,7 +358,6 @@ return function(game)
|
||||
print("[driver] 5. the trading board")
|
||||
relay:handle(green, { type = "presence", board = { species = BULBASAUR, level = 14, wantType = 10 } })
|
||||
relay:handle(yellow, { type = "presence", board = { species = PIDGEY, level = 5, wantType = 12 } })
|
||||
pushPlaza()
|
||||
result(waitFor(function() return #Union.boardOffers() >= 1 end, 5, 120), "GREEN's offer is on the board")
|
||||
Union.trade().playerSpecies, Union.trade().playerLevel, Union.trade().type = CHARMANDER, 10, 11
|
||||
place(2, 2, "up")
|
||||
@@ -395,7 +384,7 @@ return function(game)
|
||||
drive(function() return Union.state == "main" and not Message.isOpen() end, 8)
|
||||
|
||||
print("[driver] 6. a minigame group recruiting")
|
||||
place(7, 9, "up")
|
||||
place(12, 22, "up")
|
||||
wait(6)
|
||||
Flags.setVar(Space.store, ctx(), Link.VAR_0x8004, Union.LINK_GROUP.BERRY_CRUSH)
|
||||
local c = ctx()
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/game3_cut_grass_2470"
|
||||
local failures = 0
|
||||
local function result(ok, label)
|
||||
print((ok and "PASS " or "FAIL ") .. label)
|
||||
if not ok then failures = failures + 1 end
|
||||
return ok
|
||||
end
|
||||
local function finish()
|
||||
result(failures == 0, "game3_cut_grass_2470")
|
||||
love.event.quit(failures == 0 and 0 or 1)
|
||||
end
|
||||
|
||||
return function(game)
|
||||
for _ = 1, 600 do
|
||||
if game.phase == "boot" and game.boot then break end
|
||||
U.wait(1)
|
||||
end
|
||||
if not result(game.phase == "boot" and game.boot ~= nil, "boot_ready") then return finish() end
|
||||
game:_handleBootAction({ action = "new_game", name = "RED" })
|
||||
U.wait(180)
|
||||
local Runtime = require("src.core.game3.runtime")
|
||||
local Map = require("src.core.game3.map")
|
||||
local Player = require("src.core.game3.player")
|
||||
local Collision = require("src.core.game3.collision")
|
||||
local Field = require("src.core.game3.field")
|
||||
local FieldMoves = require("src.core.game3.field_moves")
|
||||
local Party = require("src.core.game3.party")
|
||||
local Objects = require("src.core.game3.objects")
|
||||
require("src.core.game3.encounters").onStep = function() return nil end
|
||||
require("src.core.game3.trainer_sight").check = function() return false end
|
||||
local session = Runtime.getSession()
|
||||
if not result(session ~= nil, "new_game_reached_field") then return finish() end
|
||||
session.party = {}
|
||||
Party.giveMon(session, 29, 20)
|
||||
local mon = session.party[1]
|
||||
|
||||
local function place(map, x, y)
|
||||
Map.load(Runtime._mod, game, map, { x = x, y = y, facing = "down" })
|
||||
Player.surfing, Player.biking = false, false
|
||||
U.wait(60)
|
||||
return Map.current == map and Player.cellX == x and Player.cellY == y
|
||||
end
|
||||
local function layout() return Map.currentDef().midLayout end
|
||||
local function findPatch(map)
|
||||
if Map.current ~= map then place(map, 0, 0) end
|
||||
local L = game.data.maps[map].midLayout
|
||||
for y = 1, L.height - 2 do
|
||||
for x = 1, L.width - 2 do
|
||||
local ok = true
|
||||
for dy = -1, 1 do
|
||||
for dx = -1, 1 do
|
||||
local cx, cy = x + dx, y + dy
|
||||
if L:midAt(cx, cy) ~= 0x00D or L:elevAt(cx, cy) ~= 3 or Objects.at(cx, cy) then ok = false end
|
||||
end
|
||||
end
|
||||
if ok then return x, y end
|
||||
end
|
||||
end
|
||||
end
|
||||
local function cut()
|
||||
local store = { flags = {}, vars = {} }
|
||||
require("src.core.game3.scripting.flags").setFlag(store, nil, FieldMoves.BADGE_FLAGS.CUT, true)
|
||||
local res = FieldMoves.cutFromMenu({ mon = mon, party = session.party, store = store,
|
||||
hasCuttableGrass = Collision.isGrass(Player.cellX, Player.cellY) })
|
||||
if not result(res and res.ok and res.action == "cut_grass", "party_menu_picks_cut_grass") then return false end
|
||||
Field.executeFieldMove(res)
|
||||
for _ = 1, 600 do
|
||||
if not Field.locked then break end
|
||||
U.wait(1)
|
||||
end
|
||||
U.wait(20)
|
||||
return not Field.locked
|
||||
end
|
||||
local function all3x3(cx, cy, fn)
|
||||
for dy = -1, 1 do
|
||||
for dx = -1, 1 do
|
||||
if not fn(cx + dx, cy + dy) then return false end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local MAP = "FR_ROUTE_1"
|
||||
local x, y = findPatch(MAP)
|
||||
if not result(x ~= nil, "route1_grass_patch_found") then return finish() end
|
||||
print(("[driver] %s grass patch centre (%d,%d)"):format(MAP, x, y))
|
||||
if not result(place(MAP, x, y), "placed_in_grass") then return finish() end
|
||||
result(all3x3(x, y, function(cx, cy) return Collision.isGrass(cx, cy) end), "3x3_is_grass_before")
|
||||
result(U.still(game, DIR .. "/2470_route1_grass_before_cut.png"), "shot_before")
|
||||
result(cut(), "cut_finished")
|
||||
local L = layout()
|
||||
result(all3x3(x, y, function(cx, cy) return L:midAt(cx, cy) == 0x001 end), "3x3_mowed_to_plain_mowed")
|
||||
result(all3x3(x, y, function(cx, cy) return not Collision.isGrass(cx, cy) end), "3x3_no_longer_grass")
|
||||
result(require("src.core.game3.field_effects")._fx == nil, "tall_grass_overlay_gone_from_player")
|
||||
result(U.still(game, DIR .. "/2470_route1_grass_mowed.png"), "shot_mowed")
|
||||
|
||||
place(MAP, x, y)
|
||||
L = layout()
|
||||
result(all3x3(x, y, function(cx, cy) return L:midAt(cx, cy) == 0x00D and Collision.isGrass(cx, cy) end),
|
||||
"grass_grows_back_on_reload")
|
||||
|
||||
local VF = "FR_VIRIDIAN_FOREST"
|
||||
local vf = game.data.maps[VF] and game.data.maps[VF].midLayout
|
||||
local tx, ty
|
||||
if vf then
|
||||
for yy = 1, vf.height - 2 do
|
||||
for xx = 1, vf.width - 2 do
|
||||
if not tx and vf:midAt(xx, yy) == 0x284 and vf:elevAt(xx, yy) == 3
|
||||
and vf:midAt(xx, yy - 1) == 0x00D and vf:elevAt(xx, yy - 1) == 3 then
|
||||
tx, ty = xx, yy
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
if tx then
|
||||
print(("[driver] %s 0x284 at (%d,%d)"):format(VF, tx, ty))
|
||||
if result(place(VF, tx, ty - 1), "placed_above_huge_tree_grass") then
|
||||
result(cut(), "forest_cut_finished")
|
||||
result(layout():midAt(tx, ty) == 0x281, "viridian_forest_0x284_mowed_to_0x281")
|
||||
result(U.still(game, DIR .. "/2470_viridian_forest_huge_tree_mowed.png"), "shot_forest")
|
||||
end
|
||||
else
|
||||
result(false, "viridian_forest_0x284_cell_found")
|
||||
end
|
||||
finish()
|
||||
end
|
||||
@@ -0,0 +1,129 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/bug2460"
|
||||
|
||||
local failures = 0
|
||||
local function result(ok, label)
|
||||
print((ok and "PASS " or "FAIL ") .. label)
|
||||
if not ok then failures = failures + 1 end
|
||||
return ok
|
||||
end
|
||||
|
||||
local function finish()
|
||||
if failures == 0 then
|
||||
print("PASS game3_ghost_trainer_sight_2460")
|
||||
love.event.quit(0)
|
||||
else
|
||||
print("FAIL game3_ghost_trainer_sight_2460 failures=" .. failures)
|
||||
love.event.quit(1)
|
||||
end
|
||||
end
|
||||
|
||||
return function(game)
|
||||
for _ = 1, 600 do
|
||||
if game.phase == "boot" and game.boot then break end
|
||||
U.wait(1)
|
||||
end
|
||||
game:_handleBootAction({ action = "new_game", name = "RED" })
|
||||
U.wait(180)
|
||||
|
||||
local Runtime = require("src.core.game3.runtime")
|
||||
local Map = require("src.core.game3.map")
|
||||
local Party = require("src.core.game3.party")
|
||||
local Objects = require("src.core.game3.objects")
|
||||
local Player = require("src.core.game3.player")
|
||||
local Field = require("src.core.game3.field")
|
||||
local Ghosts = require("src.core.game3.ghosts")
|
||||
local TrainerSight = require("src.core.game3.trainer_sight")
|
||||
local Battle = package.loaded["src.core.game3.battle"] or require("src.core.game3.battle")
|
||||
|
||||
local session = Runtime.getSession()
|
||||
if not result(session ~= nil, "new game reached the field") then return finish() end
|
||||
session.party = {}
|
||||
Party.giveMon(session, 7, 40)
|
||||
|
||||
local engaged = {}
|
||||
local allowReal = false
|
||||
local realEngage = TrainerSight.engage
|
||||
TrainerSight.engage = function(g, eo, dist)
|
||||
engaged[#engaged + 1] = { eo = eo, dist = dist, live = Objects.find(eo and eo.localId) == eo,
|
||||
script = eo and eo.scriptKey }
|
||||
print(string.format("[2460] engage lid=%s dist=%s live=%s script=%s", tostring(eo and eo.localId),
|
||||
tostring(dist), tostring(engaged[#engaged].live), tostring(eo and eo.scriptKey)))
|
||||
if allowReal then return realEngage(g, eo, dist) end
|
||||
end
|
||||
|
||||
local function place(mapId, x, y, facing)
|
||||
Map.load(nil, game, mapId, { x = x, y = y, facing = facing })
|
||||
game.session.x, game.session.y, game.session.facing = x, y, facing
|
||||
U.wait(20)
|
||||
end
|
||||
|
||||
local function ghostTrainers()
|
||||
local list = {}
|
||||
for mapId, pool in pairs(Ghosts._pools or {}) do
|
||||
for _, lid in ipairs(pool.order or {}) do
|
||||
local eo = pool.byId[lid]
|
||||
if eo and (tonumber(eo.sight) or 0) > 0 and TrainerSight.isTrainerType(eo) then
|
||||
list[#list + 1] = { mapId = mapId, eo = eo }
|
||||
end
|
||||
end
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
local function dwell(label, mapId, spots, frames, needCover)
|
||||
engaged = {}
|
||||
local cover, pools = 0, 0
|
||||
for _, s in ipairs(spots) do
|
||||
place(mapId, s[1], s[2], s[3])
|
||||
local list = ghostTrainers()
|
||||
pools = math.max(pools, #list)
|
||||
for _ = 1, frames do
|
||||
U.wait(1)
|
||||
if Player.cellX ~= s[1] or Player.cellY ~= s[2] then Player.reset(s[1], s[2], s[3]) end
|
||||
for _, t in ipairs(ghostTrainers()) do
|
||||
if not t.eo.hidden and TrainerSight.checkLineOfSight(t.eo, Player, game) then
|
||||
cover = cover + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
print(string.format("[2460] %s: %d neighbor-map trainers ticking, %d frames a ghost cone covered the player",
|
||||
label, pools, cover))
|
||||
result(pools > 0, label .. ": neighbor-map trainers are loaded and ticking")
|
||||
if needCover then
|
||||
result(cover > 0, label .. ": a neighbor-map trainer's local cone lined up with the player")
|
||||
end
|
||||
result(#engaged == 0, label .. ": no neighbor-map trainer engaged (engages=" .. #engaged .. ")")
|
||||
result(not Field.locked and not (Battle.isActive and Battle.isActive()), label .. ": field unlocked, no battle")
|
||||
end
|
||||
|
||||
place("FR_VERMILION_CITY", 28, 12, "up")
|
||||
U.still(game, DIR .. "/2460_01_vermilion_under_dave_ghost.png")
|
||||
dwell("Vermilion vs Route 11 Dave", "FR_VERMILION_CITY",
|
||||
{ { 28, 12, "up" }, { 29, 14, "up" }, { 28, 14, "up" } }, 400, true)
|
||||
U.still(game, DIR .. "/2460_02_vermilion_mart_no_engage.png")
|
||||
|
||||
dwell("Fuchsia vs Route 17/18 bikers", "FR_FUCHSIA_CITY",
|
||||
{ { 11, 17, "up" }, { 11, 19, "up" }, { 4, 20, "up" } }, 400, true)
|
||||
U.still(game, DIR .. "/2460_03_fuchsia_no_engage.png")
|
||||
|
||||
engaged = {}
|
||||
allowReal = true
|
||||
place("FR_ROUTE_11", 28, 12, "up")
|
||||
local dave = Objects.find(4)
|
||||
result(dave ~= nil and dave.sight == 1 and TrainerSight.isTrainerType(dave),
|
||||
"control: Route 11 Dave (localId 4) loaded as a current-map trainer")
|
||||
local spotted = false
|
||||
for _ = 1, 1200 do
|
||||
if #engaged > 0 then spotted = true break end
|
||||
U.wait(1)
|
||||
end
|
||||
result(spotted and engaged[1].live and engaged[1].eo == dave and engaged[1].dist == 1,
|
||||
"control: Dave on Route 11 still spots the player after a pacing step")
|
||||
if spotted then
|
||||
U.wait(10)
|
||||
U.still(game, DIR .. "/2460_04_route11_dave_spots_player.png")
|
||||
end
|
||||
return finish()
|
||||
end
|
||||
@@ -0,0 +1,102 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/game3_import_dex_count_2461"
|
||||
|
||||
local failures = 0
|
||||
local function result(ok, label)
|
||||
print((ok and "PASS " or "FAIL ") .. label)
|
||||
if not ok then failures = failures + 1 end
|
||||
return ok
|
||||
end
|
||||
local function finish()
|
||||
print((failures == 0 and "PASS" or "FAIL") .. " import_dex_count_2461 failures=" .. failures)
|
||||
love.event.quit(failures == 0 and 0 or 1)
|
||||
end
|
||||
|
||||
local function unrle(s)
|
||||
local out = {}
|
||||
for tok in s:gmatch("%S+") do
|
||||
local k, n = tok:match("^([ZF])(%x+)$")
|
||||
if k then
|
||||
out[#out + 1] = string.rep(k == "Z" and "\0" or "\255", tonumber(n, 16))
|
||||
else
|
||||
out[#out + 1] = (tok:gsub("%x%x", function(h) return string.char(tonumber(h, 16)) end))
|
||||
end
|
||||
end
|
||||
return table.concat(out)
|
||||
end
|
||||
|
||||
return function(game)
|
||||
for _ = 1, 900 do
|
||||
if game.phase == "boot" and game.boot then break end
|
||||
U.wait(1)
|
||||
end
|
||||
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local SaveSerializer = require("src.core.SaveSerializer")
|
||||
local SaveFileIO = require("src.import.SaveFileIO")
|
||||
local Boot = require("src.ui.game3.boot")
|
||||
local Dex = require("src.core.game3.dex")
|
||||
local version = GameVersion.get()
|
||||
local cart = unrle(require("tests.fixture_data.gen3_saves").images.fr_rich_game)
|
||||
love.filesystem.write("picked_cart_2461.sav", cart)
|
||||
local ok, slotId = SaveFileIO.importToSlot("picked_cart_2461.sav", version)
|
||||
love.filesystem.remove("picked_cart_2461.sav")
|
||||
if not result(ok == true, "the FireRed cart imports into " .. version .. " (" .. tostring(slotId) .. ")") then
|
||||
return finish()
|
||||
end
|
||||
|
||||
local meta
|
||||
for _, row in ipairs(SaveData.listSlots(version)) do
|
||||
if row.id == slotId then meta = row.meta end
|
||||
end
|
||||
print("launcher meta dexCount=" .. tostring(meta and meta.dexCount) .. " badges=" .. tostring(meta and meta.badges)
|
||||
.. " time=" .. tostring(meta and meta.timeText))
|
||||
result(meta and meta.dexCount == 5, "launcher card shows 5 caught for the fresh import")
|
||||
result(meta and meta.badges == 2, "launcher card shows 2 badges")
|
||||
|
||||
local raw = SaveSerializer.decode(SaveData.readSlotSource(version, slotId) or "")
|
||||
local info = raw and Boot.continueInfoFromSave(raw)
|
||||
result(info and info.dexCount == 5, "CONTINUE window counts 5 caught before the first load")
|
||||
if info then
|
||||
Boot.setHasContinue(game.boot, true)
|
||||
Boot.setContinueInfo(game.boot, info)
|
||||
end
|
||||
|
||||
local function phase() return game.boot and game.boot.phase end
|
||||
local n = 0
|
||||
while phase() ~= "title" and n < 1200 do
|
||||
U.tap(game, "start")
|
||||
U.wait(30)
|
||||
n = n + 30
|
||||
end
|
||||
for _ = 1, 20 do
|
||||
if phase() ~= "title" then break end
|
||||
U.wait(60)
|
||||
U.tap(game, "start")
|
||||
U.wait(2)
|
||||
end
|
||||
for _ = 1, 600 do
|
||||
if phase() == "menu" and (game.boot.fadeT or 0) == 0 then break end
|
||||
U.wait(1)
|
||||
end
|
||||
U.wait(4)
|
||||
print("boot phase=" .. tostring(phase()) .. " game.phase=" .. tostring(game.phase))
|
||||
if result(phase() == "menu", "main menu with CONTINUE is up") then
|
||||
U.still(game, DIR .. "/2461_continue_dex_5.png")
|
||||
end
|
||||
|
||||
game:_handleBootAction({ action = "continue" })
|
||||
local Runtime = require("src.core.game3.runtime")
|
||||
local s
|
||||
for _ = 1, 900 do
|
||||
s = Runtime.getSession()
|
||||
if s and game.phase ~= "quest_log" and game.phase ~= "boot" then break end
|
||||
if game.phase == "quest_log" then U.tap(game, "b") end
|
||||
U.wait(4)
|
||||
end
|
||||
s = Runtime.getSession()
|
||||
if not result(s ~= nil, "CONTINUE reached the field") then return finish() end
|
||||
result(Dex.countCaught(s.dex, "kanto") == 5, "in-game Kanto dex count is 5 (matches the launcher)")
|
||||
finish()
|
||||
end
|
||||
@@ -31,14 +31,15 @@ local function find_edge_item(game)
|
||||
table.sort(names)
|
||||
for _, id in ipairs(names) do
|
||||
local def = maps[id]
|
||||
for dir, conn in pairs(def.connections or {}) do
|
||||
local nid = type(conn) == "table" and conn.map or conn
|
||||
for _, conn in ipairs(require("src.core.game3.connections").each(def)) do
|
||||
local dir = conn.dir
|
||||
local nid = conn.map
|
||||
local ndef = maps[nid]
|
||||
if ndef and (dir == "north" or dir == "up") then
|
||||
for _, ev in ipairs(ndef.bgEvents or {}) do
|
||||
if is_hidden(ev) and not ev.underfoot then
|
||||
local L = Map.ensureMidLayout(game, nid, ndef)
|
||||
local off = tonumber(type(conn) == "table" and conn.offset) or 0
|
||||
local off = tonumber(conn.offset) or 0
|
||||
local fromBottom = L and (L.height - 1 - ev.y)
|
||||
if fromBottom and fromBottom <= 2 then
|
||||
return id, nid, ev, ev.x + off, fromBottom
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/game3_link_union_room"
|
||||
|
||||
local UNION_ROOM = "FR_UNION_ROOM"
|
||||
local Plaza = require("src.core.game3.link.union_plaza_map")
|
||||
local UNION_ROOM = Plaza.MAP_ID
|
||||
local CENTER_2F = "FR_VIRIDIAN_CITY_POKEMON_CENTER_2F"
|
||||
-- pokefirered/data/specials.inc:5 SetCableClubWarp
|
||||
local SET_CABLE_CLUB_WARP = 0x01
|
||||
@@ -81,8 +82,10 @@ return function(game)
|
||||
result(session.dynamicWarp ~= nil and session.dynamicWarp.map == CENTER_2F,
|
||||
"SetCableClubWarp recorded the way back to the cable club counter")
|
||||
setVar(VAR_CABLE_CLUB_STATE, 6)
|
||||
Map.load(nil, game, UNION_ROOM, { x = 7, y = 11, facing = "up" })
|
||||
place(7, 11, "up")
|
||||
local slotKey = require("src.import.gba.map_catalog").slotKeyFor("FR_UNION_ROOM") or ""
|
||||
local group, num = slotKey:match("(%d+)%D+(%d+)")
|
||||
-- pokefirered/data/scripts/cable_club.inc:797
|
||||
Space.vm.adapters.warp(tonumber(group), tonumber(num), 0xFF, 7, 11, function() end, "warpspinenter")
|
||||
U.wait(120)
|
||||
|
||||
result(Space.mapId == UNION_ROOM, "the player is in the Union Room")
|
||||
@@ -90,92 +93,9 @@ return function(game)
|
||||
print("[driver] union state after entry: " .. tostring(Union.state))
|
||||
U.shot(game, DIR .. "/union_room_01_empty.png")
|
||||
|
||||
local host, peer = Game3Link.loopback({ game = game })
|
||||
host:update(0)
|
||||
peer:update(0)
|
||||
Link.attach(host)
|
||||
result(host:isReady(), "the loopback session handshook")
|
||||
|
||||
peer:send({
|
||||
type = Union.MSG.HELLO,
|
||||
name = "BLUE",
|
||||
trainerId = 0x2222,
|
||||
gender = 0,
|
||||
activity = Union.ACTIVITY.SEARCH + Union.IN_UNION_ROOM,
|
||||
})
|
||||
result(Union.relay and Union.capacity() == Plaza.CAP, "offline, the Union Room is still the 40-player plaza")
|
||||
result(Union.playerCount() == 0, "with nobody in it")
|
||||
local waited = 0
|
||||
while waited < 300 and Union.playerCount() == 0 do
|
||||
U.wait(10)
|
||||
waited = waited + 10
|
||||
end
|
||||
if not result(Union.playerCount() == 1, "the peer appeared in the Union Room player list") then
|
||||
U.shot(game, DIR .. "/union_room_99_no_peer.png")
|
||||
return finish()
|
||||
end
|
||||
local avatar = Objects.find(Union.LOCAL_IDS[1])
|
||||
result(avatar ~= nil and avatar.visible == true,
|
||||
"the slot 1 union room avatar is on the map")
|
||||
U.wait(30)
|
||||
U.shot(game, DIR .. "/union_room_02_player_spawned.png")
|
||||
|
||||
-- pokefirered/src/union_room.c:3264 UR_STATE_INTERACT_WITH_ATTENDANT
|
||||
place(3, 3, "up")
|
||||
U.wait(15)
|
||||
waited = 0
|
||||
while waited < 200 and not Screen.isOpen() do
|
||||
U.tap(game, "a")
|
||||
U.wait(20)
|
||||
waited = waited + 20
|
||||
end
|
||||
if result(Screen.isOpen(), "the Union Room attendant opened the player list") then
|
||||
result(Screen.mode == "board", "the list is the trading board view")
|
||||
result(#Screen.players == 1, "it lists the one other player in the room")
|
||||
U.wait(20)
|
||||
U.shot(game, DIR .. "/union_room_03_player_list.png")
|
||||
U.tap(game, "b")
|
||||
U.wait(30)
|
||||
end
|
||||
|
||||
-- pokefirered/data/maps/UnionRoom/scripts.inc:28 UnionRoom_EventScript_Player1
|
||||
waited = 0
|
||||
while waited < 240 and not Screen.isOpen() do
|
||||
local eo = Objects.find(Union.LOCAL_IDS[1])
|
||||
if eo and not eo.moving then
|
||||
place(tonumber(eo.cellX), tonumber(eo.cellY) + 1, "up")
|
||||
U.wait(10)
|
||||
U.tap(game, "a")
|
||||
end
|
||||
U.wait(20)
|
||||
waited = waited + 30
|
||||
end
|
||||
if not result(Screen.isOpen(), "talking to the player opened the activity chooser") then
|
||||
print("[driver] VAR_RESULT=" .. tostring(getVar(VAR_RESULT)) ..
|
||||
" state=" .. tostring(Union.state))
|
||||
U.shot(game, DIR .. "/union_room_98_no_chooser.png")
|
||||
return finish()
|
||||
end
|
||||
result(Screen.mode == "activity", "the chooser is the invite-to-activity list")
|
||||
result(#Screen.items == 4, "GREETINGS / BATTLE / CHAT / EXIT")
|
||||
U.wait(20)
|
||||
U.shot(game, DIR .. "/union_room_04_activity_chooser.png")
|
||||
|
||||
U.tap(game, "a")
|
||||
U.wait(30)
|
||||
print("[driver] after greeting: activity=" .. tostring(Union.activity)
|
||||
.. " state=" .. tostring(Union.state))
|
||||
result(not Screen.isOpen(), "GREETINGS closed the chooser")
|
||||
result(Union.activity == Union.ACTIVITY.CARD, "and sent the trainer card activity")
|
||||
U.shot(game, DIR .. "/union_room_05_after_greeting.png")
|
||||
|
||||
-- pokefirered/src/union_room.c:2937 UR_STATE_TRAINER_APPEARS_BUSY
|
||||
waited = 0
|
||||
while waited < 600 and Union.state ~= "main" do
|
||||
U.tap(game, "a")
|
||||
U.wait(20)
|
||||
waited = waited + 20
|
||||
end
|
||||
result(Union.state == "main", "an unanswered request frees the room again")
|
||||
|
||||
-- pokefirered/src/field_fadetransition.c:578 DoUnionRoomWarp
|
||||
waited = 0
|
||||
while waited < 200 and Screen.isOpen() do
|
||||
@@ -184,7 +104,7 @@ return function(game)
|
||||
waited = waited + 20
|
||||
end
|
||||
U.wait(30)
|
||||
place(7, 10, "down")
|
||||
place(12, 22, "down")
|
||||
U.wait(30)
|
||||
local Message = require("src.ui.game3.message")
|
||||
waited = 0
|
||||
@@ -194,7 +114,12 @@ return function(game)
|
||||
waited = waited + 20
|
||||
end
|
||||
U.wait(20)
|
||||
U.hold(game, "down", 30)
|
||||
for _ = 1, 20 do
|
||||
if Space.mapId ~= UNION_ROOM then break end
|
||||
U.hold(game, "down", 8)
|
||||
end
|
||||
print("[driver] exit walk: p=" .. tostring(Player.cellX) .. "," .. tostring(Player.cellY)
|
||||
.. " msg=" .. tostring(Message.isOpen()) .. " union=" .. tostring(Union.state))
|
||||
waited = 0
|
||||
while waited < 600 and Space.mapId == UNION_ROOM do
|
||||
U.wait(15)
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/game3_pad_battle_text_bug2452"
|
||||
|
||||
return function(game)
|
||||
local fails = 0
|
||||
local function result(ok, label)
|
||||
print((ok and "PASS " or "FAIL ") .. label)
|
||||
if not ok then fails = fails + 1 end
|
||||
end
|
||||
|
||||
for _ = 1, 600 do
|
||||
if game.phase == "boot" and game.boot then break end
|
||||
U.wait(1)
|
||||
end
|
||||
game:_handleBootAction({ action = "new_game", name = "RED" })
|
||||
U.wait(180)
|
||||
|
||||
local Runtime = require("src.core.game3.runtime")
|
||||
local Party = require("src.core.game3.party")
|
||||
local BattleBridge = require("src.core.game3.battle_bridge")
|
||||
local Battle = require("src.core.game3.battle")
|
||||
local Ui = require("src.core.game3.battle.ui")
|
||||
local session = Runtime.getSession()
|
||||
session.party = {}
|
||||
Party.giveMon(session, 6, 50)
|
||||
|
||||
local ok, err = BattleBridge.startWild(Runtime._mod, game, { species = 16, level = 45 }, { fade = false })
|
||||
result(ok == true, "2452_wild_battle_started " .. tostring(err or ""))
|
||||
if not ok then love.event.quit(1) return end
|
||||
|
||||
local physical = {}
|
||||
local pad = {
|
||||
isGamepad = function() return true end,
|
||||
isGamepadDown = function(_, button) return physical[button] == true end,
|
||||
getGamepadAxis = function() return 0 end,
|
||||
getName = function() return "Xbox Controller" end,
|
||||
getGUID = function() return "030000005e040000" end,
|
||||
getID = function() return 1 end,
|
||||
isConnected = function() return true end,
|
||||
}
|
||||
|
||||
local input = game.input
|
||||
local edges = 0
|
||||
local origStep = input.step
|
||||
input.step = function(self, ...)
|
||||
local r = origStep(self, ...)
|
||||
if self.pressed and self.pressed.a then edges = edges + 1 end
|
||||
return r
|
||||
end
|
||||
|
||||
local win = love.window
|
||||
local origMin, origFocus = win.isMinimized, win.hasFocus
|
||||
local minimized, focused = false, true
|
||||
win.isMinimized = function() return minimized end
|
||||
win.hasFocus = function() return focused end
|
||||
|
||||
local function padA()
|
||||
local before = edges
|
||||
physical.a = true
|
||||
love.gamepadpressed(pad, "a")
|
||||
U.wait(2)
|
||||
physical.a = false
|
||||
love.gamepadreleased(pad, "a")
|
||||
U.wait(12)
|
||||
return edges - before
|
||||
end
|
||||
|
||||
local function atCommand()
|
||||
return Battle.isActive() and Battle._phase == "command" and Ui._mode == "menu"
|
||||
end
|
||||
|
||||
local presses, doubles = 0, 0
|
||||
for _ = 1, 300 do
|
||||
if atCommand() then break end
|
||||
if Ui.dialogPending and Ui.dialogPending() then
|
||||
focused = (presses % 2 == 1)
|
||||
local n = padA()
|
||||
presses = presses + 1
|
||||
if n ~= 1 then doubles = doubles + 1 end
|
||||
else
|
||||
U.wait(1)
|
||||
end
|
||||
end
|
||||
focused = true
|
||||
result(atCommand(), "2452_battle_text_advanced_on_pad_a")
|
||||
result(presses > 0, "2452_pad_presses_used " .. presses)
|
||||
result(doubles == 0, "2452_one_edge_per_pad_press")
|
||||
U.still(game, DIR .. "/2452_command_menu_after_pad_a.png")
|
||||
|
||||
local sawText = false
|
||||
for i = 1, 600 do
|
||||
if Ui.dialogPending and Ui.dialogPending() then sawText = true break end
|
||||
if i % 20 == 1 then U.tap(game, "a") else U.wait(1) end
|
||||
end
|
||||
if sawText then
|
||||
minimized = true
|
||||
local dropped = padA()
|
||||
result(dropped == 0, "2452_minimized_pad_press_dropped")
|
||||
minimized = false
|
||||
local landed = padA()
|
||||
result(landed == 1, "2452_restored_pad_press_lands")
|
||||
else
|
||||
result(false, "2452_no_text_after_fight")
|
||||
end
|
||||
|
||||
input.step = origStep
|
||||
win.isMinimized, win.hasFocus = origMin, origFocus
|
||||
love.event.quit(fails == 0 and 0 or 1)
|
||||
end
|
||||
@@ -0,0 +1,164 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/bug2472"
|
||||
|
||||
local PEWTER = "FR_PEWTER_CITY"
|
||||
local GYM_GUIDE, GYM_X, GYM_Y = 5, 42, 20
|
||||
local MUSEUM_GUIDE, MUSEUM_X, MUSEUM_Y = 2, 33, 17
|
||||
|
||||
local failures = 0
|
||||
local function result(ok, label)
|
||||
print((ok and "PASS " or "FAIL ") .. label)
|
||||
if not ok then failures = failures + 1 end
|
||||
return ok
|
||||
end
|
||||
|
||||
local function finish()
|
||||
if failures == 0 then
|
||||
print("PASS game3_pewter_guide_bug2472")
|
||||
love.event.quit(0)
|
||||
else
|
||||
print("FAIL game3_pewter_guide_bug2472 failures=" .. failures)
|
||||
love.event.quit(1)
|
||||
end
|
||||
end
|
||||
|
||||
return function(game)
|
||||
for _ = 1, 600 do
|
||||
if game.phase == "boot" and game.boot then break end
|
||||
U.wait(1)
|
||||
end
|
||||
game:_handleBootAction({ action = "new_game", name = "RED" })
|
||||
U.wait(180)
|
||||
|
||||
local Runtime = require("src.core.game3.runtime")
|
||||
local Map = require("src.core.game3.map")
|
||||
local Space = require("src.core.game3.scripting.space")
|
||||
local Party = require("src.core.game3.party")
|
||||
local Objects = require("src.core.game3.objects")
|
||||
local Player = require("src.core.game3.player")
|
||||
local Field = require("src.core.game3.field")
|
||||
local Message = package.loaded["src.ui.game3.message"] or require("src.ui.game3.message")
|
||||
local Choice = require("src.ui.game3.choice")
|
||||
|
||||
local session = Runtime.getSession()
|
||||
if not result(session ~= nil, "new game reached the field") then return finish() end
|
||||
session.party = {}
|
||||
Party.giveMon(session, 4, 12)
|
||||
|
||||
local function vmBusy()
|
||||
return Space.vm and Space.vm.isRunning and Space.vm:isRunning()
|
||||
end
|
||||
local function msgOpen()
|
||||
return Message.isOpen and Message.isOpen()
|
||||
end
|
||||
|
||||
Map.load(nil, game, PEWTER, { x = GYM_X - 1, y = GYM_Y, facing = "right" })
|
||||
game.session.x, game.session.y, game.session.facing = GYM_X - 1, GYM_Y, "right"
|
||||
U.wait(30)
|
||||
|
||||
local bounds = Objects._bounds
|
||||
if not result(bounds and bounds.w > 0 and bounds.h > 0, "Pewter layout bounds known") then return finish() end
|
||||
local function inBounds(x, y)
|
||||
return x >= 0 and y >= 0 and x < bounds.w and y < bounds.h
|
||||
end
|
||||
|
||||
local function escort(lid, label, answerNo)
|
||||
local worst, lastPage
|
||||
local started = false
|
||||
for _ = 1, 3000 do
|
||||
local eo = Objects.find(lid)
|
||||
if eo and not eo.hidden and not inBounds(eo.cellX, eo.cellY) then
|
||||
worst = worst or string.format("guide (%d,%d)", eo.cellX, eo.cellY)
|
||||
end
|
||||
if not inBounds(Player.cellX, Player.cellY) then
|
||||
worst = worst or string.format("player (%d,%d)", Player.cellX, Player.cellY)
|
||||
end
|
||||
if vmBusy() or Field.locked then started = true end
|
||||
if started and not vmBusy() and not msgOpen() and not Field.locked then break end
|
||||
if Choice.isOpen and Choice.isOpen() then
|
||||
print("[2472] yes/no open, answering " .. (answerNo and "NO" or "YES"))
|
||||
if answerNo then Choice.cursor = 2 end
|
||||
U.tap(game, "a")
|
||||
elseif msgOpen() then
|
||||
local page = Message.currentPage and Message.currentPage()
|
||||
if page and page ~= lastPage then
|
||||
print("[2472] " .. label .. " text: " .. tostring(page):gsub("\n", " "))
|
||||
lastPage = page
|
||||
end
|
||||
if answerNo and tostring(page):find("MUSEUM?", 1, true) then
|
||||
for _ = 1, 120 do
|
||||
if Choice.isOpen() then break end
|
||||
U.wait(1)
|
||||
end
|
||||
end
|
||||
if not Choice.isOpen() then U.tap(game, "a") end
|
||||
end
|
||||
U.wait(2)
|
||||
end
|
||||
result(started, label .. ": script ran")
|
||||
result(not vmBusy() and not Field.locked, label .. ": script finished and field unlocked")
|
||||
result(worst == nil, label .. ": guide and player stayed inside the map" .. (worst and (" (left at " .. worst .. ")") or ""))
|
||||
return Player.cellX, Player.cellY
|
||||
end
|
||||
|
||||
local function atTemplate(lid, x, y, label, before, face)
|
||||
local eo = Objects.find(lid)
|
||||
result(eo ~= nil and eo.visible and not eo.hidden, label .. ": guide visible again")
|
||||
result(eo ~= nil and eo.cellX == x and eo.cellY == y,
|
||||
string.format("%s: guide back on template (%d,%d), got (%s,%s)", label, x, y,
|
||||
tostring(eo and eo.cellX), tostring(eo and eo.cellY)))
|
||||
if face then
|
||||
result(eo ~= nil and eo.facing == face, label .. ": template facing " .. face .. ", got " .. tostring(eo and eo.facing))
|
||||
end
|
||||
result(eo ~= before, label .. ": guide is a respawned object")
|
||||
return eo
|
||||
end
|
||||
|
||||
local guide = Objects.find(GYM_GUIDE)
|
||||
if not result(guide ~= nil and guide.cellX == GYM_X and guide.cellY == GYM_Y and not guide.hidden,
|
||||
"gym guide spawned at (42,20)") then return finish() end
|
||||
U.still(game, DIR .. "/2472_01_gym_guide_at_spawn.png")
|
||||
|
||||
U.tap(game, "a")
|
||||
local px1, py1 = escort(GYM_GUIDE, "gym escort 1", false)
|
||||
print(string.format("[2472] escort 1 left the player at (%d,%d)", px1, py1))
|
||||
result(px1 < 20, "gym escort 1 brought the player to the gym side of town")
|
||||
local after = Objects.find(GYM_GUIDE)
|
||||
result(not (after and not after.hidden and after.cellX == 7 and after.cellY == 19),
|
||||
"no stale gym guide left standing at the exit cell (7,19)")
|
||||
U.still(game, DIR .. "/2472_02_gym_escort_done_guide_gone.png")
|
||||
guide = atTemplate(GYM_GUIDE, GYM_X, GYM_Y, "gym guide", guide, "down")
|
||||
|
||||
Player.reset(GYM_X - 1, GYM_Y, "right")
|
||||
U.wait(20)
|
||||
U.still(game, DIR .. "/2472_03_gym_guide_back_at_spawn.png")
|
||||
U.tap(game, "a")
|
||||
local px2, py2 = escort(GYM_GUIDE, "gym escort 2", false)
|
||||
result(px2 == px1 and py2 == py1,
|
||||
string.format("gym escort 2 ends where escort 1 did (%d,%d vs %d,%d)", px2, py2, px1, py1))
|
||||
atTemplate(GYM_GUIDE, GYM_X, GYM_Y, "gym guide after replay", guide, "down")
|
||||
|
||||
Player.reset(MUSEUM_X, MUSEUM_Y + 1, "up")
|
||||
U.wait(20)
|
||||
local museum = Objects.find(MUSEUM_GUIDE)
|
||||
if not result(museum ~= nil and museum.cellX == MUSEUM_X and museum.cellY == MUSEUM_Y and not museum.hidden,
|
||||
"museum guide spawned at (33,17)") then return finish() end
|
||||
U.tap(game, "a")
|
||||
local mx1, my1 = escort(MUSEUM_GUIDE, "museum escort 1", true)
|
||||
print(string.format("[2472] museum escort 1 left the player at (%d,%d)", mx1, my1))
|
||||
result(mx1 ~= MUSEUM_X or my1 ~= MUSEUM_Y + 1, "museum escort 1 moved the player")
|
||||
museum = atTemplate(MUSEUM_GUIDE, MUSEUM_X, MUSEUM_Y, "museum guide", museum, nil)
|
||||
U.still(game, DIR .. "/2472_04_museum_escort_done.png")
|
||||
|
||||
Player.reset(MUSEUM_X, MUSEUM_Y + 1, "up")
|
||||
U.wait(20)
|
||||
local m = Objects.find(MUSEUM_GUIDE)
|
||||
if m then m.facing = "down" end
|
||||
U.tap(game, "a")
|
||||
local mx2, my2 = escort(MUSEUM_GUIDE, "museum escort 2", true)
|
||||
result(mx2 == mx1 and my2 == my1,
|
||||
string.format("museum escort 2 ends where escort 1 did (%d,%d vs %d,%d)", mx2, my2, mx1, my1))
|
||||
atTemplate(MUSEUM_GUIDE, MUSEUM_X, MUSEUM_Y, "museum guide after replay", museum, nil)
|
||||
|
||||
return finish()
|
||||
end
|
||||
@@ -0,0 +1,154 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/game3_shift_exp_2473"
|
||||
|
||||
return function(game)
|
||||
local fails = 0
|
||||
local function result(ok, label)
|
||||
print((ok and "PASS " or "FAIL ") .. label)
|
||||
if not ok then fails = fails + 1 end
|
||||
end
|
||||
|
||||
for _ = 1, 600 do
|
||||
if game.phase == "boot" and game.boot then break end
|
||||
U.wait(1)
|
||||
end
|
||||
game:_handleBootAction({ action = "new_game", name = "RED" })
|
||||
U.wait(180)
|
||||
|
||||
local Runtime = require("src.core.game3.runtime")
|
||||
local Party = require("src.core.game3.party")
|
||||
local Options = require("src.core.game3.options")
|
||||
local BattleBridge = require("src.core.game3.battle_bridge")
|
||||
local Battle = require("src.core.game3.battle")
|
||||
local Anim = require("src.core.game3.battle.anim")
|
||||
local SwitchSeq = require("src.core.game3.battle.switch_seq")
|
||||
local Ui = require("src.core.game3.battle.ui")
|
||||
local session = Runtime.getSession()
|
||||
session.party = {}
|
||||
Party.giveMon(session, 4, 30)
|
||||
Party.giveMon(session, 7, 20)
|
||||
Options.ensure(session).battleStyle = 0
|
||||
result(Options.battleStyle(session) == "shift", "battle style is shift")
|
||||
|
||||
local ok, err = BattleBridge.start(Runtime._mod, game,
|
||||
{ trainerId = 326, party = { { species = 16, level = 5 }, { species = 19, level = 5 } } },
|
||||
{ wild = false, trainerId = 326, fade = false })
|
||||
result(ok == true, "trainer battle started " .. tostring(err or ""))
|
||||
if not ok then love.event.quit(1) return end
|
||||
|
||||
local f, lastTap = 0, 0
|
||||
local function at_command()
|
||||
return Battle.isActive() and Battle._phase == "command" and Ui._mode == "menu"
|
||||
end
|
||||
|
||||
local shiftOrder, goShot, participantsAfter = nil, false, nil
|
||||
local function watch_shift()
|
||||
if not SwitchSeq.busy() or shiftOrder then return end
|
||||
local steps = SwitchSeq._steps or {}
|
||||
local order = {}
|
||||
for _, s in ipairs(steps) do
|
||||
if s.kind == "swap_data" then order[#order + 1] = s.data and s.data.side end
|
||||
end
|
||||
if #order == 2 then shiftOrder = order end
|
||||
end
|
||||
|
||||
local function pump(stop, limit)
|
||||
for _ = 1, limit or 6000 do
|
||||
f = f + 1
|
||||
watch_shift()
|
||||
if stop() then return true end
|
||||
local cur = SwitchSeq.busy() and SwitchSeq._steps and SwitchSeq._steps[SwitchSeq._i]
|
||||
local bst = Battle.getState()
|
||||
if not goShot and cur and cur.kind == "sendout_player" and bst
|
||||
and bst.enemy and (tonumber(bst.enemy.mon.hp) or 0) <= 0 then
|
||||
U.wait(10)
|
||||
U.still(game, DIR .. "/2473_mon2_out_before_foe.png")
|
||||
goShot = true
|
||||
end
|
||||
local PM = package.loaded["src.ui.game3.party_menu"]
|
||||
if PM and PM.isOpen and PM.isOpen() then
|
||||
U.wait(20)
|
||||
PM.cursor = 2
|
||||
U.tap(game, "a")
|
||||
U.wait(12)
|
||||
U.tap(game, "a")
|
||||
U.wait(12)
|
||||
elseif Ui.choiceActive and Ui.choiceActive() then
|
||||
U.wait(8)
|
||||
U.tap(game, "a")
|
||||
U.wait(8)
|
||||
elseif Ui.dialogPending and Ui.dialogPending() and f - lastTap >= 14 then
|
||||
lastTap = f
|
||||
U.tap(game, "a")
|
||||
else
|
||||
U.wait(1)
|
||||
end
|
||||
end
|
||||
return stop()
|
||||
end
|
||||
|
||||
result(pump(at_command), "reached command menu vs foe 1")
|
||||
local st = Battle.getState()
|
||||
if not st then love.event.quit(1) return end
|
||||
for _, m in ipairs(st.foeParty or {}) do
|
||||
m.moves = { 150, 0, 0, 0 }
|
||||
m.pp = { 40, 0, 0, 0 }
|
||||
m.hp = 1
|
||||
end
|
||||
st.enemy.mon.hp = 1
|
||||
local ep = Anim.present("enemy")
|
||||
if ep then ep.displayHp = 1 end
|
||||
|
||||
local function attack()
|
||||
local mon = st.player.mon
|
||||
mon.moves[1] = 129
|
||||
mon.pp = mon.pp or {}
|
||||
mon.pp[1] = 20
|
||||
Ui._pendingCommand = { kind = "move", move = 129, slot = 1, user = "player" }
|
||||
Ui._mode = "none"
|
||||
end
|
||||
|
||||
attack()
|
||||
result(pump(function() return Battle._phase == "shift_prompt" end), "shift prompt offered after foe 1 fainted")
|
||||
result(pump(function() return at_command() and st.player.partyIndex == 2 end), "shift switched to party slot 2")
|
||||
result(shiftOrder ~= nil and shiftOrder[1] == "player" and shiftOrder[2] == "enemy",
|
||||
"player swap runs before enemy send-out (" .. table.concat(shiftOrder or {}, ",") .. ")")
|
||||
result(goShot, "shot 2473_mon2_out_before_foe.png")
|
||||
participantsAfter = {}
|
||||
for pi in pairs(st.enemy.participants or {}) do participantsAfter[#participantsAfter + 1] = pi end
|
||||
table.sort(participantsAfter)
|
||||
result(#participantsAfter == 1 and participantsAfter[1] == 2,
|
||||
"foe 2 sent mons = {" .. table.concat(participantsAfter, ",") .. "}")
|
||||
|
||||
local expA = tonumber(st.playerParty[1].exp) or 0
|
||||
local expB = tonumber(st.playerParty[2].exp) or 0
|
||||
st.enemy.mon.hp = 1
|
||||
ep = Anim.present("enemy")
|
||||
if ep then ep.displayHp = 1 end
|
||||
attack()
|
||||
local expShot = false
|
||||
result(pump(function()
|
||||
if not expShot then
|
||||
for _, t in ipairs(Ui.log()) do
|
||||
if tostring(t):find("gained", 1, true) and (tonumber(st.playerParty[2].exp) or 0) > expB then
|
||||
U.wait(90)
|
||||
U.still(game, DIR .. "/2473_only_mon2_gains_exp.png")
|
||||
expShot = true
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
return not Battle.isActive()
|
||||
end, 9000), "battle ended after foe 2 fainted")
|
||||
local party = session.party
|
||||
local aNow = tonumber(party[1] and party[1].exp) or 0
|
||||
local bNow = tonumber(party[2] and party[2].exp) or 0
|
||||
print(string.format(" exp A %d -> %d, B %d -> %d", expA, aNow, expB, bNow))
|
||||
result(aNow == expA, "switched-out mon 1 gained no EXP for foe 2")
|
||||
result(bNow > expB, "mon 2 gained EXP for foe 2")
|
||||
result(expShot, "shot 2473_only_mon2_gains_exp.png")
|
||||
for _, t in ipairs(Ui.log()) do print(" log: " .. tostring(t):gsub("\n", " ")) end
|
||||
|
||||
print(string.format("shift exp 2473 driver: %d failure(s)", fails))
|
||||
love.event.quit(fails == 0 and 0 or 1)
|
||||
end
|
||||
@@ -0,0 +1,88 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/game3_six_island_water_path"
|
||||
local failures = 0
|
||||
local function result(ok, label)
|
||||
print((ok and "PASS " or "FAIL ") .. label)
|
||||
if not ok then failures = failures + 1 end
|
||||
return ok
|
||||
end
|
||||
local function finish()
|
||||
result(failures == 0, "game3_six_island_water_path")
|
||||
love.event.quit(failures == 0 and 0 or 1)
|
||||
end
|
||||
|
||||
return function(game)
|
||||
for _ = 1, 600 do
|
||||
if game.phase == "boot" and game.boot then break end
|
||||
U.wait(1)
|
||||
end
|
||||
if not result(game.phase == "boot" and game.boot ~= nil, "boot_ready") then return finish() end
|
||||
game:_handleBootAction({ action = "new_game", name = "RED" })
|
||||
U.wait(180)
|
||||
local Runtime = require("src.core.game3.runtime")
|
||||
local Map = require("src.core.game3.map")
|
||||
local Player = require("src.core.game3.player")
|
||||
require("src.core.game3.encounters").onStep = function() return nil end
|
||||
require("src.core.game3.trainer_sight").check = function() return false end
|
||||
if not result(Runtime.getSession() ~= nil, "new_game_reached_field") then return finish() end
|
||||
local WP = "FR_SIX_ISLAND_WATER_PATH"
|
||||
|
||||
local function setup(map, x, y, facing)
|
||||
Map.load(Runtime._mod, game, map, { x = x, y = y, facing = facing })
|
||||
Player.surfing, Player.elevation = false, 3
|
||||
U.wait(60)
|
||||
return result(Map.current == map and Player.cellX == x and Player.cellY == y, "setup_" .. map .. "_" .. y)
|
||||
end
|
||||
local function step(dir)
|
||||
local oldMap, x, y = Map.current, Player.cellX, Player.cellY
|
||||
for _ = 1, 24 do
|
||||
U.hold(game, dir, 1)
|
||||
if Player.moving or Map.current ~= oldMap then break end
|
||||
end
|
||||
for _ = 1, 48 do
|
||||
if not Player.moving then break end
|
||||
U.wait(1)
|
||||
end
|
||||
return not Player.moving and (Map.current ~= oldMap or Player.cellX ~= x or Player.cellY ~= y)
|
||||
end
|
||||
local function at(map, x, y, label)
|
||||
local session = Runtime.getSession()
|
||||
return result(Map.current == map and Player.cellX == x and Player.cellY == y
|
||||
and session.map == map and session.x == x and session.y == y, label)
|
||||
end
|
||||
local function drawsWest(row, sourceMap, sx, sy, label)
|
||||
local mid, _, void = Map.worldMidAt(-1, row, Map.currentDef())
|
||||
local src = game.data.maps[sourceMap].midLayout
|
||||
return result(mid ~= nil and not void and mid == src:midAt(sx, sy), label)
|
||||
end
|
||||
|
||||
if not setup("FR_SIX_ISLAND", 23, 12, "right") then return finish() end
|
||||
if not result(step("right"), "six_island_east_step") then return finish() end
|
||||
if not at(WP, 0, 52, "six_island_east_lands_on_water_path") then return finish() end
|
||||
result(#Map.neighborList == 3, "water_path_has_three_west_neighbors")
|
||||
drawsWest(52, "FR_SIX_ISLAND", 23, 12, "water_path_row52_draws_six_island")
|
||||
U.wait(30)
|
||||
result(U.still(game, DIR .. "/2456_water_path_six_island_west.png"), "shot_six_island_west")
|
||||
if not result(step("left"), "water_path_west_step_row52") then return finish() end
|
||||
at("FR_SIX_ISLAND", 23, 12, "water_path_row52_back_to_six_island")
|
||||
|
||||
if setup(WP, 0, 10, "left") then
|
||||
drawsWest(10, "FR_SIX_ISLAND_GREEN_PATH", 71, 10, "water_path_row10_draws_green_path")
|
||||
U.wait(30)
|
||||
result(U.still(game, DIR .. "/2456_water_path_green_path_west.png"), "shot_green_path_west")
|
||||
result(step("left"), "water_path_west_step_row10")
|
||||
at("FR_SIX_ISLAND_GREEN_PATH", 71, 10, "water_path_row10_to_green_path")
|
||||
end
|
||||
|
||||
if setup(WP, 0, 90, "left") then
|
||||
drawsWest(90, "FR_SIX_ISLAND_RUIN_VALLEY", 47, 10, "water_path_row90_draws_ruin_valley")
|
||||
result(step("left"), "water_path_west_step_row90")
|
||||
at("FR_SIX_ISLAND_RUIN_VALLEY", 47, 10, "water_path_row90_to_ruin_valley")
|
||||
end
|
||||
|
||||
if setup(WP, 0, 48, "left") then
|
||||
result(step("left"), "water_path_west_step_row48")
|
||||
at("FR_SIX_ISLAND", 23, 8, "water_path_row48_to_six_island")
|
||||
end
|
||||
finish()
|
||||
end
|
||||
@@ -0,0 +1,107 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local DIR = os.getenv("SHOT_DIR") or os.getenv("POKEPORT_SHOT_DIR") or ".bazinga/union40/shots/map"
|
||||
|
||||
local CENTER_2F = "FR_VIRIDIAN_CITY_POKEMON_CENTER_2F"
|
||||
-- pokefirered/data/specials.inc:12
|
||||
local SET_CABLE_CLUB_WARP = 0x01
|
||||
-- pokefirered/include/constants/flags.h:1375
|
||||
local FLAG_SYS_POKEDEX_GET = 0x829
|
||||
|
||||
return function(game)
|
||||
local function fail(msg)
|
||||
print("FAIL game3_union_plaza_map: " .. msg)
|
||||
love.event.quit(1)
|
||||
coroutine.yield()
|
||||
end
|
||||
for _ = 1, 900 do
|
||||
if game.phase == "boot" and game.boot then break end
|
||||
U.wait(1)
|
||||
end
|
||||
game:_handleBootAction({ action = "new_game", name = "RED" })
|
||||
U.wait(240)
|
||||
if game.phase ~= "field" then return fail("never reached the field") end
|
||||
|
||||
local Map = require("src.core.game3.map")
|
||||
local Space = require("src.core.game3.scripting.space")
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
local Player = require("src.core.game3.player")
|
||||
local Natives = require("src.core.game3.scripting.natives")
|
||||
local MapCatalog = require("src.import.gba.map_catalog")
|
||||
local Plaza = require("src.core.game3.link.union_plaza_map")
|
||||
|
||||
local function ctx() return Space.vm and Space.vm.ctx end
|
||||
local function place(x, y, facing)
|
||||
Player.cellX, Player.cellY = x, y
|
||||
Player.px, Player.py = x * 16, y * 16
|
||||
Player.targetX, Player.targetY = x, y
|
||||
Player.facing = facing
|
||||
if game.session then
|
||||
game.session.x, game.session.y, game.session.facing = x, y, facing
|
||||
end
|
||||
end
|
||||
|
||||
local def = game.data and game.data.maps and game.data.maps[Plaza.MAP_ID]
|
||||
if not def then return fail("hydrate did not register " .. Plaza.MAP_ID) end
|
||||
|
||||
-- pokefirered/data/scripts/cable_club.inc:743
|
||||
Flags.setFlag(Space.store, ctx(), FLAG_SYS_POKEDEX_GET, true)
|
||||
Map.load(nil, game, CENTER_2F, { x = 5, y = 2, facing = "up" })
|
||||
place(5, 2, "up")
|
||||
U.wait(30)
|
||||
place(5, 1, "up")
|
||||
U.wait(10)
|
||||
Natives.special(ctx(), SET_CABLE_CLUB_WARP, Space.vm and Space.vm.adapters)
|
||||
local dw = game.session and game.session.dynamicWarp
|
||||
if not (dw and dw.map == CENTER_2F) then return fail("SetCableClubWarp did not record the way back") end
|
||||
|
||||
local slot = MapCatalog.slotKeyFor("FR_UNION_ROOM") or ""
|
||||
local group, num = slot:match("(%d+)%D+(%d+)")
|
||||
group, num = tonumber(group), tonumber(num)
|
||||
if not (group and num) then return fail("no group/num for FR_UNION_ROOM (" .. slot .. ")") end
|
||||
|
||||
local arrived = false
|
||||
-- pokefirered/data/scripts/cable_club.inc:797
|
||||
Space.vm.adapters.warp(group, num, 0xFF, 7, 11, function() arrived = true end, "warpspinenter")
|
||||
for _ = 1, 600 do
|
||||
if arrived and Map.current == Plaza.MAP_ID then break end
|
||||
U.wait(1)
|
||||
end
|
||||
if Map.current ~= Plaza.MAP_ID then
|
||||
return fail("warp to the Union Room landed on " .. tostring(Map.current))
|
||||
end
|
||||
local ex, ey = Plaza.entry()
|
||||
if Player.cellX ~= ex or Player.cellY ~= ey then
|
||||
return fail(("spawned at %s,%s want %d,%d"):format(tostring(Player.cellX), tostring(Player.cellY), ex, ey))
|
||||
end
|
||||
U.log("entered", Map.current, Player.cellX, Player.cellY)
|
||||
U.wait(120)
|
||||
U.still(game, DIR .. "/plaza_entrance.png")
|
||||
|
||||
place(12, 18, "up")
|
||||
U.wait(30)
|
||||
U.still(game, DIR .. "/plaza_center.png")
|
||||
|
||||
place(22, 3, "up")
|
||||
U.wait(30)
|
||||
U.still(game, DIR .. "/plaza_far_corner.png")
|
||||
|
||||
place(3, 3, "up")
|
||||
U.wait(30)
|
||||
U.still(game, DIR .. "/plaza_counter.png")
|
||||
|
||||
place(12, 22, "down")
|
||||
U.wait(30)
|
||||
U.hold(game, "down", 40)
|
||||
for _ = 1, 300 do
|
||||
if Map.current == CENTER_2F then break end
|
||||
U.wait(1)
|
||||
end
|
||||
if Map.current ~= CENTER_2F then
|
||||
return fail("exit landed on " .. tostring(Map.current) .. " want " .. CENTER_2F)
|
||||
end
|
||||
U.wait(60)
|
||||
U.still(game, DIR .. "/plaza_exit_center_2f.png")
|
||||
U.log("exit landed", Map.current, Player.cellX, Player.cellY)
|
||||
print("PASS game3_union_plaza_map")
|
||||
love.event.quit(0)
|
||||
end
|
||||
@@ -77,7 +77,7 @@ return function(game)
|
||||
U.tap(game, "a")
|
||||
U.wait(20)
|
||||
end
|
||||
list = waitForList(60)
|
||||
list = waitForList(300)
|
||||
if list then break end
|
||||
closeAll()
|
||||
end
|
||||
|
||||
@@ -22,6 +22,10 @@ return function(game)
|
||||
|
||||
game.speedOverride = 4
|
||||
U.wait(2)
|
||||
if game:logicSpeed() ~= 4 then
|
||||
error(("bug1952: the overworld did not take the 4X override (%s)")
|
||||
:format(tostring(game:logicSpeed())))
|
||||
end
|
||||
U.log("gen2 speed override", 4, "sfx rate", Sound.rate())
|
||||
if Sound.rate() ~= 1 then
|
||||
error(("bug1952: Game2:update pitched SFX off GAME SPEED (rate %s at 4X)")
|
||||
@@ -71,6 +75,12 @@ return function(game)
|
||||
:format(held, dur))
|
||||
end
|
||||
|
||||
game.linkNet = { closed = false }
|
||||
if game:logicSpeed() ~= 1 then
|
||||
error(("speed lock: an open gen 2 link ran at %sX under a 4X override")
|
||||
:format(tostring(game:logicSpeed())))
|
||||
end
|
||||
game.linkNet = nil
|
||||
game.speedOverride = nil
|
||||
U.wait(2)
|
||||
if Sound.rate() ~= 1 then
|
||||
@@ -80,5 +90,5 @@ return function(game)
|
||||
|
||||
U.shot(game, DIR .. "/bug1952_gold_after.png")
|
||||
U.log("PASS the gen 2 jingle kept natural pitch under GAME SPEED")
|
||||
love.event.quit()
|
||||
love.event.quit(0)
|
||||
end
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
-- home/text_script.asm:141-150
|
||||
-- engine/events/pokemart.asm:7-19
|
||||
-- engine/events/pokemart.asm:131-133
|
||||
-- engine/events/pokemart.asm:199-206
|
||||
-- engine/events/pokemart.asm:220-222
|
||||
-- engine/menus/text_box.asm:168-176
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or os.getenv("SHOT_DIR") or "/tmp/shots"
|
||||
local Menu = require("src.ui.Menu")
|
||||
local ListMenu = require("src.ui.ListMenu")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
|
||||
local failures = 0
|
||||
local function check(label, ok)
|
||||
if not ok then failures = failures + 1 end
|
||||
U.log(ok and "PASS" or "FAIL", label)
|
||||
return ok
|
||||
end
|
||||
local function top() return game.stack:top() end
|
||||
local function states() return game.stack.states or {} end
|
||||
local function waitFor(pred, frames)
|
||||
for _ = 1, frames or 600 do
|
||||
if pred() then return true end
|
||||
U.wait(1)
|
||||
end
|
||||
return pred()
|
||||
end
|
||||
local function typing(first)
|
||||
local t = top()
|
||||
if getmetatable(t) ~= TextBox or not t.stay or t.done then return false end
|
||||
local page = t.pages and t.pages[1] or {}
|
||||
if page[1] ~= first then return false end
|
||||
local line = t.shown and t.shown[1]
|
||||
return line ~= nil and #line >= 3 and #line < #page[1]
|
||||
end
|
||||
local function onStack(cls)
|
||||
for _, s in ipairs(states()) do
|
||||
if getmetatable(s) == cls then return s end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
local function finish()
|
||||
U.log(failures == 0 and "PASS mart_clerk_typing_bug2384"
|
||||
or ("FAIL mart_clerk_typing_bug2384 (" .. failures .. " failures)"))
|
||||
love.event.quit(failures == 0 and 0 or 1)
|
||||
U.wait(600)
|
||||
end
|
||||
|
||||
local ok, err = pcall(function()
|
||||
game.save.money = 3000
|
||||
game.save.options = game.save.options or {}
|
||||
game.save.options.textSpeed = 3
|
||||
U.teleport(game, "PEWTER_MART", 2, 5, "left")
|
||||
U.wait(20)
|
||||
local ow = top()
|
||||
|
||||
U.tap(game, "a")
|
||||
if not check("the greeting types in an empty box",
|
||||
waitFor(function() return typing("Hi there!") end, 120)) then return end
|
||||
local menu = onStack(Menu)
|
||||
check("BUY/SELL/QUIT is on the stack but hidden", menu ~= nil and menu.hidden == true)
|
||||
U.still(game, DIR .. "/2384_greeting_typing.png")
|
||||
|
||||
if not check("BUY/SELL/QUIT comes up after the greeting",
|
||||
waitFor(function() return top() == menu end, 300)) then return end
|
||||
check("the menu is drawn now", menu.hidden == false)
|
||||
check("filled cursor on BUY", menu.index == 1 and menu.hollowIndex == nil)
|
||||
U.wait(4)
|
||||
U.still(game, DIR .. "/2384_menu_filled_cursor.png")
|
||||
|
||||
U.tap(game, "a")
|
||||
if not check("Take your time. types before the list",
|
||||
waitFor(function() return typing("Take your time.") end, 60)) then return end
|
||||
check("BUY goes hollow", menu.hollowIndex == 1)
|
||||
check("no list while the lead-in types", onStack(ListMenu) == nil)
|
||||
U.still(game, DIR .. "/2384_take_your_time_typing.png")
|
||||
|
||||
if not check("the buy list opens after the lead-in",
|
||||
waitFor(function() return getmetatable(top()) == ListMenu end, 300)) then return end
|
||||
U.wait(6)
|
||||
check("BUY stays hollow while the list is up", menu.hollowIndex == 1)
|
||||
U.still(game, DIR .. "/2384_buy_list_hollow_buy.png")
|
||||
|
||||
U.tap(game, "b")
|
||||
if not check("anything-else types after the list closes",
|
||||
waitFor(function() return typing("Is there anything") end, 60)) then return end
|
||||
check("BUY still hollow while it types", menu.hollowIndex == 1)
|
||||
U.still(game, DIR .. "/2384_anything_else_typing.png")
|
||||
|
||||
if not check("back on BUY/SELL/QUIT",
|
||||
waitFor(function() return top() == menu end, 300)) then return end
|
||||
check("cursor reset to BUY and filled", menu.index == 1 and menu.hollowIndex == nil)
|
||||
U.wait(4)
|
||||
U.still(game, DIR .. "/2384_back_on_buy.png")
|
||||
|
||||
U.tap(game, "down")
|
||||
U.wait(4)
|
||||
U.tap(game, "down")
|
||||
U.wait(4)
|
||||
check("cursor on QUIT", menu.index == 3)
|
||||
U.tap(game, "a")
|
||||
local bye
|
||||
if not check("Thank you! goes up", waitFor(function()
|
||||
local t = top()
|
||||
if getmetatable(t) == TextBox and not t.stay then bye = t return true end
|
||||
return false
|
||||
end, 30)) then return end
|
||||
local s = states()
|
||||
check("the mart menu stays under Thank you!", s[#s - 1] == menu)
|
||||
check("QUIT is hollow under Thank you!", menu.hollowIndex == 3)
|
||||
waitFor(function() return bye.done end, 300)
|
||||
U.wait(4)
|
||||
U.still(game, DIR .. "/2384_thank_you.png")
|
||||
|
||||
U.tap(game, "a")
|
||||
check("the mart closes back to the overworld",
|
||||
waitFor(function() return top() == ow end, 60))
|
||||
check("no mart screen left", onStack(Menu) == nil and onStack(ListMenu) == nil)
|
||||
end)
|
||||
if not ok then
|
||||
failures = failures + 1
|
||||
U.log("FAIL driver error:", tostring(err))
|
||||
end
|
||||
finish()
|
||||
end
|
||||
@@ -58,7 +58,7 @@ return function(game)
|
||||
local menu = top()
|
||||
|
||||
U.tap(game, "a")
|
||||
if not check("BUY opens the list", waitFor(function() return topIs(ListMenu) end, 60)) then return end
|
||||
if not check("BUY opens the list", waitFor(function() return topIs(ListMenu) end, 300)) then return end
|
||||
local list = top()
|
||||
local itemId = list.items[list.index].value
|
||||
local def = game.data.items[itemId]
|
||||
@@ -109,12 +109,12 @@ return function(game)
|
||||
check("NO buys nothing", game.save.money == 3000)
|
||||
|
||||
U.tap(game, "b")
|
||||
if not check("B returns to BUY/SELL/QUIT", waitFor(function() return top() == menu end, 30)) then return end
|
||||
if not check("B returns to BUY/SELL/QUIT", waitFor(function() return top() == menu end, 300)) then return end
|
||||
U.wait(4)
|
||||
U.tap(game, "down")
|
||||
U.wait(4)
|
||||
U.tap(game, "a")
|
||||
if not check("SELL opens the sell list", waitFor(function() return topIs(ListMenu) end, 60)) then return end
|
||||
if not check("SELL opens the sell list", waitFor(function() return topIs(ListMenu) end, 300)) then return end
|
||||
local sellList = top()
|
||||
for _ = 1, 20 do
|
||||
local row = sellList.items[sellList.index]
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local FixedStep = require("src.core.FixedStep")
|
||||
|
||||
local SHOT_DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/pokeport-shots"
|
||||
|
||||
return function(game)
|
||||
local failed = false
|
||||
local function expect(cond, label)
|
||||
print((cond and "PASS " or "FAIL ") .. label)
|
||||
if not cond then failed = true end
|
||||
end
|
||||
|
||||
local pos
|
||||
local gen = "gen" .. tostring(require("src.core.GameVersion").generation())
|
||||
|
||||
if gen == "gen1" then
|
||||
U.newGame(game)
|
||||
U.teleport(game, "PALLET_TOWN", 8, 6, "down")
|
||||
pos = function()
|
||||
local p = game.overworld and game.overworld.player
|
||||
return p and p.cellX, p and p.cellY
|
||||
end
|
||||
elseif gen == "gen2" then
|
||||
U.wait(60)
|
||||
if game.world and game.world.map then
|
||||
game.world:warpToMapId("NEW_BARK_TOWN", 5, 6, "down")
|
||||
U.wait(120)
|
||||
end
|
||||
pos = function()
|
||||
local p = game.world and game.world.player
|
||||
return p and p.cellX, p and p.cellY
|
||||
end
|
||||
else
|
||||
for _ = 1, 900 do
|
||||
if game.phase == "boot" and game.boot then break end
|
||||
U.wait(1)
|
||||
end
|
||||
game:_handleBootAction({ action = "new_game", name = "RED" })
|
||||
U.wait(240)
|
||||
pos = function()
|
||||
local s = game.session
|
||||
return s and s.x, s and s.y
|
||||
end
|
||||
end
|
||||
local x0, y0 = pos()
|
||||
expect(x0 ~= nil, gen .. "_2471_overworld_reached")
|
||||
if x0 == nil then love.event.quit(1) return end
|
||||
|
||||
local counted = 0
|
||||
local orig = FixedStep.callback
|
||||
FixedStep.callback = function(dt)
|
||||
counted = counted + 1
|
||||
return orig(dt)
|
||||
end
|
||||
|
||||
local function stepsPerUpdate(speed, updates)
|
||||
game.speedOverride = speed
|
||||
counted = 0
|
||||
for _ = 1, updates do game:update(1 / 60) end
|
||||
game.speedOverride = 1
|
||||
return counted / updates
|
||||
end
|
||||
|
||||
local function walk(speed, dir, updates)
|
||||
local ax, ay = pos()
|
||||
game.speedOverride = speed
|
||||
for _ = 1, updates do
|
||||
table.insert(game.input.pressQueue, dir)
|
||||
game.input.state[dir] = true
|
||||
game:update(1 / 60)
|
||||
end
|
||||
game.input.state[dir] = false
|
||||
game.speedOverride = 1
|
||||
for _ = 1, 60 do game:update(1 / 60) end
|
||||
local bx, by = pos()
|
||||
return math.abs((bx or ax) - ax) + math.abs((by or ay) - ay)
|
||||
end
|
||||
|
||||
local s10 = stepsPerUpdate(10, 30)
|
||||
local s200 = stepsPerUpdate(200, 30)
|
||||
print(("[2471] %s logic steps per rendered frame: 10X=%.1f 200X=%.1f"):format(gen, s10, s200))
|
||||
expect(math.abs(s10 - 10) < 0.5, gen .. "_2471_10x_runs_10_steps_a_frame")
|
||||
expect(s200 > 60, gen .. "_2471_200x_not_capped_at_15")
|
||||
|
||||
local best, bestDir = -1, nil
|
||||
for _, dir in ipairs({ "left", "right", "down", "up" }) do
|
||||
local sx, sy = pos()
|
||||
local cells10 = walk(10, dir, 1)
|
||||
local back = ({ left = "right", right = "left", up = "down", down = "up" })[dir]
|
||||
for _ = 1, 120 do
|
||||
local cx, cy = pos()
|
||||
if cx == sx and cy == sy then break end
|
||||
walk(1, back, 1)
|
||||
end
|
||||
local cells200 = walk(200, dir, 1)
|
||||
print(("[2471] %s %s: 10X moved %d cells, 200X moved %d cells in one frame")
|
||||
:format(gen, dir, cells10, cells200))
|
||||
if cells200 - cells10 > best then best, bestDir = cells200 - cells10, dir end
|
||||
if cells200 >= 3 and cells200 > cells10 * 2 then break end
|
||||
for _ = 1, 200 do
|
||||
local cx, cy = pos()
|
||||
if cx == sx and cy == sy then break end
|
||||
walk(4, back, 1)
|
||||
end
|
||||
end
|
||||
expect(best >= 2, gen .. "_2471_200x_moves_more_cells_than_10x_" .. tostring(bestDir))
|
||||
FixedStep.callback = orig
|
||||
|
||||
U.still(game, SHOT_DIR .. "/2471_" .. gen .. "_after_200x_frame.png")
|
||||
love.event.quit(failed and 1 or 0)
|
||||
end
|
||||
@@ -22,6 +22,9 @@ return function(game)
|
||||
for _, r in ipairs(results) do U.log(r.ok and "PASS" or "FAIL", r.label) end
|
||||
end
|
||||
local function idleForever()
|
||||
local failed = false
|
||||
for _, r in ipairs(results) do if not r.ok then failed = true end end
|
||||
love.event.quit(failed and 1 or 0)
|
||||
while true do coroutine.yield() end
|
||||
end
|
||||
|
||||
@@ -37,7 +40,10 @@ return function(game)
|
||||
-- follower and every count below would read as the bug.
|
||||
game.save.flags = game.save.flags or {}
|
||||
game.save.flags.EVENT_GOT_STARTER = true
|
||||
game.save.flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB = true
|
||||
game.save.pikachuInBall = false
|
||||
game.save.party = { Pokemon.new(game.data, "PIKACHU", 20) }
|
||||
require("src.battle.BattleState").stampOT(game.save, game.save.party[1])
|
||||
game.save.onBike = false
|
||||
|
||||
local frames = 0
|
||||
@@ -261,8 +267,12 @@ return function(game)
|
||||
|
||||
U.tap(game, "a")
|
||||
U.wait(1)
|
||||
for _ = 1, 600 do
|
||||
if ow.emote and ow.emote.pikaPic then break end
|
||||
U.wait(1)
|
||||
end
|
||||
local emote = ow.emote
|
||||
if not check("the A press raised the framed pikapic", emote ~= nil) then
|
||||
if not check("the A press raised the framed pikapic", emote ~= nil and emote.pikaPic ~= nil) then
|
||||
report()
|
||||
idleForever()
|
||||
end
|
||||
@@ -275,12 +285,12 @@ return function(game)
|
||||
local lifted, grounded, liftShot = 0, 0, nil
|
||||
for _ = 1, 60 do
|
||||
if not ow.emote then break end
|
||||
local lift = PikachuFollower.picLift(ow.emote)
|
||||
if lift > 0 then
|
||||
local path = PikachuFollower.picFrame(ow.emote)
|
||||
if path and path:find("gfx_e6646", 1, true) then
|
||||
lifted = lifted + 1
|
||||
if not liftShot then
|
||||
liftShot = SHOT_DIR .. "/bug424_pikapic_lift.png"
|
||||
if U.shot(game, liftShot) then U.log("captured", liftShot)
|
||||
liftShot = SHOT_DIR .. "/bug424_pikapic_pose_e6646.png"
|
||||
if U.still(game, liftShot) then U.log("captured", liftShot)
|
||||
else liftShot = nil end
|
||||
end
|
||||
else
|
||||
@@ -288,9 +298,9 @@ return function(game)
|
||||
end
|
||||
step(1)
|
||||
end
|
||||
check("the pic sits on the box floor for part of the beat", grounded > 0)
|
||||
check("and rises off it for another part", lifted > 0)
|
||||
U.log("of the first 60 frames the pic was up for", lifted, "and down for",
|
||||
check("the base pic shows for part of the beat", grounded > 0)
|
||||
check("and PikaAnimTilemap_34's GFX_e6646 pose for another part", lifted > 0)
|
||||
U.log("of the first 60 frames the pose was up for", lifted, "and the base for",
|
||||
grounded)
|
||||
|
||||
local leftWhenCut = ow.emote and ow.emote.frames or 0
|
||||
@@ -309,7 +319,7 @@ return function(game)
|
||||
U.log("Hold Down: Pikachu should walk up to the ledge lip and WAIT there,")
|
||||
U.log("then clear both cells in one motion on your next step, at walking")
|
||||
U.log("speed. Let go and count the first glance: about a second, not half.")
|
||||
U.log("Face it, press A: the framed pic hops inside the box for the whole")
|
||||
U.log("Face it, press A: the framed pic swaps to the hearts pose and back for the whole")
|
||||
U.log("beat, and A or B cuts the beat off early.")
|
||||
|
||||
idleForever()
|
||||
|
||||
@@ -163,15 +163,36 @@ return function(game)
|
||||
check("2347_talk1_pikapic_25", pic:find("pikapic_25", 1, true) ~= nil)
|
||||
local emote = ow.emote
|
||||
check("2347_talk1_bolt_spec", emote and emote.boltAt == 45)
|
||||
local bgpSeq, shadeSeq, frames = {}, {}, 0
|
||||
local bgpSeq, poseSeq, doneSeq, frames = {}, {}, {}, 0
|
||||
local prebolt = 0
|
||||
while ow.emote == emote and frames < 900 do
|
||||
local function poseName(e)
|
||||
local path, lift = PikachuFollower.picFrame(e)
|
||||
if (lift or 0) ~= 0 then return "lifted" end
|
||||
return path and path:match("([^/]+)%.png$") or false
|
||||
end
|
||||
local PROBE = (os.getenv("TMPDIR") or "/tmp/") .. "2347_probe.png"
|
||||
local function drawnShade()
|
||||
U.still(game, PROBE)
|
||||
os.remove(PROBE)
|
||||
return PaletteFX.shadeMap()
|
||||
end
|
||||
local darkDuringPre, whiteDrawn, litTail
|
||||
while ow.emote == emote and frames < 1500 do
|
||||
U.wait(1)
|
||||
frames = frames + 1
|
||||
if ow.emote ~= emote then break end
|
||||
bgpSeq[#bgpSeq + 1] = emote.bgp or false
|
||||
shadeSeq[#bgpSeq] = PaletteFX.shadeMap() or false
|
||||
poseSeq[#bgpSeq] = poseName(emote)
|
||||
doneSeq[#bgpSeq] = emote.boltDone and true or false
|
||||
if not emote.bgp then prebolt = prebolt + 1 end
|
||||
local k = (emote.boltT or 0) - emote.boltAt - 2
|
||||
if darkDuringPre == nil and not emote.bgp and frames > 10 then
|
||||
darkDuringPre = isDarkMap(drawnShade())
|
||||
elseif whiteDrawn == nil and emote.bgp == 0xC0 then
|
||||
whiteDrawn = isBoltMap(drawnShade())
|
||||
elseif litTail == nil and k >= 81 and not emote.boltDone then
|
||||
litTail = drawnShade() == nil
|
||||
end
|
||||
end
|
||||
local strobeOk, seen = true, 0
|
||||
local first
|
||||
@@ -190,28 +211,35 @@ return function(game)
|
||||
end
|
||||
U.log("prebolt frames", prebolt, "strobe start", tostring(first), "total", frames)
|
||||
check("2347_talk1_strobe_alternates_4f", first ~= nil and strobeOk and seen == 80)
|
||||
local whiteDrawn, darkDuringPre = false, false
|
||||
for i = 1, #bgpSeq do
|
||||
if isBoltMap(shadeSeq[i]) then whiteDrawn = true end
|
||||
if first and i < first and isDarkMap(shadeSeq[i]) then darkDuringPre = true end
|
||||
end
|
||||
check("2347_talk1_prebolt_dark", darkDuringPre)
|
||||
check("2347_talk1_strobe_white_drawn", whiteDrawn)
|
||||
local litTail = first and #bgpSeq > first + 82
|
||||
if litTail then
|
||||
for i = first + 82, #bgpSeq do
|
||||
if shadeSeq[i] then litTail = false end
|
||||
check("2347_talk1_prebolt_dark", darkDuringPre == true)
|
||||
check("2347_talk1_strobe_white_drawn", whiteDrawn == true)
|
||||
check("2347_talk1_lit_tail_while_box_up", litTail == true)
|
||||
local boltPose, sawE7863, blank = first ~= nil, false, 0
|
||||
for i = 1, #poseSeq do
|
||||
if poseSeq[i] == "gfx_e7863" then sawE7863 = true end
|
||||
if first and i >= first then
|
||||
if poseSeq[i] == false then blank = blank + 1
|
||||
elseif blank > 0 or poseSeq[i] ~= "gfx_e79f3" then boltPose = false end
|
||||
end
|
||||
end
|
||||
check("2347_talk1_lit_tail_while_box_up", litTail and true or false)
|
||||
U.log("pose at strobe start", tostring(first and poseSeq[first]), "blank close frames", blank)
|
||||
check("2347_talk1_e7863_before_bolt", sawE7863)
|
||||
check("2347_talk1_bolt_pose", boltPose)
|
||||
local closeSeq = {}
|
||||
for i = 1, #poseSeq do
|
||||
if doneSeq[i] then closeSeq[#closeSeq + 1] = tostring(poseSeq[i]) end
|
||||
end
|
||||
U.log("close frames", table.concat(closeSeq, ","))
|
||||
check("2347_talk1_empty_box_at_close",
|
||||
table.concat(closeSeq, ",") == "gfx_e79f3,gfx_e79f3,gfx_e79f3,false,false,false")
|
||||
check("2347_talk1_thunderbolt_sound", moveSounds[1] == "Battle_2F")
|
||||
check("2347_talk1_music_muted", ducks == 1)
|
||||
U.wait(6)
|
||||
check("2347_talk1_dark_after_close", isDarkMap(PaletteFX.shadeMap()))
|
||||
check("2347_talk1_dark_after_close", ow.emote == nil and isDarkMap(drawnShade()))
|
||||
check("2347_modifier_survives_talk", game.save.pikachuEmotionModifier == 5)
|
||||
|
||||
if not check("2347_talk2_emote", startTalk("2347_talk2")) then quit() end
|
||||
U.shot(game, SHOT_DIR .. "/2347_02_bolt_bubble.png")
|
||||
U.still(game, SHOT_DIR .. "/2347_02_bolt_bubble.png")
|
||||
for _ = 1, 200 do
|
||||
if ow.emote and ow.emote.pikaPic then break end
|
||||
U.wait(1)
|
||||
@@ -219,28 +247,36 @@ return function(game)
|
||||
emote = ow.emote
|
||||
check("2347_talk2_replays_bolt", emote and emote.boltAt ~= nil)
|
||||
U.wait(20)
|
||||
U.shot(game, SHOT_DIR .. "/2347_03_pikapic_prebolt.png")
|
||||
U.still(game, SHOT_DIR .. "/2347_03_pikapic_prebolt.png")
|
||||
for _ = 1, 200 do
|
||||
if emote.bgp == 0xC0 then break end
|
||||
U.wait(1)
|
||||
end
|
||||
U.shot(game, SHOT_DIR .. "/2347_04_strobe_white.png")
|
||||
local function drawn()
|
||||
return tostring(ow.pikaPicDrawn):match("([^/]+)%.png$")
|
||||
end
|
||||
U.still(game, SHOT_DIR .. "/2347_04_strobe_white.png")
|
||||
check("2347_talk2_c0_draws_e79f3", emote.bgp == 0xC0 and drawn() == "gfx_e79f3")
|
||||
for _ = 1, 200 do
|
||||
if emote.bgp == 0xE4 then break end
|
||||
U.wait(1)
|
||||
end
|
||||
U.shot(game, SHOT_DIR .. "/2347_05_strobe_lit.png")
|
||||
U.still(game, SHOT_DIR .. "/2347_05_strobe_lit.png")
|
||||
check("2347_talk2_e4_draws_e79f3", emote.bgp == 0xE4 and drawn() == "gfx_e79f3")
|
||||
for _ = 1, 400 do
|
||||
if emote.boltDone or ow.emote ~= emote then break end
|
||||
if (emote.boltT or 0) - emote.boltAt > 2 + 80 or ow.emote ~= emote then break end
|
||||
U.wait(1)
|
||||
end
|
||||
if ow.emote == emote then U.shot(game, SHOT_DIR .. "/2347_06_lit_tail.png") end
|
||||
for _ = 1, 200 do
|
||||
if ow.emote == emote and not emote.boltDone then
|
||||
U.still(game, SHOT_DIR .. "/2347_06_lit_tail.png")
|
||||
check("2347_talk2_tail_draws_e79f3", drawn() == "gfx_e79f3")
|
||||
end
|
||||
for _ = 1, 1500 do
|
||||
if ow.emote ~= emote then break end
|
||||
U.wait(1)
|
||||
end
|
||||
U.wait(10)
|
||||
U.shot(game, SHOT_DIR .. "/2347_07_after_close_dark.png")
|
||||
U.still(game, SHOT_DIR .. "/2347_07_after_close_dark.png")
|
||||
|
||||
for _ = 1, 3 do
|
||||
stepAndBack()
|
||||
@@ -256,7 +292,7 @@ return function(game)
|
||||
U.wait(1)
|
||||
end
|
||||
U.wait(20)
|
||||
U.shot(game, SHOT_DIR .. "/2347_08_after_steps_mood_talk.png")
|
||||
U.still(game, SHOT_DIR .. "/2347_08_after_steps_mood_talk.png")
|
||||
check("2347_talk3_no_bolt", ow.emote and ow.emote.pikaPic and not ow.emote.boltAt
|
||||
and true or false)
|
||||
for _ = 1, 600 do
|
||||
@@ -278,22 +314,126 @@ return function(game)
|
||||
U.wait(1)
|
||||
end
|
||||
emote = ow.emote
|
||||
local veiled = false
|
||||
for _ = 1, 400 do
|
||||
local tiles = ow.map.renderer
|
||||
local darkAtlas = tiles.image
|
||||
local function atlasFor(byte)
|
||||
local imgs = tiles.bgpImages or {}
|
||||
return imgs[byte] and tiles.image == imgs[byte] and tiles.curBgp == byte
|
||||
and tiles.baseImage == darkAtlas
|
||||
end
|
||||
local shotC0, shotE4 = false, false
|
||||
local litOnE4, c0Atlas, veilOnC0 = true, true, false
|
||||
for _ = 1, 1500 do
|
||||
if ow.emote ~= emote then break end
|
||||
U.wait(1)
|
||||
local v = game.renderer and game.renderer.screenVeil
|
||||
if emote.bgp == 0xC0 and v and v[1] == 1 then
|
||||
if not veiled then U.shot(game, SHOT_DIR .. "/2347_09_baked_strobe_white.png") end
|
||||
veiled = true
|
||||
if ow.emote ~= emote then break end
|
||||
if emote.bgp == 0xC0 and not shotC0 then
|
||||
shotC0 = true
|
||||
U.still(game, SHOT_DIR .. "/2347_09_baked_strobe_white.png")
|
||||
local v = game.renderer and game.renderer.screenVeil
|
||||
if v then veilOnC0 = true end
|
||||
if not atlasFor(0xC0) then c0Atlas = false end
|
||||
check("2347_baked_c0_draws_e79f3", drawn() == "gfx_e79f3")
|
||||
elseif emote.bgp == 0xE4 and shotC0 and not shotE4 then
|
||||
shotE4 = true
|
||||
U.still(game, SHOT_DIR .. "/2347_10_baked_strobe_lit.png")
|
||||
if not atlasFor(0xE4) then litOnE4 = false end
|
||||
check("2347_baked_e4_draws_e79f3", drawn() == "gfx_e79f3")
|
||||
end
|
||||
end
|
||||
check("2347_baked_white_veil", veiled)
|
||||
check("2347_baked_strobe_seen", shotC0 and shotE4)
|
||||
check("2347_baked_lit_atlas", shotE4 and litOnE4)
|
||||
check("2347_baked_c0_atlas", shotC0 and c0Atlas)
|
||||
check("2347_baked_no_white_veil", shotC0 and not veilOnC0)
|
||||
U.wait(10)
|
||||
U.still(game, SHOT_DIR .. "/2347_11_baked_after_close_dark.png")
|
||||
check("2347_baked_dark_after_close", ow.emote == nil and tiles.image == darkAtlas
|
||||
and tiles.curBgp == nil and tiles.baseImage == nil and ow.dark == true)
|
||||
end
|
||||
else
|
||||
U.log("SKIP 2347_baked (no gbc atlas in this cache)")
|
||||
end
|
||||
|
||||
if PaletteFX.mode == "redpp" then
|
||||
U.teleport(game, "PALLET_TOWN", 11, 11, "down")
|
||||
U.wait(20)
|
||||
ow = game.overworld
|
||||
npc = follower()
|
||||
local tiles = ow.map.renderer
|
||||
local renderers = { tiles }
|
||||
for _, nb in ipairs(ow.neighbors or {}) do
|
||||
if nb.map.renderer and nb.map.renderer.gbcAtlas then
|
||||
renderers[#renderers + 1] = nb.map.renderer
|
||||
end
|
||||
end
|
||||
check("2347_outdoor_lit", ow.dark ~= true and ow:bakedWorldColors())
|
||||
check("2347_outdoor_neighbors_baked", #renderers >= 2)
|
||||
local animated = 0
|
||||
for _, a in ipairs(tiles.anims or {}) do
|
||||
if a.spec then animated = animated + 1 end
|
||||
end
|
||||
check("2347_outdoor_water_flower_anims", animated >= 2)
|
||||
local function allOn(byte)
|
||||
for _, r in ipairs(renderers) do
|
||||
local img = r.bgpImages and r.bgpImages[byte]
|
||||
if not (img and r.image == img and r.curBgp == byte) then return false end
|
||||
for _, a in ipairs(r.anims or {}) do
|
||||
if a.spec and not (a.bgpTextures and a.textures == a.bgpTextures[byte]) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
local function allBase()
|
||||
for _, r in ipairs(renderers) do
|
||||
if r.curBgp ~= nil or r.baseImage ~= nil then return false end
|
||||
for _, a in ipairs(r.anims or {}) do
|
||||
if a.baseTextures then return false end
|
||||
for _, t in pairs(a.bgpTextures or {}) do
|
||||
if a.textures == t then return false end
|
||||
end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
PikachuFollower.onMoveLearned(game.save, pika, "THUNDERBOLT")
|
||||
if npc then PikachuFollower.talk(game, ow, npc, function() end) end
|
||||
for _ = 1, 200 do
|
||||
if ow.emote and ow.emote.pikaPic then break end
|
||||
U.wait(1)
|
||||
end
|
||||
emote = ow.emote
|
||||
if check("2347_outdoor_emote", emote ~= nil) then
|
||||
local onC0, onE4, sawC0, sawE4 = nil, nil, 0, 0
|
||||
local last
|
||||
for _ = 1, 2000 do
|
||||
if ow.emote ~= emote then break end
|
||||
U.wait(1)
|
||||
if ow.emote ~= emote then break end
|
||||
local bgp = emote.bgp
|
||||
if bgp ~= last and (bgp == 0xC0 and sawC0 < 3 or bgp == 0xE4 and sawE4 < 3) then
|
||||
if bgp == 0xC0 then
|
||||
sawC0 = sawC0 + 1
|
||||
U.still(game, SHOT_DIR .. "/2347_12_outdoor_strobe_white_" .. sawC0 .. ".png")
|
||||
onC0 = (onC0 ~= false) and allOn(0xC0)
|
||||
else
|
||||
sawE4 = sawE4 + 1
|
||||
U.still(game, SHOT_DIR .. "/2347_13_outdoor_strobe_lit_" .. sawE4 .. ".png")
|
||||
onE4 = (onE4 ~= false) and allBase()
|
||||
end
|
||||
end
|
||||
last = bgp
|
||||
end
|
||||
check("2347_outdoor_strobe_seen", sawC0 == 3 and sawE4 == 3)
|
||||
check("2347_outdoor_c0_every_renderer_and_anim", onC0 == true)
|
||||
check("2347_outdoor_e4_lit_base", onE4 == true)
|
||||
U.wait(6)
|
||||
U.still(game, SHOT_DIR .. "/2347_14_outdoor_after_close.png")
|
||||
check("2347_outdoor_base_after_close", ow.emote == nil and allBase())
|
||||
end
|
||||
end
|
||||
|
||||
Sound.playMove, Music.duckForFanfare = realPlayMove, realDuck
|
||||
quit()
|
||||
end
|
||||
|
||||
+15
-8
@@ -2403,13 +2403,14 @@ local function sellItem(id)
|
||||
press("a")
|
||||
U.wait(10)
|
||||
end
|
||||
pressUntil(isList, "a", 20)
|
||||
sold = ((G.save.inventory or {})[id] or 0) == 0
|
||||
pressUntil(function() return isList() or isMenu() end, "a", 20)
|
||||
sold =((G.save.inventory or {})[id] or 0) == 0
|
||||
end
|
||||
-- back out of the sell list to the BUY/SELL menu
|
||||
for _ = 1, 10 do
|
||||
for _ = 1, 60 do
|
||||
if isMenu() then break end
|
||||
press("b")
|
||||
local t = top()
|
||||
if not (t and t.stay) then press("b") end
|
||||
U.wait(6)
|
||||
end
|
||||
return sold
|
||||
@@ -2526,10 +2527,16 @@ end
|
||||
-- shopping spree that empties the wallet and then sits on the "not enough
|
||||
-- money" footer forever. B is the only safe key once a shop screen is up.
|
||||
local function closeShop()
|
||||
for _ = 1, 40 do
|
||||
if not (isList() or isQty() or isChoice() or isMenu()) then return true end
|
||||
press("b")
|
||||
U.wait(6)
|
||||
for _ = 1, 80 do
|
||||
local t = top()
|
||||
if t and t.stay then
|
||||
U.wait(6)
|
||||
elseif not (isList() or isQty() or isChoice() or isMenu()) then
|
||||
return true
|
||||
else
|
||||
press("b")
|
||||
U.wait(6)
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
@@ -78,9 +78,9 @@ return function(game)
|
||||
check("route buyItem paid 600", game.save.money == money0 - 600)
|
||||
check("route buyItem leaves the buy list on top", R.isList())
|
||||
|
||||
for _ = 1, 10 do
|
||||
for _ = 1, 60 do
|
||||
if R.isMenu() then break end
|
||||
U.tap(game, "b")
|
||||
if R.isList() then U.tap(game, "b") end
|
||||
U.wait(6)
|
||||
end
|
||||
if not check("back on the BUY/SELL menu", R.isMenu()) then return end
|
||||
|
||||
@@ -39,7 +39,11 @@ return function(game)
|
||||
U.wait(20)
|
||||
check(tag .. " menu", mash("a", function() return topIs(Menu) end))
|
||||
U.tap(game, "a") -- BUY
|
||||
U.wait(8)
|
||||
for _ = 1, 300 do
|
||||
if topIs(ListMenu) then break end
|
||||
U.wait(1)
|
||||
end
|
||||
U.wait(4)
|
||||
check(tag .. " list", topIs(ListMenu))
|
||||
U.shot(game, ("%s/%s_0_list.png"):format(DIR, tag))
|
||||
U.tap(game, "a") -- first item
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local DIR = os.getenv("SHOT_DIR") or os.getenv("POKEPORT_SHOT_DIR") or "/tmp/shots"
|
||||
|
||||
local UNION_ROOM = "FR_UNION_ROOM"
|
||||
local CENTER_2F = "FR_VIRIDIAN_CITY_POKEMON_CENTER_2F"
|
||||
-- pokefirered/data/specials.inc:12
|
||||
local SET_CABLE_CLUB_WARP = 0x01
|
||||
-- pokefirered/include/constants/vars.h:163
|
||||
local VAR_CABLE_CLUB_STATE = 0x406F
|
||||
-- pokefirered/include/constants/flags.h:1375
|
||||
local FLAG_SYS_POKEDEX_GET = 0x829
|
||||
|
||||
local function key(k)
|
||||
love.keypressed(k, k, false)
|
||||
love.keyreleased(k, k)
|
||||
end
|
||||
|
||||
return function(game)
|
||||
local function fail(msg)
|
||||
error("FAIL speed lock: " .. msg, 0)
|
||||
end
|
||||
for _ = 1, 900 do
|
||||
if game.phase == "boot" and game.boot then break end
|
||||
U.wait(1)
|
||||
end
|
||||
game:_handleBootAction({ action = "new_game", name = "RED" })
|
||||
U.wait(240)
|
||||
if game.phase ~= "field" then fail("never reached the field") end
|
||||
|
||||
local Runtime = require("src.core.game3.runtime")
|
||||
local Party = require("src.core.game3.party")
|
||||
local BattleBridge = require("src.core.game3.battle_bridge")
|
||||
local Battle = require("src.core.game3.battle")
|
||||
local Map = require("src.core.game3.map")
|
||||
local Space = require("src.core.game3.scripting.space")
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
local Player = require("src.core.game3.player")
|
||||
local Union = require("src.core.game3.link.union_room")
|
||||
local o = game.options
|
||||
|
||||
game.speedOverride = nil
|
||||
o.speedOverworld, o.speedBattle, o.speedMenu = 10, 200, 1
|
||||
U.wait(2)
|
||||
if game:logicSpeed() ~= 10 then
|
||||
fail(("field at OVERWORLD SPEED 10 reads %s"):format(tostring(game:logicSpeed())))
|
||||
end
|
||||
|
||||
local session = Runtime.getSession()
|
||||
session.party = {}
|
||||
Party.giveMon(session, 6, 50)
|
||||
local ok, err = BattleBridge.startWild(Runtime._mod, game, { species = 16, level = 2 }, { fade = false })
|
||||
if not ok then fail("wild battle did not start " .. tostring(err)) end
|
||||
|
||||
local checked, shot = 0, false
|
||||
local Ui = require("src.core.game3.battle.ui")
|
||||
for f = 1, 20000 do
|
||||
if not Battle.isActive() then break end
|
||||
if game:logicSpeed() ~= 1 then
|
||||
fail(("battle ran at %sX with speedBattle 200"):format(tostring(game:logicSpeed())))
|
||||
end
|
||||
checked = checked + 1
|
||||
if checked == 30 then
|
||||
key("1")
|
||||
game:gamepadaxis(nil, "triggerright", 1)
|
||||
game:gamepadaxis(nil, "triggerright", 0)
|
||||
if o.speedOverworld ~= 10 or o.speedBattle ~= 200 or o.speedMenu ~= 1 then
|
||||
fail(("speed presses in battle changed options (%s/%s/%s)"):format(
|
||||
tostring(o.speedOverworld), tostring(o.speedBattle), tostring(o.speedMenu)))
|
||||
end
|
||||
game.speedOverride = 200
|
||||
if game:logicSpeed() ~= 1 then fail("speedOverride 200 defeated the battle lock") end
|
||||
game.speedOverride = nil
|
||||
if game.touchSkinHotkey then game:touchSkinHotkey("fast_forward_hold", true) end
|
||||
if game:logicSpeed() ~= 1 then fail("skin FF hold defeated the battle lock") end
|
||||
if game.touchSkinHotkey then game:touchSkinHotkey("fast_forward_hold", false) end
|
||||
end
|
||||
if checked == 200 and not shot then
|
||||
shot = U.shot(game, DIR .. "/firered_battle_locked.png")
|
||||
end
|
||||
local st = Battle.getState and Battle.getState()
|
||||
if st and Battle._phase == "command" and Ui._mode == "menu" and not Ui._pendingCommand then
|
||||
local mon = st.player.mon
|
||||
mon.moves[1] = 33
|
||||
mon.pp = mon.pp or {}
|
||||
mon.pp[1] = 10
|
||||
st.enemy.mon.hp = 1
|
||||
Ui._pendingCommand = { kind = "move", move = 33, slot = 1, user = "player" }
|
||||
Ui._mode = "none"
|
||||
U.wait(1)
|
||||
elseif f % 14 == 0 then
|
||||
U.tap(game, "a")
|
||||
else
|
||||
U.wait(1)
|
||||
end
|
||||
end
|
||||
if Battle.isActive() then
|
||||
fail(("battle never ended (phase %s, ui %s)"):format(tostring(Battle._phase), tostring(Ui._mode)))
|
||||
end
|
||||
U.log("battle frames checked at 1X", checked)
|
||||
U.wait(120)
|
||||
if game:logicSpeed() ~= 10 then
|
||||
fail(("after the battle logic speed is %s, want 10"):format(tostring(game:logicSpeed())))
|
||||
end
|
||||
|
||||
local function ctx() return Space.vm and Space.vm.ctx end
|
||||
local function place(x, y, facing)
|
||||
Player.cellX, Player.cellY = x, y
|
||||
Player.px, Player.py = x * 16, y * 16
|
||||
Player.targetX, Player.targetY = x, y
|
||||
Player.facing = facing
|
||||
if game.session then
|
||||
game.session.x, game.session.y, game.session.facing = x, y, facing
|
||||
end
|
||||
end
|
||||
-- pokefirered/data/scripts/cable_club.inc:743
|
||||
Flags.setFlag(Space.store, ctx(), FLAG_SYS_POKEDEX_GET, true)
|
||||
Map.load(nil, game, CENTER_2F, { x = 5, y = 2, facing = "up" })
|
||||
place(5, 2, "up")
|
||||
U.wait(60)
|
||||
if game:logicSpeed() ~= 10 then
|
||||
fail(("the cable club counter room reads %s, want 10"):format(tostring(game:logicSpeed())))
|
||||
end
|
||||
place(5, 1, "up")
|
||||
U.wait(10)
|
||||
local Natives = require("src.core.game3.scripting.natives")
|
||||
Natives.special(ctx(), SET_CABLE_CLUB_WARP, Space.vm and Space.vm.adapters)
|
||||
Flags.setVar(Space.store, ctx(), VAR_CABLE_CLUB_STATE, 6)
|
||||
Map.load(nil, game, UNION_ROOM, { x = 7, y = 11, facing = "up" })
|
||||
place(7, 11, "up")
|
||||
local seen = 0
|
||||
for _ = 1, 120 do
|
||||
U.wait(1)
|
||||
if game:logicSpeed() ~= 1 then
|
||||
fail(("the Union Room ran at %sX with OVERWORLD SPEED 10"):format(tostring(game:logicSpeed())))
|
||||
end
|
||||
seen = seen + 1
|
||||
end
|
||||
if not Union.isActive() then U.log("note: union session not active, locked by map") end
|
||||
key("1")
|
||||
game:gamepadaxis(nil, "triggerright", 1)
|
||||
game:gamepadaxis(nil, "triggerright", 0)
|
||||
if o.speedOverworld ~= 10 then
|
||||
fail(("speed presses in the Union Room moved OVERWORLD SPEED to %s"):format(tostring(o.speedOverworld)))
|
||||
end
|
||||
U.shot(game, DIR .. "/firered_union_locked.png")
|
||||
U.log("union room frames checked at 1X", seen, "union state", tostring(Union.state))
|
||||
U.log("PASS firered battle and Union Room locked to 1X, OVERWORLD SPEED 10 back after the battle")
|
||||
love.event.quit(0)
|
||||
end
|
||||
@@ -0,0 +1,102 @@
|
||||
local U = require("tests.drivers.util")
|
||||
|
||||
local Mon = require("src.battle.gen2.Mon")
|
||||
|
||||
return function(game)
|
||||
local DIR = os.getenv("SHOT_DIR") or os.getenv("POKEPORT_SHOT_DIR") or "/tmp/shots"
|
||||
local function fail(msg)
|
||||
error("FAIL speed lock: " .. msg, 0)
|
||||
end
|
||||
local function tap(button, frames)
|
||||
game.input.pressQueue[#game.input.pressQueue + 1] = button
|
||||
game.input.state[button] = true
|
||||
U.wait(2)
|
||||
game.input.state[button] = false
|
||||
U.wait(frames or 6)
|
||||
end
|
||||
|
||||
U.wait(45)
|
||||
local world = game.world
|
||||
if not (world and world.map) then return fail("gold world did not boot") end
|
||||
|
||||
game.speedOverride = nil
|
||||
game.options.speed = 10
|
||||
U.wait(2)
|
||||
if game:logicSpeed() ~= 10 then
|
||||
return fail(("overworld at GAME SPEED 10 reads %s"):format(tostring(game:logicSpeed())))
|
||||
end
|
||||
U.log("overworld logic speed", game:logicSpeed())
|
||||
|
||||
local player = Mon.new(game.data, "CYNDAQUIL", 10)
|
||||
game.save.party = { player }
|
||||
local wild = Mon.new(game.data, "PIDGEY", 2)
|
||||
if not world:startBattle({ wild = wild }) then return fail("startBattle failed") end
|
||||
|
||||
local battle
|
||||
for _ = 1, 600 do
|
||||
local top = game.stack:top()
|
||||
if top and top.battle then battle = top break end
|
||||
U.wait(1)
|
||||
end
|
||||
if not battle then return fail("battle screen never came up") end
|
||||
|
||||
local function inBattle()
|
||||
for _, st in ipairs(game.stack.states) do
|
||||
if st == battle then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local checked, shot = 0, false
|
||||
for _ = 1, 2000 do
|
||||
if not inBattle() then break end
|
||||
if game:logicSpeed() ~= 1 then
|
||||
return fail(("battle ran at %sX with GAME SPEED 10"):format(tostring(game:logicSpeed())))
|
||||
end
|
||||
checked = checked + 1
|
||||
if checked == 30 then
|
||||
game:hotkey("1")
|
||||
game:_cycleSpeed(1)
|
||||
game:_cycleSpeed(-1)
|
||||
if game.options.speed ~= 10 then
|
||||
return fail(("speed presses in battle moved GAME SPEED to %s"):format(tostring(game.options.speed)))
|
||||
end
|
||||
game.speedOverride = 200
|
||||
if game:logicSpeed() ~= 1 then
|
||||
return fail("speedOverride 200 defeated the battle lock")
|
||||
end
|
||||
game.speedOverride = nil
|
||||
end
|
||||
if battle.phase == "menu" and not shot then
|
||||
shot = U.shot(game, DIR .. "/gold_battle_locked.png")
|
||||
end
|
||||
if battle.battle and battle.battle.over then
|
||||
tap("a", 3)
|
||||
elseif battle.phase == "menu" then
|
||||
tap("a", 4)
|
||||
tap("a", 4)
|
||||
else
|
||||
tap("a", 3)
|
||||
end
|
||||
end
|
||||
if inBattle() then
|
||||
local top = game.stack:top()
|
||||
return fail(("battle never ended (phase %s, over %s, top %s)"):format(
|
||||
tostring(battle.phase), tostring(battle.battle and battle.battle.over),
|
||||
tostring(top and (top.screenId or top.name) or top)))
|
||||
end
|
||||
U.log("battle frames checked at 1X", checked)
|
||||
U.wait(30)
|
||||
if game:logicSpeed() ~= 10 then
|
||||
return fail(("after the battle logic speed is %s, want 10"):format(tostring(game:logicSpeed())))
|
||||
end
|
||||
game.linkNet = { closed = false }
|
||||
if game:logicSpeed() ~= 1 then return fail("an open linkNet did not lock 1X") end
|
||||
game.linkNet = nil
|
||||
game.linkSession = true
|
||||
if game:logicSpeed() ~= 1 then return fail("a linkSession did not lock 1X") end
|
||||
game.linkSession = nil
|
||||
U.shot(game, DIR .. "/gold_after_battle.png")
|
||||
U.log("PASS gold battle and link locked to 1X, GAME SPEED 10 back after")
|
||||
love.event.quit(0)
|
||||
end
|
||||
@@ -0,0 +1,87 @@
|
||||
return function(game)
|
||||
local U = dofile("tests/drivers/util.lua")
|
||||
local DIR = os.getenv("SHOT_DIR") or os.getenv("POKEPORT_SHOT_DIR") or "/tmp/shots"
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
local function fail(msg)
|
||||
error("FAIL speed lock: " .. msg, 0)
|
||||
end
|
||||
local function key(k)
|
||||
love.keypressed(k, k, false)
|
||||
love.keyreleased(k, k)
|
||||
end
|
||||
|
||||
local squirtle = Pokemon.new(game.data, "SQUIRTLE", 30, function(_, b) return b end)
|
||||
game.save.party = { squirtle }
|
||||
U.teleport(game, "ROUTE_1", 5, 5, "down")
|
||||
local ow = game.overworld
|
||||
|
||||
local o = game.save.options
|
||||
game.speedOverride = nil
|
||||
o.speedOverworld, o.speedBattle, o.speedMenu = 10, 200, 1
|
||||
U.wait(2)
|
||||
if game:logicSpeed() ~= 10 then
|
||||
fail(("overworld at OVERWORLD SPEED 10 reads %s"):format(tostring(game:logicSpeed())))
|
||||
end
|
||||
U.log("overworld logic speed", game:logicSpeed())
|
||||
|
||||
local battle = BattleState.newWild(game, "RATTATA", 2)
|
||||
battle.onFinish = function() end
|
||||
battle.enemy.mon.hp = 1
|
||||
ow:pushBattle(battle)
|
||||
|
||||
local function inBattle()
|
||||
for _, st in ipairs(game.stack.states) do
|
||||
if st == battle then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
for _ = 1, 600 do
|
||||
if inBattle() then break end
|
||||
U.wait(1)
|
||||
end
|
||||
if not inBattle() then fail("the battle never reached the stack") end
|
||||
|
||||
local checked, shot = 0, false
|
||||
for _ = 1, 2400 do
|
||||
if not inBattle() then break end
|
||||
if game:logicSpeed() ~= 1 then
|
||||
fail(("battle ran at %sX with speedBattle 200"):format(tostring(game:logicSpeed())))
|
||||
end
|
||||
checked = checked + 1
|
||||
if checked == 20 then
|
||||
key("1")
|
||||
game:gamepadaxis(nil, "triggerright", 1)
|
||||
game:gamepadaxis(nil, "triggerright", 0)
|
||||
game:touchSkinHotkey("fast_forward_toggle", true)
|
||||
if o.speedOverworld ~= 10 or o.speedBattle ~= 200 or o.speedMenu ~= 1 then
|
||||
fail(("speed presses in battle changed options (%s/%s/%s)"):format(
|
||||
tostring(o.speedOverworld), tostring(o.speedBattle), tostring(o.speedMenu)))
|
||||
end
|
||||
game:touchSkinHotkey("fast_forward_hold", true)
|
||||
if game:logicSpeed() ~= 1 then fail("skin FF hold defeated the battle lock") end
|
||||
game:touchSkinHotkey("fast_forward_hold", false)
|
||||
game.speedOverride = 200
|
||||
if game:logicSpeed() ~= 1 then fail("speedOverride 200 defeated the battle lock") end
|
||||
game.speedOverride = nil
|
||||
end
|
||||
if battle.phase == "menu" and not shot then
|
||||
shot = U.shot(game, DIR .. "/red_battle_locked.png")
|
||||
end
|
||||
U.tap(game, "a")
|
||||
U.wait(2)
|
||||
end
|
||||
if inBattle() then fail("battle never ended") end
|
||||
U.log("battle frames checked at 1X", checked)
|
||||
U.wait(10)
|
||||
if game:logicSpeed() ~= 10 then
|
||||
fail(("after the battle logic speed is %s, want 10"):format(tostring(game:logicSpeed())))
|
||||
end
|
||||
game.linkSession = true
|
||||
if game:logicSpeed() ~= 1 then fail("a link session did not lock 1X") end
|
||||
game.linkSession = nil
|
||||
U.shot(game, DIR .. "/red_after_battle.png")
|
||||
U.log("PASS red battle and link locked to 1X, OVERWORLD SPEED 10 back after")
|
||||
love.event.quit(0)
|
||||
end
|
||||
@@ -85,24 +85,16 @@ if check(one.mapSign ~= nil, "the sign comes up at 1X") then
|
||||
end
|
||||
|
||||
-- ../pokecrystal/engine/overworld/events.asm:177-191
|
||||
FixedStep.clock = function() return 0 end
|
||||
for _, speed in ipairs({ 1, 4, 10, 20, 200 }) do
|
||||
local w = raise(gameAt(speed))
|
||||
local frame, steps, maxPerFrame = renderedFramesToClear(w, speed, 2000)
|
||||
eq(frame, 122, ("%dX: the sign clears on rendered frame 122"):format(speed))
|
||||
if speed <= 10 then
|
||||
eq(steps, 122 * speed,
|
||||
("%dX: under the catch-up cap the logic ran %d steps"):format(speed, speed * 122))
|
||||
else
|
||||
check(maxPerFrame <= 15 and steps < 122 * speed,
|
||||
("%dX: FixedStep pinned at %d steps a frame, %d in all (nominal would be %d)")
|
||||
:format(speed, maxPerFrame, steps, 122 * speed))
|
||||
end
|
||||
eq(steps, 122 * speed,
|
||||
("%dX: the logic ran %d steps"):format(speed, speed * 122))
|
||||
eq(maxPerFrame, speed, ("%dX: %d steps a rendered frame"):format(speed, speed))
|
||||
end
|
||||
|
||||
local capped = raise(gameAt(200))
|
||||
local _, _, perFrame = renderedFramesToClear(capped, 200, 2000)
|
||||
check(perFrame >= 14 and perFrame <= 15,
|
||||
("200X: the capped regime really ran ~15 steps a frame (%d)"):format(perFrame))
|
||||
FixedStep.clock = nil
|
||||
|
||||
local hidden = raise(gameAt(1))
|
||||
for _ = 1, 3 do Sign.frame(hidden) end
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user