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.
A platform-specific launcher wrapper (a native shell embedding this engine,
owning its own UI around the game window) needs to pause the simulation
while its own UI is on top, live-reload options it wrote outside any Lua
UI, and veto main.lua's "closing the window returns to the Lua launcher"
behavior when it owns that job itself. Implementing this by hand-patching
main.lua's love.update/love.quit directly ties every such integration to
editing the one file every other engine change also touches, guaranteeing
merge conflicts. No existing hook covers "should the per-frame simulation
step run" or "should closing the window return to the Lua launcher."
Adds two generic, additive hooks (src/core/PlatformHooks.lua): core.update
and core.quit_to_launcher, replacing what would otherwise be inline
main.lua special-casing. Also adds Manifest.force_enable_env, letting a
mod that cannot function disabled on the one build where its env var is
set (a platform-bridge mod bundled only with that build) re-enable itself
regardless of a saved disable.
RFC 0006 status: Proposed.
mod_world_tests is already the mod.world suite and already the T3 tier,
with the same off-the-world refusal pattern the standalone file was
duplicating. Reuses its liveWorld fixture instead of standing up a
second one.
overworld() resolves the world from under the stack, so a call from a
battle hook stacked a second battle over the live one -- on a loss its
afterBattle blacked out and warped with the outer battle still up.
newWild marks the species SEEN before it reports an empty party, so a
refused call still wrote the Pokedex; test the party before building it.
tonumber accepts 5.5, which Pokemon.new writes straight into the stat
calc and the exp curve.
Starting a wild encounter had no supported entry point, so mods built a
BattleState and pushed it themselves -- silently losing onFinish (and
with it evolutions and blackout-on-loss) and pushBattle (entry wipe,
battle theme). Neither failure raises.
Also covers awardExp -> leveledUp -> afterBattle -> checkParty, which
parity_trainer_evolution_order stubs BattleState out of.
options.lua is a whole-file rewrite, so a caller handing saveOptions a
partial table (only the keys it changed) silently dropped every key it did
not mention: launcher-only keys like lastVersion, and keys the launcher set
(battleBg, tilt) all fell back to defaults.
saveOptions now reads the on-disk file first and folds caller-absent
values underneath before mergeOptions backfills defaults. A table holding
every defaultOptions key is a full snapshot and stays authoritative, so the
fold is inert for all in-repo writers (every one passes loadOptions-ed
tables) and cannot resurrect the bindings/activeProfile deletions the
RESET REBINDS and mod-manager paths make on full tables.
Adds a regression suite (options_partial_write_bug932.lua) pinning the
merge, and updates the #828 suite's partial-write assertion, which now
expects lastVersion to survive a delta write.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
After the Fly departure animation finished (bird off-screen) and during
Dig/teleport, the trainer sprite popped back in standing at the old cell
for the whole 32-frame black fade-out before the transition. The
player-hide guard only held while a departure animation was live:
flyAnim went nil the instant path2 completed and the teleportOut
countdown cleared the spin fields at 0, but startWarpTo's Transition
(not isOpaque) keeps the overworld drawing beneath the veil, and the
arrival animation is not armed until setMap's midpoint.
Add a playerHidden flag on OverworldState that bridges the gap:
- set when each departure completes (flyAnim path2 / teleportOut hit 0),
immediately before the warp starts;
- cleared in startWarpTo's Transition enter callback, synchronously
after setMap and before the arrival arms flyArrive / spinDrop, so the
player is never drawable mid-fade and never bare on the landing frame;
- folded into both player-draw guards.
ROM-free regression test (tests/engine/warp_sprite_hidden_bug916.lua)
drives the REAL Transition + setMap headlessly for Dig and Fly and
asserts zero fade frames leave the player drawable bare (would have
observed 31/32 gap frames before the fix). Runs in the CI headless T2
tier.
Dig spin timing/lift and the black fade color are left as-is (fade is
intentional per #607).
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
trainers.battleTheme validated and merged onto the trainer record but was
never read: battle music came solely from data.audio.battle[kind] where
kind is computeMusicKind()'s final/gym/trainer/wild. Route both battle-
theme start sites through a single choke point:
- BattleState:playBattleTheme() cues Music.playBattle with the override
(self.trainer.battleTheme via battleTheme()), defaulting to the kind
when unset, so vanilla fights and #782's non-gym Giovanni are unchanged.
- BattleState:enter() and OverworldController:pushBattle() both call it.
- Music.playBattle gains an optional 4th song arg that overrides the kind
default, and real call sites now populate the music.select trainerId.
- Victory jingles stay kind-based: a custom battle theme has no derivable
win-variant.
New ROM-free T2 suite tests/engine/trainer_battle_theme_bug945.lua covers
mod load, override resolution, the choke point, and the nil-override
parity gate.
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>