diff --git a/docs/modding.md b/docs/modding.md index a90aacaf..f1c72926 100644 --- a/docs/modding.md +++ b/docs/modding.md @@ -1251,13 +1251,13 @@ 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), but only on frames where the speed is not locked. Every -battle (wild, trainer, link, online, tournament, spectate), link play and a +(once per frame), but only on frames where the speed is not locked. Link +play (link, online, tournament and spectate battles included) 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 +so it cannot fast-forward a link battle. On every other frame vanilla +behavior resolves the per-category GAME SPEED option (`speedOverworld`, +`speedBattle` 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 diff --git a/docs/new-features.md b/docs/new-features.md index 24e913fa..d9c660a4 100644 --- a/docs/new-features.md +++ b/docs/new-features.md @@ -27,7 +27,7 @@ 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 +* **Fast-forward locks to 1X in link play**, every link or online 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 diff --git a/src/core/Game.lua b/src/core/Game.lua index a4230709..237042c7 100644 --- a/src/core/Game.lua +++ b/src/core/Game.lua @@ -387,9 +387,6 @@ function Game:speedLocked() if Game.isFixedSpeedInStack and Game.isFixedSpeedInStack(self.stack) then return true, "minigame" end - if Game.speedCategoryInStack(self.stack) == "battle" then - return true, "battle" - end return false end diff --git a/src/core/Game2.lua b/src/core/Game2.lua index c084e56a..111fdf23 100644 --- a/src/core/Game2.lua +++ b/src/core/Game2.lua @@ -1291,7 +1291,6 @@ function Game2:speedLocked() 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 diff --git a/src/core/Game3.lua b/src/core/Game3.lua index 7fae71b0..a5229f0b 100644 --- a/src/core/Game3.lua +++ b/src/core/Game3.lua @@ -595,11 +595,8 @@ 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" + local st = Battle.getState and Battle.getState() + if type(st) == "table" and st.link then return true, "link" end end local Union = package.loaded["src.core.game3.link.union_room"] if type(Union) == "table" and Union.isActive and Union.isActive() then diff --git a/src/import/LauncherSettings.lua b/src/import/LauncherSettings.lua index 5d84989b..b0e4f338 100644 --- a/src/import/LauncherSettings.lua +++ b/src/import/LauncherSettings.lua @@ -350,6 +350,12 @@ local function coreRows(opts, hooks) 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) diff --git a/src/render/BattleTransition.lua b/src/render/BattleTransition.lua index 012a146d..aa214344 100644 --- a/src/render/BattleTransition.lua +++ b/src/render/BattleTransition.lua @@ -14,7 +14,6 @@ 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 diff --git a/src/ui/OptionsMenu.lua b/src/ui/OptionsMenu.lua index cdc9f444..6cbd98d9 100644 --- a/src/ui/OptionsMenu.lua +++ b/src/ui/OptionsMenu.lua @@ -539,6 +539,15 @@ 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) @@ -714,7 +723,7 @@ local GROUPS = { members = { "uiLayout", "videoMode", "orientation", "faithfulRes", "screenPos", "fpsCap", "vsync", "logicClock" } }, { id = "group.speed", label = "SPEED", - members = { "textSpeed", "speedOverworld", "speedMenu" } }, + members = { "textSpeed", "speedOverworld", "speedBattle", "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 diff --git a/src/ui/game3/option_rows.lua b/src/ui/game3/option_rows.lua index cd2d0e56..baac784c 100644 --- a/src/ui/game3/option_rows.lua +++ b/src/ui/game3/option_rows.lua @@ -70,6 +70,7 @@ 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({ @@ -396,7 +397,7 @@ end Rows.GROUPS = { { id = "group.speed", label = "SPEED", - members = { "textSpeed", "speedOverworld", "speedMenu" } }, + members = { "textSpeed", "speedOverworld", "speedBattle", "speedMenu" } }, { id = "group.video", label = "VIDEO", members = { "uiLayout", "videoMode", "orientation", "faithfulRes", "screenPos", "fpsCap", "vsync", "logicClock" } }, diff --git a/src/ui/gen2/BattleTransition.lua b/src/ui/gen2/BattleTransition.lua index 9f9411f3..1a778fb3 100644 --- a/src/ui/gen2/BattleTransition.lua +++ b/src/ui/gen2/BattleTransition.lua @@ -35,7 +35,6 @@ 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 diff --git a/tests/drivers/battle_cry_speed_bug2087_test.lua b/tests/drivers/battle_cry_speed_bug2087_test.lua index 8cfbb8f2..01cc5be6 100644 --- a/tests/drivers/battle_cry_speed_bug2087_test.lua +++ b/tests/drivers/battle_cry_speed_bug2087_test.lua @@ -25,19 +25,6 @@ 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()))) @@ -71,11 +58,8 @@ 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 @@ -103,7 +87,6 @@ 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 @@ -122,12 +105,11 @@ 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 (%.3fs of %.3fs)"):format( + fail = ("bug2087: %s cut short at 4X (%.3fs of %.3fs)"):format( g.label, g.held, g.dur) end end if fail then error(fail) end - 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") + U.log("PASS every battle sfx gate held for its real length at 4X") love.event.quit(0) end diff --git a/tests/drivers/battle_fanfare_speed_bug1952_test.lua b/tests/drivers/battle_fanfare_speed_bug1952_test.lua index e62b0ed3..b2131e67 100644 --- a/tests/drivers/battle_fanfare_speed_bug1952_test.lua +++ b/tests/drivers/battle_fanfare_speed_bug1952_test.lua @@ -25,26 +25,12 @@ 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) @@ -63,7 +49,6 @@ 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() @@ -94,25 +79,13 @@ 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 battle cut the fanfare short (%.3fs of %.3fs)") + error(("bug2087: the 4X 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 - 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") + U.log("PASS the fanfare kept natural pitch and played to completion at 4X") love.event.quit(0) end diff --git a/tests/drivers/speed_lock_firered.lua b/tests/drivers/speed_lock_firered.lua index b7f408ce..f2a65e71 100644 --- a/tests/drivers/speed_lock_firered.lua +++ b/tests/drivers/speed_lock_firered.lua @@ -39,7 +39,7 @@ return function(game) local o = game.options game.speedOverride = nil - o.speedOverworld, o.speedBattle, o.speedMenu = 10, 200, 1 + o.speedOverworld, o.speedBattle, o.speedMenu = 10, 4, 1 U.wait(2) if game:logicSpeed() ~= 10 then fail(("field at OVERWORLD SPEED 10 reads %s"):format(tostring(game:logicSpeed()))) @@ -55,27 +55,27 @@ return function(game) 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()))) + if game:logicSpeed() ~= 4 then + fail(("local battle ran at %sX with speedBattle 4"):format(tostring(game:logicSpeed()))) end checked = checked + 1 if checked == 30 then + local st = Battle.getState() + st.link = true 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( + if o.speedOverworld ~= 10 or o.speedBattle ~= 4 or o.speedMenu ~= 1 then + fail(("speed presses in a link 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 + if game:logicSpeed() ~= 1 then fail("a link battle was not locked to 1X") 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 + st.link = false end if checked == 200 and not shot then - shot = U.shot(game, DIR .. "/firered_battle_locked.png") + shot = U.shot(game, DIR .. "/firered_battle_speed.png") end local st = Battle.getState and Battle.getState() if st and Battle._phase == "command" and Ui._mode == "menu" and not Ui._pendingCommand then @@ -96,7 +96,7 @@ return function(game) 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.log("battle frames checked at 4X", checked) U.wait(120) if game:logicSpeed() ~= 10 then fail(("after the battle logic speed is %s, want 10"):format(tostring(game:logicSpeed()))) @@ -144,6 +144,6 @@ return function(game) 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") + U.log("PASS firered local battle at BATTLE SPEED 4, link battle and Union Room locked to 1X") love.event.quit(0) end diff --git a/tests/drivers/speed_lock_gold.lua b/tests/drivers/speed_lock_gold.lua index 3636e3a7..d4610808 100644 --- a/tests/drivers/speed_lock_gold.lua +++ b/tests/drivers/speed_lock_gold.lua @@ -50,25 +50,25 @@ return function(game) 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()))) + if game:logicSpeed() ~= 10 then + return fail(("local 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.linkNet = { closed = false } game.speedOverride = 200 if game:logicSpeed() ~= 1 then - return fail("speedOverride 200 defeated the battle lock") + return fail("a link battle was not locked to 1X") + end + game:_cycleSpeed(1) + if game.options.speed ~= 10 then + return fail(("speed press in a link battle moved GAME SPEED to %s"):format(tostring(game.options.speed))) end game.speedOverride = nil + game.linkNet = nil end if battle.phase == "menu" and not shot then - shot = U.shot(game, DIR .. "/gold_battle_locked.png") + shot = U.shot(game, DIR .. "/gold_battle_speed.png") end if battle.battle and battle.battle.over then tap("a", 3) @@ -85,7 +85,7 @@ return function(game) 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.log("battle frames checked at 10X", checked) U.wait(30) if game:logicSpeed() ~= 10 then return fail(("after the battle logic speed is %s, want 10"):format(tostring(game:logicSpeed()))) @@ -97,6 +97,6 @@ return function(game) 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") + U.log("PASS gold local battle at GAME SPEED 10, link locked to 1X") love.event.quit(0) end diff --git a/tests/drivers/speed_lock_red.lua b/tests/drivers/speed_lock_red.lua index 29fe7e28..8074b22b 100644 --- a/tests/drivers/speed_lock_red.lua +++ b/tests/drivers/speed_lock_red.lua @@ -18,7 +18,7 @@ return function(game) local o = game.save.options game.speedOverride = nil - o.speedOverworld, o.speedBattle, o.speedMenu = 10, 200, 1 + o.speedOverworld, o.speedBattle, o.speedMenu = 10, 4, 1 U.wait(2) if game:logicSpeed() ~= 10 then fail(("overworld at OVERWORLD SPEED 10 reads %s"):format(tostring(game:logicSpeed()))) @@ -46,34 +46,32 @@ return function(game) 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()))) + if game:logicSpeed() ~= 4 then + fail(("local battle ran at %sX with speedBattle 4"):format(tostring(game:logicSpeed()))) end checked = checked + 1 if checked == 20 then + game.linkSession = true 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( + if o.speedOverworld ~= 10 or o.speedBattle ~= 4 or o.speedMenu ~= 1 then + fail(("speed presses in a link 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 + if game:logicSpeed() ~= 1 then fail("a link battle was not locked to 1X") end game.speedOverride = nil + game.linkSession = nil end if battle.phase == "menu" and not shot then - shot = U.shot(game, DIR .. "/red_battle_locked.png") + shot = U.shot(game, DIR .. "/red_battle_speed.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.log("battle frames checked at 4X", checked) U.wait(10) if game:logicSpeed() ~= 10 then fail(("after the battle logic speed is %s, want 10"):format(tostring(game:logicSpeed()))) @@ -82,6 +80,6 @@ return function(game) 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") + U.log("PASS red local battle at BATTLE SPEED 4, link locked to 1X") love.event.quit(0) end diff --git a/tests/engine/game3_speed_hotkey_test.lua b/tests/engine/game3_speed_hotkey_test.lua index a8969f77..9ca29c62 100644 --- a/tests/engine/game3_speed_hotkey_test.lua +++ b/tests/engine/game3_speed_hotkey_test.lua @@ -39,12 +39,9 @@ do battleActive = true eq(g:speedCategory(), "battle", "battle stays battle") g.options.speedBattle = 10 - eq(g:logicSpeed(), 1, "a gen3 battle runs 1X whatever BATTLE SPEED says") - g.speedOverride = 20 - eq(g:logicSpeed(), 1, "and whatever speedOverride says") - g.speedOverride = nil + eq(g:logicSpeed(), 10, "a local gen3 battle runs at BATTLE SPEED") g:keypressed("1") - eq(g.options.speedBattle, 10, "1 in battle is ignored") + check(g.options.speedBattle ~= 10, "1 in battle cycles BATTLE SPEED") eq(g.options.speedOverworld, 2, "and leaves OVERWORLD SPEED alone") battleActive = false eq(newGame3("boot"):speedCategory(), "menu", "boot phase stays menu") diff --git a/tests/engine/game_speed_categories_test.lua b/tests/engine/game_speed_categories_test.lua index 200124c2..85c8ad05 100644 --- a/tests/engine/game_speed_categories_test.lua +++ b/tests/engine/game_speed_categories_test.lua @@ -1,5 +1,5 @@ -- Per-category GAME SPEED (RFC 0007): Game.speedCategoryInStack's stack --- walk, Game:logicSpeed()'s precedence (battle/link lock / run-argument override / +-- walk, Game:logicSpeed()'s precedence (link lock / run-argument override / -- the core.logic_speed hook), Game:_cycleSpeed's per-category cycling, and -- the core.logic_speed hook itself exercised through the public mod API -- (Hooks.new() + bus:wrap, the same idiom other hooks' tests use -- not a @@ -66,7 +66,7 @@ do local g = gameWith({ battle }, { speedOverworld = 4, speedBattle = 10, speedMenu = 2 }) eq(g:_resolveLogicSpeed(), 10, "battle reads speedBattle") - eq(g:logicSpeed(), 1, "but a battle always runs 1X whatever speedBattle says") + eq(g:logicSpeed(), 10, "and a local battle runs at BATTLE SPEED") end do local g = gameWith({ overlay }, @@ -105,9 +105,8 @@ end do local g = gameWith({ overworld, battle }, { speedOverworld = 50, speedBattle = 50 }) g.speedOverride = 20 - eq(g:logicSpeed(), 1, "a battle forces 1X over speedOverride too") - local locked, why = g:speedLocked() - check(locked and why == "battle", "speedLocked reports the battle") + eq(g:logicSpeed(), 20, "speedOverride wins in a local battle") + check(not g:speedLocked(), "a local battle is not speed locked") end do local g = gameWith({ overworld }, { speedOverworld = 4 }) @@ -180,10 +179,10 @@ do { speedOverworld = 1, speedBattle = 1, speedMenu = 1 }) function g:writeOptions() writeOptions.calls = writeOptions.calls + 1 end g:_cycleSpeed(1) - eq(g.save.options.speedBattle, 1, "cycling during battle is ignored") + eq(g.save.options.speedBattle, 2, "cycling during battle bumps speedBattle") eq(g.save.options.speedOverworld, 1, "...and leaves speedOverworld alone") eq(g.save.options.speedMenu, 1, "...and leaves speedMenu alone") - eq(writeOptions.calls, 0, "and persists nothing") + eq(writeOptions.calls, 1, "a successful cycle persists the option") end do local calls = 0 diff --git a/tests/engine/gen2_logic_speed_bug1994.lua b/tests/engine/gen2_logic_speed_bug1994.lua index b434d876..ae88cd04 100644 --- a/tests/engine/gen2_logic_speed_bug1994.lua +++ b/tests/engine/gen2_logic_speed_bug1994.lua @@ -31,11 +31,11 @@ local world = {} eq(game({ options = { speed = 10 }, stack = stack(world) }):logicSpeed(), 10, "the overworld under a plain stack honors GAME SPEED") -eq(game({ options = { speed = 10 }, stack = stack(world, battle) }):logicSpeed(), 1, - "a battle on the stack is locked to 1X") -eq(game({ options = { speed = 10 }, speedOverride = 20, - stack = stack(world, battle, {}) }):logicSpeed(), 1, - "even with speedOverride and a menu over the battle") +eq(game({ options = { speed = 10 }, stack = stack(world, battle) }):logicSpeed(), 10, + "a local battle honors GAME SPEED") +eq(game({ options = { speed = 10 }, linkNet = { closed = false }, + stack = stack(world, battle) }):logicSpeed(), 1, + "a link battle is locked to 1X") eq(game({ options = { speed = 10 }, linkNet = { closed = false } }):logicSpeed(), 1, "an open linkNet (online arena, LinkBattle2) is locked to 1X") eq(game({ options = { speed = 10 }, linkNet = { closed = true } }):logicSpeed(), 10, @@ -48,10 +48,12 @@ eq(require("src.ui.gen2.BattleState").isBattle, true, do local persisted = 0 local g = game({ options = { speed = 1 }, stack = stack(world, battle), + linkNet = { closed = false }, persistOptions = function() persisted = persisted + 1 end }) g:_cycleSpeed(1) - eq(g.options.speed, 1, "SPEED + in battle is ignored") + eq(g.options.speed, 1, "SPEED + in a link battle is ignored") eq(persisted, 0, "and persists nothing") + g.linkNet = nil g.stack = stack(world) g:_cycleSpeed(1) eq(g.options.speed, 2, "SPEED + on the overworld still cycles") diff --git a/tests/engine/options_submenu_marquee_test.lua b/tests/engine/options_submenu_marquee_test.lua index 4b119db2..b9f570c1 100644 --- a/tests/engine/options_submenu_marquee_test.lua +++ b/tests/engine/options_submenu_marquee_test.lua @@ -53,7 +53,7 @@ end for _, spec in ipairs({ { "group.battle", 6 }, { "group.audio", 3 }, { "group.video", 5 }, - { "group.speed", 3 }, { "group.graphics", 4 }, { "group.extras", 3 }, + { "group.speed", 4 }, { "group.graphics", 4 }, { "group.extras", 3 }, }) do local row = find(menu.view, spec[1]) T.check(row, spec[1] .. " has an opener row") @@ -100,7 +100,7 @@ game.stack:pop() local speed = find(menu.view, "group.speed") speed.activate(game) T.eq(ids(game.stack:top().view), - "textSpeed,speedOverworld,speedMenu", + "textSpeed,speedOverworld,speedBattle,speedMenu", "TEXT SPEED heads the SPEED page") game.stack:pop() diff --git a/tests/engine/speed_lock_test.lua b/tests/engine/speed_lock_test.lua index 7903b8b2..15735781 100644 --- a/tests/engine/speed_lock_test.lua +++ b/tests/engine/speed_lock_test.lua @@ -31,16 +31,18 @@ do eq(g:logicSpeed(), 200, "gen1 overworld runs the saved 200X") check(not g:speedLocked(), "gen1 overworld is not locked") g = gen1({ overworld, battle }) - eq(g:logicSpeed(), 1, "gen1 battle is 1X at speedBattle 200") + eq(g:logicSpeed(), 200, "gen1 local battle runs BATTLE SPEED") + g.linkSession = true + eq(g:logicSpeed(), 1, "gen1 link battle is 1X at speedBattle 200") g.speedOverride = 200 - eq(g:logicSpeed(), 1, "gen1 battle ignores speedOverride (--speed, skin FF hold)") + eq(g:logicSpeed(), 1, "gen1 link battle ignores speedOverride (--speed, skin FF hold)") local bus = Hooks.new() local saved = Runtime.hooks Runtime.hooks = bus local ran = false local unsub = bus:wrap("core.logic_speed", function() ran = true; return 200 end) g.speedOverride = nil - eq(g:logicSpeed(), 1, "gen1 battle ignores a core.logic_speed hook returning 200") + eq(g:logicSpeed(), 1, "gen1 link battle ignores a core.logic_speed hook returning 200") check(not ran, "the hook is never called while locked") local ow = gen1({ overworld }) eq(ow:logicSpeed(), 200, "outside battle the hook still runs") @@ -52,6 +54,7 @@ end do GameSpeed.setAllowed({ 2, 4 }) local g = gen1({ overworld, battle }) + g.linkSession = true eq(g:logicSpeed(), 1, "a cart ladder without 1 still locks to a literal 1") eq(gen1({ overworld }, { speedOverworld = 4 }):logicSpeed(), 4, "and outside battle the ladder applies") @@ -61,17 +64,21 @@ end do local g = gen1({ overworld, battle }, { speedOverworld = 1, speedBattle = 1, speedMenu = 1 }) + g.linkSession = true g:_cycleSpeed(1) g:_cycleSpeed(-1) g:_cycleSpeed(1) - eq(g.save.options.speedBattle, 1, "gen1 speed presses in battle leave speedBattle") + eq(g.save.options.speedBattle, 1, "gen1 speed presses in a link battle leave speedBattle") eq(g.save.options.speedOverworld, 1, "and speedOverworld") g:touchSkinHotkey("fast_forward_toggle", true) - eq(g.save.options.speedBattle, 1, "gen1 skin FF toggle in battle is ignored") + eq(g.save.options.speedBattle, 1, "gen1 skin FF toggle in a link battle is ignored") g:touchSkinHotkey("fast_forward_hold", true) - eq(g:logicSpeed(), 1, "gen1 skin FF hold in battle stays 1X") + eq(g:logicSpeed(), 1, "gen1 skin FF hold in a link battle stays 1X") g:touchSkinHotkey("fast_forward_hold", false) eq(g.speedOverride, nil, "and releasing restores the pre-hold override") + g.linkSession = nil + g:_cycleSpeed(1) + eq(g.save.options.speedBattle, 2, "gen1 speed press in a local battle cycles speedBattle") end do @@ -91,27 +98,28 @@ end do eq(gen2({ options = { speed = 200 }, stack = stack({}) }):logicSpeed(), 200, "gen2 overworld runs GAME SPEED") - eq(gen2({ options = { speed = 200 }, stack = stack({}, battle) }):logicSpeed(), 1, - "gen2 battle is 1X") - eq(gen2({ options = { speed = 200 }, speedOverride = 200, + eq(gen2({ options = { speed = 200 }, stack = stack({}, battle) }):logicSpeed(), 200, + "gen2 local battle runs GAME SPEED") + eq(gen2({ options = { speed = 200 }, speedOverride = 200, linkNet = { closed = false }, stack = stack({}, battle) }):logicSpeed(), 1, - "gen2 battle ignores speedOverride") + "gen2 link battle ignores speedOverride") eq(gen2({ options = { speed = 200 }, linkNet = { closed = false } }):logicSpeed(), 1, "gen2 online arena / LinkBattle2 linkNet is 1X") eq(gen2({ options = { speed = 200 }, linkSession = true }):logicSpeed(), 1, "gen2 tournament / spectate linkSession is 1X") local g = gen2({ options = { speed = 1 }, stack = stack({}, battle) }) g:_cycleSpeed(1) - eq(g.options.speed, 1, "gen2 pad speed press in battle is ignored") + eq(g.options.speed, 2, "gen2 pad speed press in a local battle cycles") g.stack = stack({}) g.linkNet = { closed = false } g:_cycleSpeed(1) - eq(g.options.speed, 1, "gen2 pad speed press over a link is ignored") + eq(g.options.speed, 2, "gen2 pad speed press over a link is ignored") end -local battleActive = false +local battleActive, battleLink = false, false package.loaded["src.core.game3.battle"] = { isActive = function() return battleActive end, + getState = function() return { link = battleLink } end, } local function gen3() @@ -126,13 +134,15 @@ do local g = gen3() eq(g:logicSpeed(), 200, "gen3 field runs OVERWORLD SPEED") battleActive = true - eq(g:logicSpeed(), 1, "gen3 battle is 1X") + eq(g:logicSpeed(), 200, "gen3 local battle runs BATTLE SPEED") + battleLink = true + eq(g:logicSpeed(), 1, "gen3 link battle is 1X") g.speedOverride = 200 - eq(g:logicSpeed(), 1, "gen3 battle ignores speedOverride") + eq(g:logicSpeed(), 1, "gen3 link battle ignores speedOverride") g.speedOverride = nil g:_cycleSpeed(1) - eq(g.options.speedBattle, 200, "gen3 speed press in battle is ignored") - battleActive = false + eq(g.options.speedBattle, 200, "gen3 speed press in a link battle is ignored") + battleActive, battleLink = false, false package.loaded["src.core.game3.link.union_room"] = { isActive = function() return true end } eq(g:logicSpeed(), 1, "gen3 Union Room is 1X") package.loaded["src.core.game3.link.union_room"] = nil @@ -148,7 +158,11 @@ local function runFrame(g, lockAt) FixedStep.clock = function() return 0 end FixedStep:init(function() steps = steps + 1 - if steps == lockAt then battleActive = true; g.stack = stack(overworld, battle) end + if steps == lockAt then + battleActive, battleLink = true, true + g.linkSession = true + g.stack = stack(overworld, battle) + end g:_speedLockEdge() end) local speed = g:logicSpeed() @@ -165,18 +179,18 @@ do local g = gen3() local steps, speed = runFrame(g, 3) eq(speed, 200, "the frame started unlocked at 200X") - eq(steps, 3, "gen3: a battle starting mid-frame ends the frame on that step") - battleActive = false + eq(steps, 3, "gen3: a link battle starting mid-frame ends the frame on that step") + battleActive, battleLink = false, false local g1 = gen1({ overworld }) steps = runFrame(g1, 5) - eq(steps, 5, "gen1: a battle push mid-frame ends the frame on that step") + eq(steps, 5, "gen1: a link battle push mid-frame ends the frame on that step") eq(g1:logicSpeed(), 1, "and the next frame samples 1X") local g2 = gen2({ options = { speed = 200 }, stack = stack({}) }) steps = runFrame(g2, 2) - eq(steps, 2, "gen2: a battle push mid-frame ends the frame on that step") - battleActive = false + eq(steps, 2, "gen2: a link battle push mid-frame ends the frame on that step") + battleActive, battleLink = false, false local plain = gen1({ overworld }) steps = runFrame(plain, -1) diff --git a/tests/mod_ui_tests.lua b/tests/mod_ui_tests.lua index 37e84a8a..0d4d7f5a 100644 --- a/tests/mod_ui_tests.lua +++ b/tests/mod_ui_tests.lua @@ -288,7 +288,7 @@ local WANT_IDS = { "textSpeed", "animations", "battleStyle", "battleLayout", "performance", "colors", "tilt", "uiLetterbox", "shaderfx", "shaderfx2", "zoom", "voidFill", "videoMode", "faithfulRes", "screenPos", "fpsCap", "vsync", "logicClock", - "speedOverworld", "speedMenu", + "speedOverworld", "speedBattle", "speedMenu", "mods", "controls", "dateFormat", "timeFormat" } local function orow(menu, id) for _, row in ipairs(menu.rows) do diff --git a/tests/run_tests.lua b/tests/run_tests.lua index a182bc9e..58f64f53 100644 --- a/tests/run_tests.lua +++ b/tests/run_tests.lua @@ -2980,7 +2980,11 @@ do -- bug when the cycling is fine and the row is simply one longer. for _ = 1, #GameSpeed.LEVELS - 1 do press("a") end eq(og.save.options.speedOverworld, 1, "OVERWORLD SPEED wraps back to NORMAL") - check(not seek("speedBattle"), "no BATTLE SPEED row: battles always run 1X") + check(seek("speedBattle"), "cursor reaches BATTLE SPEED") + press("a") + eq(og.save.options.speedBattle, 2, "A cycles BATTLE SPEED to 2X") + for _ = 1, #GameSpeed.LEVELS - 1 do press("a") end + eq(og.save.options.speedBattle, 1, "BATTLE SPEED wraps back to NORMAL") check(seek("speedMenu"), "cursor reaches MENU SPEED") press("a") eq(og.save.options.speedMenu, 2, "A cycles MENU SPEED to 2X")