mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
feat: reconstruct standard battle continuations
This commit is contained in:
@@ -98,6 +98,15 @@ function BattleState:bgMode()
|
||||
return "white"
|
||||
end
|
||||
|
||||
-- Resume a semantic checkpoint directly at the command menu. Unlike enter(),
|
||||
-- this deliberately does not replay the battle transition, intro queues,
|
||||
-- cries, happiness changes, or battle-start events.
|
||||
function BattleState:resumeCheckpoint()
|
||||
self.isOpaque = self:bgMode() ~= "world"
|
||||
require("src.core.Music").playBattle(self.data,
|
||||
self.musicKind or self:computeMusicKind())
|
||||
end
|
||||
|
||||
-- How far to dim the overworld behind a "world" background, 0..1. Enough
|
||||
-- that the battle reads as the foreground rather than competing with a fully
|
||||
-- lit map behind it.
|
||||
|
||||
@@ -111,6 +111,12 @@ function BattleCheckpoint.validate(game, checkpoint)
|
||||
return nil, "battle_origin_unsupported",
|
||||
"Battle continuation data is unsupported or inconsistent."
|
||||
end
|
||||
if model.kind == "trainer" and (type(model.origin.npcId) ~= "string"
|
||||
or model.origin.trainerClass ~= model.oppClass
|
||||
or model.origin.partyIndex ~= (model.partyIndex or 1)) then
|
||||
return nil, "battle_origin_unsupported",
|
||||
"Trainer continuation data is incomplete or inconsistent."
|
||||
end
|
||||
local party = checkpoint.save.party
|
||||
if type(party) ~= "table" or not validateBattler(game.data, model.player, #party) then
|
||||
return nil, "invalid_content", "Player battle state is invalid."
|
||||
@@ -150,7 +156,11 @@ end
|
||||
local function applyBattler(target, captured, copy)
|
||||
for _, field in ipairs(BATTLER_FIELDS) do
|
||||
if field ~= "curStats" and field ~= "curTypes" and field ~= "curMoves" then
|
||||
target[field] = captured[field] ~= nil and clone(captured[field], copy) or nil
|
||||
if captured[field] ~= nil then
|
||||
target[field] = clone(captured[field], copy)
|
||||
else
|
||||
target[field] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
target.curStats = captured.curStatsFromMon and target.mon.stats
|
||||
@@ -192,7 +202,11 @@ function BattleCheckpoint.restore(game, checkpoint, copy)
|
||||
applyBattler(battle.enemy, model.enemy, copy)
|
||||
|
||||
for _, field in ipairs(BATTLE_FIELDS) do
|
||||
battle[field] = model[field] ~= nil and clone(model[field], copy) or nil
|
||||
if model[field] ~= nil then
|
||||
battle[field] = clone(model[field], copy)
|
||||
else
|
||||
battle[field] = nil
|
||||
end
|
||||
end
|
||||
battle.kind = model.kind
|
||||
battle.checkpointOrigin = assert(copy(model.origin))
|
||||
|
||||
@@ -1143,4 +1143,15 @@ function Game:restoreCheckpointSave(loaded)
|
||||
{ via = "checkpoint", checkpoint = true })
|
||||
end
|
||||
|
||||
-- Install a reconstructed battle without calling BattleState:enter(), whose
|
||||
-- transition, intro queues and battle-start side effects already happened in
|
||||
-- the checkpointed timeline.
|
||||
function Game:restoreCheckpointBattle(battle)
|
||||
if self.stack:top() ~= self.overworld then
|
||||
error("battle checkpoint requires a reconstructed overworld base", 0)
|
||||
end
|
||||
self.stack.states[#self.stack.states + 1] = battle
|
||||
if battle.resumeCheckpoint then battle:resumeCheckpoint() end
|
||||
end
|
||||
|
||||
return Game
|
||||
|
||||
@@ -3059,6 +3059,14 @@ function OverworldState:engageTrainer(npc, onDone, endBattleText, skipBattleText
|
||||
if theme then require("src.core.Music").play(Game.data, theme) end
|
||||
end
|
||||
local battle = BattleState.newTrainer(Game, d.trainerClass, d.trainerParty)
|
||||
battle.checkpointOrigin = {
|
||||
kind = "trainer_encounter",
|
||||
map = self.map.id,
|
||||
npcId = npc.id,
|
||||
trainerClass = d.trainerClass,
|
||||
partyIndex = d.trainerParty or 1,
|
||||
event = header and header.event or nil,
|
||||
}
|
||||
-- PrintEndBattleText (home/trainers.asm:341) is called from
|
||||
-- TrainerBattleVictory (engine/battle/core.asm:942), i.e. ON the battle
|
||||
-- screen once ScrollTrainerPicAfterBattle has brought the beaten trainer
|
||||
@@ -3596,6 +3604,10 @@ function OverworldState:onStepComplete()
|
||||
end
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
local battle = BattleState.newWild(Game, enc.species, enc.level)
|
||||
battle.checkpointOrigin = {
|
||||
kind = "wild_encounter",
|
||||
map = self.map.id,
|
||||
}
|
||||
-- map.ghostBattles: unidentifiable without the named item (the
|
||||
-- Pokemon Tower's Silph Scope)
|
||||
local ghost = Map.ghostBattles(self.map.def)
|
||||
@@ -4000,6 +4012,39 @@ function OverworldState:afterBattle(result, battle)
|
||||
end
|
||||
end
|
||||
|
||||
-- Rebind the data-only continuation attached to a supported battle checkpoint.
|
||||
-- The overworld was reconstructed first, so transient input/NPC freezes from
|
||||
-- the original encounter are intentionally not resumed.
|
||||
function OverworldState:restoreBattleContinuation(battle, origin)
|
||||
local game = battle and battle.game
|
||||
if not game or type(origin) ~= "table" or not self.map
|
||||
or origin.map ~= self.map.id then
|
||||
return false
|
||||
end
|
||||
if origin.kind == "wild_encounter" and battle.kind == "wild" then
|
||||
battle.onFinish = function(result) self:afterBattle(result, battle) end
|
||||
return true
|
||||
end
|
||||
if origin.kind ~= "trainer_encounter" or battle.kind ~= "trainer"
|
||||
or origin.trainerClass ~= battle.oppClass
|
||||
or origin.partyIndex ~= (battle.partyIndex or 1)
|
||||
or type(origin.npcId) ~= "string" then
|
||||
return false
|
||||
end
|
||||
battle.onFinish = function(result)
|
||||
if result == "win" then
|
||||
game.save.defeatedTrainers[origin.npcId] = true
|
||||
if origin.event then game.save.flags[origin.event] = true end
|
||||
self:checkVictoryRewards(battle.oppClass, battle.partyIndex)
|
||||
end
|
||||
self:afterBattle(result, battle)
|
||||
self.engaging = false
|
||||
local npc = self.npcPool and self.npcPool[origin.npcId]
|
||||
if npc then npc.frozen = false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
-- -------------------------------------------------------------------------
|
||||
-- warps
|
||||
-- -------------------------------------------------------------------------
|
||||
|
||||
@@ -46,7 +46,8 @@ local function makeGame(kind)
|
||||
if kind == "trainer" then
|
||||
battle = BattleState.newTrainer(game, "OPP_FIX_YOUNGSTER", 1)
|
||||
battle.checkpointOrigin = {
|
||||
kind = "trainer_encounter", map = "FIX_TOWN", npc = "TRAINER_1",
|
||||
kind = "trainer_encounter", map = "FIX_TOWN", npcId = "TRAINER_1",
|
||||
trainerClass = "OPP_FIX_YOUNGSTER", partyIndex = 1,
|
||||
event = "EVENT_BEAT_TRAINER_1",
|
||||
}
|
||||
else
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
-- Engine-owned battle continuations replace unserializable onFinish closures
|
||||
-- after a persistent checkpoint reconstructs the overworld and battle.
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
love = love or require("tests.love_stub")
|
||||
|
||||
local T = require("tests.harness").suite("battle checkpoint continuation")
|
||||
local GameMethods = require("src.core.Game")
|
||||
local OverworldState = require("src.world.OverworldController")
|
||||
|
||||
local function fakeOverworld()
|
||||
local npc = { id = "FIX_TOWN_obj_1", frozen = true }
|
||||
local ow = setmetatable({
|
||||
map = { id = "FIX_TOWN" },
|
||||
npcPool = { [npc.id] = npc },
|
||||
engaging = true,
|
||||
}, { __index = OverworldState })
|
||||
ow.afterBattle = function(self, result, battle)
|
||||
self.after = { result = result, battle = battle }
|
||||
end
|
||||
ow.checkVictoryRewards = function(self, class, party)
|
||||
self.reward = { class = class, party = party }
|
||||
end
|
||||
return ow, npc
|
||||
end
|
||||
|
||||
local wildOw = fakeOverworld()
|
||||
local wildGame = { save = { defeatedTrainers = {}, flags = {} } }
|
||||
local wild = { game = wildGame, kind = "wild" }
|
||||
T.check(wildOw:restoreBattleContinuation(wild,
|
||||
{ kind = "wild_encounter", map = "FIX_TOWN" }) == true,
|
||||
"ordinary wild continuation binds")
|
||||
wild.onFinish("run")
|
||||
T.same(wildOw.after, { result = "run", battle = wild },
|
||||
"wild continuation returns through canonical afterBattle")
|
||||
|
||||
local trainerOw, trainerNpc = fakeOverworld()
|
||||
local trainerGame = { save = { defeatedTrainers = {}, flags = {} } }
|
||||
local trainer = {
|
||||
game = trainerGame, kind = "trainer",
|
||||
oppClass = "OPP_FIX_YOUNGSTER", partyIndex = 1,
|
||||
}
|
||||
local trainerOrigin = {
|
||||
kind = "trainer_encounter", map = "FIX_TOWN",
|
||||
npcId = trainerNpc.id, trainerClass = trainer.oppClass, partyIndex = 1,
|
||||
event = "EVENT_BEAT_FIX_TRAINER",
|
||||
}
|
||||
T.check(trainerOw:restoreBattleContinuation(trainer, trainerOrigin) == true,
|
||||
"ordinary trainer continuation binds")
|
||||
trainer.onFinish("win")
|
||||
T.check(trainerGame.save.defeatedTrainers[trainerNpc.id] == true,
|
||||
"trainer win stamps the stable object id")
|
||||
T.check(trainerGame.save.flags.EVENT_BEAT_FIX_TRAINER == true,
|
||||
"trainer win stamps the header event")
|
||||
T.same(trainerOw.reward,
|
||||
{ class = "OPP_FIX_YOUNGSTER", party = 1 },
|
||||
"trainer win runs canonical victory rewards")
|
||||
T.same(trainerOw.after, { result = "win", battle = trainer },
|
||||
"trainer win returns through canonical afterBattle")
|
||||
T.check(trainerOw.engaging == false and trainerNpc.frozen == false,
|
||||
"reconstructed trainer completion leaves overworld input unfrozen")
|
||||
|
||||
local lossOw, lossNpc = fakeOverworld()
|
||||
local lossGame = { save = { defeatedTrainers = {}, flags = {} } }
|
||||
local lossBattle = {
|
||||
game = lossGame, kind = "trainer",
|
||||
oppClass = "OPP_FIX_YOUNGSTER", partyIndex = 1,
|
||||
}
|
||||
T.check(lossOw:restoreBattleContinuation(lossBattle, trainerOrigin) == true,
|
||||
"trainer loss continuation binds")
|
||||
lossBattle.onFinish("lose")
|
||||
T.eq(lossGame.save.defeatedTrainers[lossNpc.id], nil,
|
||||
"trainer loss does not stamp the trainer defeated")
|
||||
T.eq(lossGame.save.flags.EVENT_BEAT_FIX_TRAINER, nil,
|
||||
"trainer loss does not stamp the header event")
|
||||
T.eq(lossOw.reward, nil, "trainer loss does not grant victory rewards")
|
||||
|
||||
local mismatchOw = fakeOverworld()
|
||||
T.check(mismatchOw:restoreBattleContinuation(trainer, {
|
||||
kind = "trainer_encounter", map = "OTHER_MAP", npcId = trainerNpc.id,
|
||||
trainerClass = trainer.oppClass, partyIndex = 1,
|
||||
}) == false, "continuation from another map is rejected")
|
||||
T.check(mismatchOw:restoreBattleContinuation(trainer, {
|
||||
kind = "trainer_encounter", map = "FIX_TOWN", npcId = trainerNpc.id,
|
||||
trainerClass = "OPP_OTHER", partyIndex = 1,
|
||||
}) == false, "mismatched trainer identity is rejected")
|
||||
|
||||
local ow = {}
|
||||
local stack = { states = { ow } }
|
||||
function stack:top() return self.states[#self.states] end
|
||||
local game = setmetatable({ overworld = ow, stack = stack }, { __index = GameMethods })
|
||||
local entered, resumed = false, false
|
||||
local battle = {
|
||||
enter = function() entered = true end,
|
||||
resumeCheckpoint = function() resumed = true end,
|
||||
}
|
||||
game:restoreCheckpointBattle(battle)
|
||||
T.check(game.stack:top() == battle, "reconstructed battle is installed on stack")
|
||||
T.check(resumed == true, "checkpoint-specific battle resume path runs")
|
||||
T.check(entered == false, "ordinary battle intro is not replayed")
|
||||
|
||||
T.finish()
|
||||
@@ -8,6 +8,8 @@ local T = require("tests.harness").suite("battle checkpoint restore")
|
||||
local Fixtures = require("tests.modkit").fixtures
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
local Checkpoint = require("src.core.Checkpoint")
|
||||
local GameMethods = require("src.core.Game")
|
||||
local Music = require("src.core.Music")
|
||||
local Pokemon = require("src.pokemon.Pokemon")
|
||||
local SaveData = require("src.core.SaveData")
|
||||
local StateStack = require("src.core.StateStack")
|
||||
@@ -15,6 +17,8 @@ local StateStack = require("src.core.StateStack")
|
||||
local Data = Fixtures.fresh()
|
||||
local oldRandom = love.math.random
|
||||
local oldGet, oldSet = love.math.getRandomState, love.math.setRandomState
|
||||
local oldPlayBattle = Music.playBattle
|
||||
Music.playBattle = function() end
|
||||
local rng = 12345
|
||||
love.math.getRandomState = function() return tostring(rng) end
|
||||
love.math.setRandomState = function(state) rng = assert(tonumber(state)) end
|
||||
@@ -59,7 +63,9 @@ local function makeGame(kind)
|
||||
end
|
||||
return true
|
||||
end
|
||||
local game = { data = Data, save = save, stack = stack, overworld = overworld }
|
||||
local game = setmetatable(
|
||||
{ data = Data, save = save, stack = stack, overworld = overworld },
|
||||
{ __index = GameMethods })
|
||||
function game:restoreCheckpointSave(loaded)
|
||||
self.save = loaded
|
||||
self.overworld.map = { id = loaded.player.map }
|
||||
@@ -73,15 +79,13 @@ local function makeGame(kind)
|
||||
self.overworld.parallelQueue, self.overworld.scriptMoves = {}, {}
|
||||
self.stack.states = { self.overworld }
|
||||
end
|
||||
function game:restoreCheckpointBattle(battle)
|
||||
self.stack.states[#self.stack.states + 1] = battle
|
||||
end
|
||||
stack.states[1] = overworld
|
||||
local battle
|
||||
if kind == "trainer" then
|
||||
battle = BattleState.newTrainer(game, "OPP_FIX_YOUNGSTER", 1)
|
||||
battle.checkpointOrigin = {
|
||||
kind = "trainer_encounter", map = "FIX_TOWN", npc = "TRAINER_1",
|
||||
kind = "trainer_encounter", map = "FIX_TOWN", npcId = "TRAINER_1",
|
||||
trainerClass = "OPP_FIX_YOUNGSTER", partyIndex = 1,
|
||||
event = "EVENT_BEAT_TRAINER_1",
|
||||
}
|
||||
else
|
||||
@@ -89,6 +93,7 @@ local function makeGame(kind)
|
||||
battle.checkpointOrigin = { kind = "wild_encounter", map = "FIX_TOWN" }
|
||||
end
|
||||
battle.phase, battle.queue = "menu", {}
|
||||
battle.musicKind = battle:computeMusicKind()
|
||||
battle.onFinish = function() end
|
||||
stack.states[2] = battle
|
||||
return game, battle
|
||||
@@ -168,4 +173,5 @@ end
|
||||
|
||||
love.math.random = oldRandom
|
||||
love.math.getRandomState, love.math.setRandomState = oldGet, oldSet
|
||||
Music.playBattle = oldPlayBattle
|
||||
T.finish()
|
||||
|
||||
Reference in New Issue
Block a user