mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-21 05:00:43 +02:00
feat: expose settled battle checkpoint boundary
This commit is contained in:
+60
-3
@@ -4,6 +4,7 @@
|
|||||||
local SaveSerializer = require("src.core.SaveSerializer")
|
local SaveSerializer = require("src.core.SaveSerializer")
|
||||||
local SaveData = require("src.core.SaveData")
|
local SaveData = require("src.core.SaveData")
|
||||||
local Version = require("src.core.Version")
|
local Version = require("src.core.Version")
|
||||||
|
local BattleState = require("src.battle.BattleState")
|
||||||
|
|
||||||
local Checkpoint = {}
|
local Checkpoint = {}
|
||||||
|
|
||||||
@@ -27,6 +28,61 @@ local function nonempty(value)
|
|||||||
return type(value) == "table" and next(value) ~= nil
|
return type(value) == "table" and next(value) ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function scriptsBusy(ow)
|
||||||
|
return running(ow.runner) or nonempty(ow.parallelRunners)
|
||||||
|
or nonempty(ow.pendingScripts) or nonempty(ow.parallelQueue)
|
||||||
|
or nonempty(ow.scriptMoves)
|
||||||
|
end
|
||||||
|
|
||||||
|
local BATTLE_BUSY_FIELDS = {
|
||||||
|
"current", "afterQueue", "nextInsert", "pendingHit", "waitingUI",
|
||||||
|
"waitingSound", "waitFrames", "draining", "animPlaying", "growIn",
|
||||||
|
"introSlide", "ghostReveal", "mimicCtx", "mimicMoves", "result",
|
||||||
|
}
|
||||||
|
|
||||||
|
local function inspectBattle(ow, battle)
|
||||||
|
if battle.kind == "link" then
|
||||||
|
return refusal("battle", "link_battle_unsupported",
|
||||||
|
"Network battles cannot be checkpointed.")
|
||||||
|
end
|
||||||
|
if battle.safari or battle.ghost or battle.scopeReveal or battle.demo
|
||||||
|
or battle.noCatch then
|
||||||
|
return refusal("battle", "battle_variant_unsupported",
|
||||||
|
"This battle variant does not have a checkpoint contract.")
|
||||||
|
end
|
||||||
|
if battle.kind ~= "wild" and battle.kind ~= "trainer" then
|
||||||
|
return refusal("battle", "battle_variant_unsupported",
|
||||||
|
"This battle kind does not have a checkpoint contract.")
|
||||||
|
end
|
||||||
|
local origin = battle.checkpointOrigin
|
||||||
|
local expectedOrigin = battle.kind == "wild" and "wild_encounter"
|
||||||
|
or "trainer_encounter"
|
||||||
|
if type(origin) ~= "table" or origin.kind ~= expectedOrigin then
|
||||||
|
return refusal("battle", "battle_origin_unsupported",
|
||||||
|
"The battle completion path cannot be reconstructed safely.")
|
||||||
|
end
|
||||||
|
if scriptsBusy(ow) then
|
||||||
|
return refusal("battle", "script_busy",
|
||||||
|
"A suspended or queued script cannot be checkpointed.")
|
||||||
|
end
|
||||||
|
if battle.phase ~= "menu" or nonempty(battle.queue) then
|
||||||
|
return refusal("battle", "battle_phase_busy",
|
||||||
|
"Wait for the player command menu before creating a checkpoint.")
|
||||||
|
end
|
||||||
|
for _, field in ipairs(BATTLE_BUSY_FIELDS) do
|
||||||
|
if battle[field] ~= nil and battle[field] ~= false then
|
||||||
|
return refusal("battle", "battle_phase_busy",
|
||||||
|
"Wait for the current battle action to finish.")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not battle.player or not battle.enemy or battle.player.mon.hp <= 0
|
||||||
|
or (battle.menuLockedAction and battle:menuLockedAction(battle.player)) then
|
||||||
|
return refusal("battle", "battle_phase_busy",
|
||||||
|
"Wait for an ordinary player decision before creating a checkpoint.")
|
||||||
|
end
|
||||||
|
return { canCapture = true, canRestore = true, kind = "battle" }
|
||||||
|
end
|
||||||
|
|
||||||
function Checkpoint.inspect(game)
|
function Checkpoint.inspect(game)
|
||||||
local save = game and game.save
|
local save = game and game.save
|
||||||
if type(save) ~= "table" or type(save.version) ~= "string" then
|
if type(save) ~= "table" or type(save.version) ~= "string" then
|
||||||
@@ -41,6 +97,9 @@ function Checkpoint.inspect(game)
|
|||||||
"Only a settled overworld can be checkpointed.")
|
"Only a settled overworld can be checkpointed.")
|
||||||
end
|
end
|
||||||
local top = game.stack and game.stack.top and game.stack:top()
|
local top = game.stack and game.stack.top and game.stack:top()
|
||||||
|
if getmetatable(top) == BattleState then
|
||||||
|
return inspectBattle(ow, top)
|
||||||
|
end
|
||||||
if top ~= ow then
|
if top ~= ow then
|
||||||
return refusal("overworld", "screen_busy",
|
return refusal("overworld", "screen_busy",
|
||||||
"Close the active menu or screen before creating a checkpoint.")
|
"Close the active menu or screen before creating a checkpoint.")
|
||||||
@@ -57,9 +116,7 @@ function Checkpoint.inspect(game)
|
|||||||
return refusal("overworld", "transition_busy",
|
return refusal("overworld", "transition_busy",
|
||||||
"Wait for the map transition to finish.")
|
"Wait for the map transition to finish.")
|
||||||
end
|
end
|
||||||
if running(ow.runner) or nonempty(ow.parallelRunners)
|
if scriptsBusy(ow) then
|
||||||
or nonempty(ow.pendingScripts) or nonempty(ow.parallelQueue)
|
|
||||||
or nonempty(ow.scriptMoves) then
|
|
||||||
return refusal("overworld", "script_busy",
|
return refusal("overworld", "script_busy",
|
||||||
"Wait for the active or queued script to finish.")
|
"Wait for the active or queued script to finish.")
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
-- Battle checkpoints are exposed only at a settled, reconstructable player
|
||||||
|
-- decision boundary. This suite is ROM-free and exercises the public engine
|
||||||
|
-- checkpoint capability against the fixture battle implementation.
|
||||||
|
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
love = love or require("tests.love_stub")
|
||||||
|
|
||||||
|
local T = require("tests.harness").suite("battle checkpoint boundary")
|
||||||
|
local Fixtures = require("tests.modkit").fixtures
|
||||||
|
local BattleState = require("src.battle.BattleState")
|
||||||
|
local Checkpoint = require("src.core.Checkpoint")
|
||||||
|
local Pokemon = require("src.pokemon.Pokemon")
|
||||||
|
local SaveData = require("src.core.SaveData")
|
||||||
|
local StateStack = require("src.core.StateStack")
|
||||||
|
|
||||||
|
local Data = Fixtures.fresh()
|
||||||
|
|
||||||
|
local function makeGame()
|
||||||
|
local save = SaveData.newGame()
|
||||||
|
save.meta.playthroughId = "battle-playthrough"
|
||||||
|
save.party = { Pokemon.new(Data, "FIXMON_A", 20) }
|
||||||
|
local stack = setmetatable({ states = {} }, { __index = StateStack })
|
||||||
|
local overworld = {
|
||||||
|
map = { id = save.player.map },
|
||||||
|
player = {
|
||||||
|
cellX = save.player.x, cellY = save.player.y,
|
||||||
|
facing = save.player.facing, surfing = false,
|
||||||
|
},
|
||||||
|
runner = { isRunning = function() return false end },
|
||||||
|
parallelRunners = {}, pendingScripts = {}, parallelQueue = {}, scriptMoves = {},
|
||||||
|
}
|
||||||
|
local game = { data = Data, save = save, stack = stack, overworld = overworld }
|
||||||
|
stack.states[1] = overworld
|
||||||
|
local battle = BattleState.newWild(game, "FIXMON_B", 12)
|
||||||
|
battle.phase = "menu"
|
||||||
|
battle.queue = {}
|
||||||
|
battle.checkpointOrigin = { kind = "wild_encounter" }
|
||||||
|
battle.onFinish = function() end
|
||||||
|
stack.states[2] = battle
|
||||||
|
return game, overworld, battle
|
||||||
|
end
|
||||||
|
|
||||||
|
local game, overworld, battle = makeGame()
|
||||||
|
T.same(Checkpoint.inspect(game), {
|
||||||
|
canCapture = true, canRestore = true, kind = "battle",
|
||||||
|
}, "settled standard wild battle is a checkpoint boundary")
|
||||||
|
|
||||||
|
local function refused(mutator, code, label)
|
||||||
|
local game2, ow2, battle2 = makeGame()
|
||||||
|
mutator(game2, ow2, battle2)
|
||||||
|
local capability = Checkpoint.inspect(game2)
|
||||||
|
T.check(capability.canCapture == false and capability.reason == code,
|
||||||
|
label .. ": " .. tostring(capability.reason))
|
||||||
|
end
|
||||||
|
|
||||||
|
refused(function(_, _, b) b.phase = "messages" end,
|
||||||
|
"battle_phase_busy", "message phase is rejected")
|
||||||
|
refused(function(_, _, b) b.queue = { { text = "busy" } } end,
|
||||||
|
"battle_phase_busy", "nonempty action queue is rejected")
|
||||||
|
refused(function(_, _, b) b.waitFrames = 1 end,
|
||||||
|
"battle_phase_busy", "partial wait is rejected")
|
||||||
|
refused(function(_, _, b) b.player.mustRecharge = true end,
|
||||||
|
"battle_phase_busy", "automatic locked action is rejected")
|
||||||
|
refused(function(_, ow) ow.runner = { isRunning = function() return true end } end,
|
||||||
|
"script_busy", "suspended script beneath battle is rejected")
|
||||||
|
refused(function(_, _, b) b.checkpointOrigin = nil end,
|
||||||
|
"battle_origin_unsupported", "unknown completion closure is rejected")
|
||||||
|
refused(function(_, _, b) b.safari = { balls = 30, steps = 10 } end,
|
||||||
|
"battle_variant_unsupported", "Safari battle is rejected")
|
||||||
|
refused(function(_, _, b) b.ghost = true end,
|
||||||
|
"battle_variant_unsupported", "ghost battle is rejected")
|
||||||
|
refused(function(_, _, b) b.demo = true end,
|
||||||
|
"battle_variant_unsupported", "old-man demo is rejected")
|
||||||
|
refused(function(_, _, b) b.kind = "link" end,
|
||||||
|
"link_battle_unsupported", "link battle is rejected")
|
||||||
|
|
||||||
|
-- Ordinary overworld behavior remains unchanged by the battle branch.
|
||||||
|
game.stack.states[2] = nil
|
||||||
|
T.same(Checkpoint.inspect(game), {
|
||||||
|
canCapture = true, canRestore = true, kind = "overworld",
|
||||||
|
}, "settled overworld remains supported")
|
||||||
|
|
||||||
|
T.finish()
|
||||||
Reference in New Issue
Block a user