mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
Per-category GAME SPEED: overworld/battle/menu + core.logic_speed hook (RFC 0007)
GameSpeed is a single fast-forward multiplier applied uniformly to the whole logic clock -- overworld walking, menu navigation and battle turns all scale together. A player who wants 4X battles but 1X overworld (so a cutscene or NPC dialogue doesn't blur past) has no way to get both. Splits save.options.speed into speedOverworld/speedBattle/speedMenu, each cycling independently, with an automatic migration so an existing save's speed choice carries over. Game.speedCategoryInStack resolves which category is active by walking the state stack (the same idiom wideBattleInStack/fillScaleInStack already use), so a menu opened mid- battle inherits battle speed rather than resetting to whatever "menu" defaults to. Adds a new core.logic_speed hook so a mod can read or override the resolved multiplier for the current frame regardless of which category produced it, sitting after the link-play and run-argument overrides so neither is a seam a mod can defeat. RFC 0007 status: Proposed.
This commit is contained in:
@@ -37,6 +37,12 @@ local BattleState = {}
|
||||
BattleState.__index = BattleState
|
||||
BattleState.isOpaque = true
|
||||
|
||||
-- Category identity for per-category GAME SPEED (RFC 0007), the same
|
||||
-- style OverworldController.isOverworld already uses. Every battle --
|
||||
-- wild, trainer, link, safari, the old-man demo -- is this metatable, so
|
||||
-- Game.speedCategoryInStack needs no special-casing beyond this one flag.
|
||||
BattleState.isBattle = true
|
||||
|
||||
function BattleState:romText(label, fallback, ...)
|
||||
return romText(self.data, label, fallback, ...)
|
||||
end
|
||||
|
||||
+58
-9
@@ -221,24 +221,44 @@ function Game:step(dt)
|
||||
-- it on its own real-time 60Hz accumulator instead.
|
||||
end
|
||||
|
||||
-- The per-category (RFC 0007) save.options multiplier for whichever of
|
||||
-- "battle"/"overworld"/"menu" Game.speedCategoryInStack says is active
|
||||
-- right now. This is the "vanilla" the core.logic_speed hook wraps below
|
||||
-- -- Game:logicSpeed calls it AFTER the link and speedOverride checks, so
|
||||
-- neither a mod nor the category resolution ever has a seam to defeat them.
|
||||
function Game:_resolveLogicSpeed()
|
||||
local GameSpeed = require("src.core.GameSpeed")
|
||||
local category = Game.speedCategoryInStack(self.stack)
|
||||
local key = GameSpeed.optionKey(category)
|
||||
local opts = self.save and self.save.options
|
||||
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 row takes effect immediately; speedOverride is the --speed /
|
||||
-- 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")
|
||||
-- Link play is always 1X on both machines, and this wins over every other
|
||||
-- source including POKEPORT_SPEED. Fast-forward multiplies the logic
|
||||
-- clock, so a peer at 10X burned a tournament shot clock ten times faster
|
||||
-- than the opponent it is racing, and drove its own animation/message
|
||||
-- queue at a different rate than the peer it is locked to. Nothing about
|
||||
-- a match should depend on what either player set this to.
|
||||
-- source including POKEPORT_SPEED and every per-category option.
|
||||
-- Fast-forward multiplies the logic clock, so a peer at 10X burned a
|
||||
-- tournament shot clock ten times faster than the opponent it is racing,
|
||||
-- and drove its own animation/message queue at a different rate than the
|
||||
-- peer it is locked to. Nothing about a match should depend on what
|
||||
-- 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
|
||||
end
|
||||
if self.speedOverride then return GameSpeed.clamp(self.speedOverride) end
|
||||
local opts = self.save and self.save.options
|
||||
return GameSpeed.clamp(opts and opts.speed or GameSpeed.DEFAULT)
|
||||
-- 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
|
||||
-- Hooks:call only guards against a hook that throws, not one that
|
||||
-- returns a bad value, so an unclamped result would flow straight into
|
||||
-- the FixedStep accumulator math below and freeze or destabilize logic.
|
||||
return GameSpeed.clamp(ModRuntime.call("core.logic_speed",
|
||||
function(g) return g:_resolveLogicSpeed() end, self))
|
||||
end
|
||||
|
||||
function Game:update(dt)
|
||||
@@ -337,6 +357,28 @@ function Game.wideBattleInStack(stack)
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Which of "battle"/"overworld"/"menu" per-category GAME SPEED (RFC 0007)
|
||||
-- applies right now. Whole-stack, the same idiom as fillScaleInStack/
|
||||
-- wideBattleInStack above: an overlay with neither marker (PartyMenu,
|
||||
-- ChoiceBox, a NamingScreen, a text box) is transparent to the walk and
|
||||
-- inherits whatever is under it, making the category a property of the
|
||||
-- STACK POSITION the overlay sits over, not of the overlay itself. A
|
||||
-- scripted sequence (script.started/ended) never pushes a state of its
|
||||
-- own either -- it runs through the owning overworld/battle state's own
|
||||
-- script runner or message queue -- so it inherits the same way. Nothing
|
||||
-- identifying as either (the title screen, credits, an intro cutscene
|
||||
-- with nothing under it) falls to "menu", the bucket every non-gameplay
|
||||
-- screen gets; see the RFC's Decisions section for the full reasoning.
|
||||
function Game.speedCategoryInStack(stack)
|
||||
local states = stack and stack.states
|
||||
for i = #(states or {}), 1, -1 do
|
||||
local state = states[i]
|
||||
if state and state.isBattle then return "battle" end
|
||||
if state and state.isOverworld then return "overworld" end
|
||||
end
|
||||
return "menu"
|
||||
end
|
||||
|
||||
-- Whether a state on the stack composes its own screen and so wants the
|
||||
-- edge anchors held off (BattleState.holdsUIAnchors). Whole-stack, like
|
||||
-- everything else here: the text box and YES/NO a battle puts up are states
|
||||
@@ -556,8 +598,15 @@ 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")
|
||||
self.save.options.speed = GameSpeed.cycle(self.save.options.speed, dir)
|
||||
local key = GameSpeed.optionKey(Game.speedCategoryInStack(self.stack))
|
||||
self.save.options[key] = GameSpeed.cycle(self.save.options[key], dir)
|
||||
self:writeOptions()
|
||||
end
|
||||
|
||||
|
||||
@@ -51,4 +51,19 @@ function GameSpeed.cycle(v, dir)
|
||||
return levels[nextIdx]
|
||||
end
|
||||
|
||||
-- Per-category speed (RFC 0007): overworld walking, battle turns and menu
|
||||
-- navigation each cycle their own multiplier instead of one global "speed"
|
||||
-- value. This list is the single source of truth for which categories
|
||||
-- exist and the order the Options rows/save.options keys follow;
|
||||
-- Game.lua's stack-walk (Game.speedCategoryInStack) decides WHICH category
|
||||
-- is active on a given frame, this module only knows the category names.
|
||||
GameSpeed.CATEGORIES = { "overworld", "battle", "menu" }
|
||||
|
||||
-- the save.options field name a category's multiplier lives under, e.g.
|
||||
-- "overworld" -> "speedOverworld". Centralized so Game.lua, OptionsMenu.lua,
|
||||
-- LauncherSettings.lua and the SaveData migration never hand-spell the key.
|
||||
function GameSpeed.optionKey(category)
|
||||
return "speed" .. category:sub(1, 1):upper() .. category:sub(2)
|
||||
end
|
||||
|
||||
return GameSpeed
|
||||
|
||||
+19
-2
@@ -261,8 +261,12 @@ function SaveData.defaultOptions()
|
||||
-- (the PIKACHU VOL row appears only on Yellow; see Sound.lua)
|
||||
pikaVol = 7,
|
||||
musicFilter = 0,
|
||||
-- logic fast-forward multiplier; audio is unaffected (GameSpeed.lua)
|
||||
speed = 1,
|
||||
-- Per-category logic fast-forward multiplier (RFC 0007); audio is
|
||||
-- unaffected (GameSpeed.lua). Superseded from a single "speed" field --
|
||||
-- mergeOptions migrates an old save's value into all three below.
|
||||
speedOverworld = 1,
|
||||
speedBattle = 1,
|
||||
speedMenu = 1,
|
||||
-- port display options (OptionsMenu / hotkeys 2/3/4/5)
|
||||
colors = "gbc",
|
||||
tilt = 0,
|
||||
@@ -339,6 +343,19 @@ function SaveData.mergeOptions(loaded)
|
||||
for k, v in pairs(loaded) do
|
||||
opts[k] = v
|
||||
end
|
||||
-- RFC 0007 migration: a save from before per-category GAME SPEED still
|
||||
-- has a single "speed" and none of the three new fields, so seed all
|
||||
-- three from it -- an existing player's fast-forward preference
|
||||
-- carries over instead of two of the three categories silently
|
||||
-- resetting to 1X. "speed" is dropped on the way out (not kept as a
|
||||
-- stale alias), so a re-save never re-triggers this migration.
|
||||
if loaded.speed ~= nil and loaded.speedOverworld == nil
|
||||
and loaded.speedBattle == nil and loaded.speedMenu == nil then
|
||||
opts.speedOverworld = loaded.speed
|
||||
opts.speedBattle = loaded.speed
|
||||
opts.speedMenu = loaded.speed
|
||||
end
|
||||
opts.speed = nil
|
||||
end
|
||||
return opts
|
||||
end
|
||||
|
||||
@@ -214,10 +214,24 @@ local function coreRows(opts, hooks)
|
||||
|
||||
local okSpd, GameSpeed = pcall(require, "src.core.GameSpeed")
|
||||
if okSpd then
|
||||
add(Strings("GAME SPEED"),
|
||||
function() return GameSpeed.levelLabel(opts.speed) end,
|
||||
-- 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.speed = GameSpeed.cycle(opts.speed, 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)
|
||||
opts.speedMenu = GameSpeed.cycle(opts.speedMenu, dir)
|
||||
return true
|
||||
end)
|
||||
end
|
||||
|
||||
+25
-4
@@ -392,14 +392,35 @@ 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
|
||||
{ id = "speed", label = Strings("GAME SPEED"),
|
||||
-- (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.
|
||||
{ id = "speedOverworld", label = Strings("OVERWORLD SPEED"),
|
||||
value = function(g)
|
||||
return GameSpeed.levelLabel(g.save.options.speed)
|
||||
return GameSpeed.levelLabel(g.save.options.speedOverworld)
|
||||
end,
|
||||
step = function(g, dir)
|
||||
local o = g.save.options
|
||||
o.speed = GameSpeed.cycle(o.speed, dir)
|
||||
o.speedOverworld = GameSpeed.cycle(o.speedOverworld, dir)
|
||||
return true
|
||||
end },
|
||||
{ id = "speedBattle", label = Strings("BATTLE SPEED"),
|
||||
value = function(g)
|
||||
return GameSpeed.levelLabel(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 GameSpeed.levelLabel(g.save.options.speedMenu)
|
||||
end,
|
||||
step = function(g, dir)
|
||||
local o = g.save.options
|
||||
o.speedMenu = GameSpeed.cycle(o.speedMenu, dir)
|
||||
return true
|
||||
end },
|
||||
-- the manager's discoverable home (18-mod-manager-ux); inert until
|
||||
|
||||
Reference in New Issue
Block a user