mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-15 07:41:21 +02:00
fix(mod-api): harden deferred trainer preparation
This commit is contained in:
+7
-2
@@ -436,7 +436,7 @@ mod.hooks:wrap("trainer.before_battle", function(next, game, context, continue)
|
|||||||
continue({ playerPartyIndices = indices })
|
continue({ playerPartyIndices = indices })
|
||||||
end,
|
end,
|
||||||
onCancel = function()
|
onCancel = function()
|
||||||
continue()
|
continue({ cancel = true })
|
||||||
end,
|
end,
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
@@ -444,7 +444,12 @@ end)
|
|||||||
```
|
```
|
||||||
|
|
||||||
Return `true` only when retaining `continue` for a later callback. Calling
|
Return `true` only when retaining `continue` for a later callback. Calling
|
||||||
`continue()` uses the full save party; passing
|
`continue({ cancel = true })` ends the encounter without constructing a battle;
|
||||||
|
the normal encounter completion callback returns control to the overworld and
|
||||||
|
no trainer-defeated state is written. A cancelled sight encounter is suppressed
|
||||||
|
at the current player cell so it cannot immediately reopen; moving one cell or
|
||||||
|
talking to the trainer permits a new challenge. Calling `continue()` uses the
|
||||||
|
full save party; passing
|
||||||
`{ playerPartyIndices = { 2, 4, 5 } }` uses those ordered, one-based party
|
`{ playerPartyIndices = { 2, 4, 5 } }` uses those ordered, one-based party
|
||||||
members for initial send, switching and forced replacement, exhaustion,
|
members for initial send, switching and forced replacement, exhaustion,
|
||||||
experience traversal, and battle party displays. The continuation is one-shot.
|
experience traversal, and battle party displays. The continuation is one-shot.
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ Add the guarded hook:
|
|||||||
mod.hooks:wrap("trainer.before_battle", function(next, game, context, continue)
|
mod.hooks:wrap("trainer.before_battle", function(next, game, context, continue)
|
||||||
-- context = { trainerClass, partyIndex, mapId, npcId }
|
-- context = { trainerClass, partyIndex, mapId, npcId }
|
||||||
-- Return true only when the battle has been deferred.
|
-- Return true only when the battle has been deferred.
|
||||||
|
-- continue({ cancel = true }) returns without constructing a battle.
|
||||||
-- Call continue() for the full save party, or:
|
-- Call continue() for the full save party, or:
|
||||||
-- continue({ playerPartyIndices = { 2, 4, 5 } })
|
-- continue({ playerPartyIndices = { 2, 4, 5 } })
|
||||||
end)
|
end)
|
||||||
@@ -57,6 +58,13 @@ exhaustion/blackout checks, participant and EXP.ALL traversal, party counts,
|
|||||||
and party-ball presentation. Checkpoints preserve the index list and rebuild
|
and party-ball presentation. Checkpoints preserve the index list and rebuild
|
||||||
the same view before restoring battlers.
|
the same view before restoring battlers.
|
||||||
|
|
||||||
|
`{ cancel = true }` ends a deferred encounter through its normal completion
|
||||||
|
callback without constructing a battle or writing trainer-defeated state. A
|
||||||
|
cancelled sight encounter is suppressed while the player remains on the same
|
||||||
|
cell, preventing immediate reacquisition; moving or directly talking permits a
|
||||||
|
new challenge. Cancellation is also one-shot; if supplied alongside a party
|
||||||
|
index list, cancellation wins.
|
||||||
|
|
||||||
The API sets no maximum, chooses no members, identifies no boss, and contains
|
The API sets no maximum, chooses no members, identifies no boss, and contains
|
||||||
no scaling or challenge policy.
|
no scaling or challenge policy.
|
||||||
|
|
||||||
|
|||||||
@@ -738,7 +738,7 @@ function BattleState.newTrainer(game, oppClass, partyIndex, opts)
|
|||||||
-- data/scripts/victories.lua on class#party, so keep it on the battle (#782).
|
-- data/scripts/victories.lua on class#party, so keep it on the battle (#782).
|
||||||
self.partyIndex = partyIndex or 1
|
self.partyIndex = partyIndex or 1
|
||||||
self.playerParty, self.playerPartyIndices = scopedPlayerParty(game,
|
self.playerParty, self.playerPartyIndices = scopedPlayerParty(game,
|
||||||
opts and opts.playerPartyIndices)
|
type(opts) == "table" and opts.playerPartyIndices or nil)
|
||||||
self.trainer = game.data.trainers[oppClass]
|
self.trainer = game.data.trainers[oppClass]
|
||||||
assert(self.trainer, "unknown trainer class " .. tostring(oppClass))
|
assert(self.trainer, "unknown trainer class " .. tostring(oppClass))
|
||||||
-- pret GetTrainerName_: RIVAL1/2/3 copy wRivalName into wTrainerName
|
-- pret GetTrainerName_: RIVAL1/2/3 copy wRivalName into wTrainerName
|
||||||
|
|||||||
@@ -383,6 +383,7 @@ local function pickTargetAndUse(game, battle, id, list)
|
|||||||
local def = game.data.items[id]
|
local def = game.data.items[id]
|
||||||
local opts = {
|
local opts = {
|
||||||
pickOnly = true,
|
pickOnly = true,
|
||||||
|
battle = battle,
|
||||||
-- HP medicine animates its bar with the picker still up (#252). Only
|
-- HP medicine animates its bar with the picker still up (#252). Only
|
||||||
-- out of battle: the in-battle tail closes the bag list underneath
|
-- out of battle: the in-battle tail closes the bag list underneath
|
||||||
-- first, which needs the picker already gone.
|
-- first, which needs the picker already gone.
|
||||||
|
|||||||
@@ -230,6 +230,7 @@ function OverworldState:enter(mapId, x, y, facing, opts)
|
|||||||
-- a fresh entry, or a stale flag can freeze player input forever
|
-- a fresh entry, or a stale flag can freeze player input forever
|
||||||
self.engaging = false
|
self.engaging = false
|
||||||
self.emote = nil
|
self.emote = nil
|
||||||
|
self.cancelledTrainerSight = nil
|
||||||
-- volatile WRAM state in pokered; never serialize across save/load
|
-- volatile WRAM state in pokered; never serialize across save/load
|
||||||
self.wildEncounterGraceSteps = 0
|
self.wildEncounterGraceSteps = 0
|
||||||
-- survives save/load: a loaded game may start inside a building whose
|
-- survives save/load: a loaded game may start inside a building whose
|
||||||
@@ -3095,7 +3096,8 @@ end
|
|||||||
-- Public pre-trainer gate. A mod may retain continueBattle while a registered
|
-- Public pre-trainer gate. A mod may retain continueBattle while a registered
|
||||||
-- preparation screen is on top, then resume once with an optional ordered
|
-- preparation screen is on top, then resume once with an optional ordered
|
||||||
-- save-party index scope. The hook is cold on a no-mod boot.
|
-- save-party index scope. The hook is cold on a no-mod boot.
|
||||||
function OverworldState.prepareTrainerBattle(game, context, startBattle)
|
function OverworldState.prepareTrainerBattle(game, context, startBattle,
|
||||||
|
cancelBattle)
|
||||||
if not Runtime.wantsHook("trainer.before_battle") then
|
if not Runtime.wantsHook("trainer.before_battle") then
|
||||||
startBattle()
|
startBattle()
|
||||||
return false
|
return false
|
||||||
@@ -3104,7 +3106,11 @@ function OverworldState.prepareTrainerBattle(game, context, startBattle)
|
|||||||
local function continueBattle(options)
|
local function continueBattle(options)
|
||||||
if started then return false end
|
if started then return false end
|
||||||
started = true
|
started = true
|
||||||
startBattle(options)
|
if type(options) == "table" and options.cancel == true then
|
||||||
|
if cancelBattle then cancelBattle() end
|
||||||
|
else
|
||||||
|
startBattle(options)
|
||||||
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
local deferred = Runtime.call("trainer.before_battle",
|
local deferred = Runtime.call("trainer.before_battle",
|
||||||
@@ -3141,6 +3147,7 @@ function OverworldState:engageTrainer(npc, onDone, endBattleText, skipBattleText
|
|||||||
|
|
||||||
local BattleState = require("src.battle.BattleState")
|
local BattleState = require("src.battle.BattleState")
|
||||||
local function startBattle(options)
|
local function startBattle(options)
|
||||||
|
self.cancelledTrainerSight = nil
|
||||||
-- TalkToTrainer (home/trainers.asm:88) prints the before-battle text
|
-- TalkToTrainer (home/trainers.asm:88) prints the before-battle text
|
||||||
-- FIRST and only then runs `call EngageMapTrainer` / `jp
|
-- FIRST and only then runs `call EngageMapTrainer` / `jp
|
||||||
-- StartTrainerBattle`, so a trainer challenged on foot gets the sting
|
-- StartTrainerBattle`, so a trainer challenged on foot gets the sting
|
||||||
@@ -3203,7 +3210,16 @@ function OverworldState:engageTrainer(npc, onDone, endBattleText, skipBattleText
|
|||||||
partyIndex = d.trainerParty or 1,
|
partyIndex = d.trainerParty or 1,
|
||||||
mapId = self.map.id,
|
mapId = self.map.id,
|
||||||
npcId = npc.id,
|
npcId = npc.id,
|
||||||
}, startBattle)
|
}, startBattle, function()
|
||||||
|
if self.player then
|
||||||
|
self.cancelledTrainerSight = {
|
||||||
|
npcId = npc.id,
|
||||||
|
playerX = self.player.cellX,
|
||||||
|
playerY = self.player.cellY,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
if onDone then onDone() end
|
||||||
|
end)
|
||||||
end
|
end
|
||||||
if skipBattleText then
|
if skipBattleText then
|
||||||
prepareBattle()
|
prepareBattle()
|
||||||
@@ -3406,11 +3422,18 @@ function OverworldState:checkTrainerSight()
|
|||||||
if self.player.moving or self.engaging then return end
|
if self.player.moving or self.engaging then return end
|
||||||
if Game.stack:top() ~= self then return end
|
if Game.stack:top() ~= self then return end
|
||||||
local p = self.player
|
local p = self.player
|
||||||
|
local cancelled = self.cancelledTrainerSight
|
||||||
|
if cancelled and (cancelled.playerX ~= p.cellX
|
||||||
|
or cancelled.playerY ~= p.cellY) then
|
||||||
|
self.cancelledTrainerSight = nil
|
||||||
|
cancelled = nil
|
||||||
|
end
|
||||||
for _, npc in ipairs(self.npcs) do
|
for _, npc in ipairs(self.npcs) do
|
||||||
local d = npc.def
|
local d = npc.def
|
||||||
-- CheckFightingMapTrainers engages ANY aligned trainer sprite,
|
-- CheckFightingMapTrainers engages ANY aligned trainer sprite,
|
||||||
-- walkers included (they sight between steps)
|
-- walkers included (they sight between steps)
|
||||||
if d.trainerClass and not npc.moving
|
if d.trainerClass and not npc.moving
|
||||||
|
and not (cancelled and cancelled.npcId == npc.id)
|
||||||
and not self:trainerDefeated(npc)
|
and not self:trainerDefeated(npc)
|
||||||
and not mapScripts.talkScript(self.map.id, d.text)
|
and not mapScripts.talkScript(self.map.id, d.text)
|
||||||
and trainerSpriteOnScreen(npc, p) then
|
and trainerSpriteOnScreen(npc, p) then
|
||||||
|
|||||||
@@ -6,12 +6,16 @@ love = love or require("tests.love_stub")
|
|||||||
|
|
||||||
local T = require("tests.harness").suite("trainer battle party scope")
|
local T = require("tests.harness").suite("trainer battle party scope")
|
||||||
local BattleState = require("src.battle.BattleState")
|
local BattleState = require("src.battle.BattleState")
|
||||||
|
local BagMenu = require("src.ui.BagMenu")
|
||||||
local Fixtures = require("tests.modkit").fixtures
|
local Fixtures = require("tests.modkit").fixtures
|
||||||
local PartyMenu = require("src.ui.PartyMenu")
|
local PartyMenu = require("src.ui.PartyMenu")
|
||||||
local Pokemon = require("src.pokemon.Pokemon")
|
local Pokemon = require("src.pokemon.Pokemon")
|
||||||
local SaveData = require("src.core.SaveData")
|
local SaveData = require("src.core.SaveData")
|
||||||
|
local Bag = require("src.inventory.Bag")
|
||||||
|
|
||||||
local Data = Fixtures.fresh()
|
local Data = Fixtures.fresh()
|
||||||
|
Data.items.POTION = { id = "POTION", index = 99, name = "POTION",
|
||||||
|
price = 300, tossable = true }
|
||||||
|
|
||||||
local function makeGame()
|
local function makeGame()
|
||||||
local save = SaveData.newGame()
|
local save = SaveData.newGame()
|
||||||
@@ -20,9 +24,11 @@ local function makeGame()
|
|||||||
Pokemon.new(Data, "FIXMON_B", 11),
|
Pokemon.new(Data, "FIXMON_B", 11),
|
||||||
Pokemon.new(Data, "FIXMON_C", 12),
|
Pokemon.new(Data, "FIXMON_C", 12),
|
||||||
}
|
}
|
||||||
return { data = Data, save = save, stack = {
|
local stack = { states = {} }
|
||||||
push = function() end, pop = function() end, top = function() end,
|
function stack:push(value) self.states[#self.states + 1] = value end
|
||||||
} }
|
function stack:pop() return table.remove(self.states) end
|
||||||
|
function stack:top() return self.states[#self.states] end
|
||||||
|
return { data = Data, save = save, stack = stack }
|
||||||
end
|
end
|
||||||
|
|
||||||
local game = makeGame()
|
local game = makeGame()
|
||||||
@@ -47,6 +53,18 @@ local menu = PartyMenu.new(game, { battle = battle })
|
|||||||
T.check(menu.party == battle.playerParty,
|
T.check(menu.party == battle.playerParty,
|
||||||
"battle party menus traverse only the local eligible view")
|
"battle party menus traverse only the local eligible view")
|
||||||
|
|
||||||
|
Bag.add(game.save, "POTION", 1)
|
||||||
|
local bag = BagMenu.new(game, { battle = battle })
|
||||||
|
local potion
|
||||||
|
for _, row in ipairs(bag.items) do
|
||||||
|
if row.value == "POTION" then potion = row; break end
|
||||||
|
end
|
||||||
|
T.check(potion ~= nil, "the fixture potion is available for target selection")
|
||||||
|
bag.onChoose(potion, bag)
|
||||||
|
local targetPicker = game.stack:top()
|
||||||
|
T.check(targetPicker and targetPicker.party == battle.playerParty,
|
||||||
|
"in-battle item target selection traverses only eligible members")
|
||||||
|
|
||||||
second.hp = 0
|
second.hp = 0
|
||||||
battle.player.mon.hp = 0
|
battle.player.mon.hp = 0
|
||||||
battle:playerMonFainted()
|
battle:playerMonFainted()
|
||||||
@@ -94,6 +112,12 @@ local duplicate = BattleState.newTrainer(duplicateGame,
|
|||||||
T.eq(duplicate.playerParty, nil,
|
T.eq(duplicate.playerParty, nil,
|
||||||
"duplicate members make the entire scope fall back")
|
"duplicate members make the entire scope fall back")
|
||||||
|
|
||||||
|
local malformedOptionsGame = makeGame()
|
||||||
|
local malformedOptions = BattleState.newTrainer(malformedOptionsGame,
|
||||||
|
"OPP_FIX_YOUNGSTER", 1, 7)
|
||||||
|
T.eq(malformedOptions.playerParty, nil,
|
||||||
|
"a malformed options value degrades to the vanilla full-party path")
|
||||||
|
|
||||||
local linkGame = makeGame()
|
local linkGame = makeGame()
|
||||||
local linkBattle = BattleState.newTrainer(linkGame,
|
local linkBattle = BattleState.newTrainer(linkGame,
|
||||||
"OPP_FIX_YOUNGSTER", 1, { playerPartyIndices = { 2, 3 } })
|
"OPP_FIX_YOUNGSTER", 1, { playerPartyIndices = { 2, 3 } })
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ package.path = "./?.lua;./?/init.lua;" .. package.path
|
|||||||
|
|
||||||
local T = require("tests.modkit")
|
local T = require("tests.modkit")
|
||||||
local OW = require("src.world.OverworldController")
|
local OW = require("src.world.OverworldController")
|
||||||
|
local Hooks = require("src.mods.Hooks")
|
||||||
|
local Runtime = require("src.mods.Runtime")
|
||||||
|
|
||||||
local function setUpvalue(fn, name, val)
|
local function setUpvalue(fn, name, val)
|
||||||
local i = 1
|
local i = 1
|
||||||
@@ -96,6 +98,89 @@ T.eq(rivalCount, 0, "rival classes play no encounter sting here")
|
|||||||
local _, seenCount = stingFor("OPP_LASS", true)
|
local _, seenCount = stingFor("OPP_LASS", true)
|
||||||
T.eq(seenCount, 0, "self.engaging suppresses a second sting")
|
T.eq(seenCount, 0, "self.engaging suppresses a second sting")
|
||||||
|
|
||||||
|
-- A deferred preparation may cancel instead of constructing a battle. For a
|
||||||
|
-- sight trainer, that must leave a one-position latch: otherwise the still
|
||||||
|
-- undefeated adjacent trainer sees the stationary player again next frame and
|
||||||
|
-- immediately reopens the preparation screen.
|
||||||
|
local oldEvents, oldHooks, oldErrors = Runtime.events, Runtime.hooks,
|
||||||
|
Runtime.errors
|
||||||
|
local cancelHooks = Hooks.new()
|
||||||
|
Runtime.install(oldEvents, cancelHooks, oldErrors)
|
||||||
|
cancelHooks:wrap("trainer.before_battle", function(_, _, _, continue)
|
||||||
|
continue({ cancel = true })
|
||||||
|
return true
|
||||||
|
end, 0, "cancel_probe")
|
||||||
|
local cancelNpc = { id = "npc#cancel", cellX = 0, cellY = -1,
|
||||||
|
facing = "down", moving = false, def = { trainerClass = "OPP_LASS",
|
||||||
|
trainerParty = 1, index = 1 } }
|
||||||
|
fakeSelf.player = { cellX = 0, cellY = 0, moving = false }
|
||||||
|
fakeSelf.map.id = "FIX_ROUTE"
|
||||||
|
fakeSelf.engaging = false
|
||||||
|
pushed, plays = {}, {}
|
||||||
|
local completed = 0
|
||||||
|
fakeSelf:engageTrainer(cancelNpc, function() completed = completed + 1 end)
|
||||||
|
pushed[1].onDone()
|
||||||
|
T.eq(completed, 1, "cancel completes the deferred encounter without a battle")
|
||||||
|
T.same(fakeSelf.cancelledTrainerSight, {
|
||||||
|
npcId = "npc#cancel", playerX = 0, playerY = 0,
|
||||||
|
}, "cancel suppresses immediate sight re-entry at the current player cell")
|
||||||
|
|
||||||
|
local approaches = 0
|
||||||
|
fakeSelf.npcs = { cancelNpc }
|
||||||
|
fakeSelf.trainerDefeated = function() return false end
|
||||||
|
fakeSelf.startTrainerApproach = function() approaches = approaches + 1 end
|
||||||
|
fakeGame.stack.top = function() return fakeSelf end
|
||||||
|
fakeGame.data.trainerHeader = function() return { range = 2 } end
|
||||||
|
T.check(setUpvalue(OW.checkTrainerSight, "mapScripts", {
|
||||||
|
talkScript = function() return nil end,
|
||||||
|
}), "mapScripts upvalue on checkTrainerSight")
|
||||||
|
fakeSelf:checkTrainerSight()
|
||||||
|
T.eq(approaches, 0,
|
||||||
|
"a cancelled adjacent trainer cannot reacquire the stationary player")
|
||||||
|
fakeSelf.player.cellX = 1
|
||||||
|
fakeSelf:checkTrainerSight()
|
||||||
|
T.eq(fakeSelf.cancelledTrainerSight, nil,
|
||||||
|
"moving one cell releases the cancelled sight latch")
|
||||||
|
fakeSelf.player.cellX = 0
|
||||||
|
fakeSelf:checkTrainerSight()
|
||||||
|
T.eq(approaches, 1,
|
||||||
|
"returning to the sight line permits a fresh trainer challenge")
|
||||||
|
Runtime.install(oldEvents, oldHooks, oldErrors)
|
||||||
|
|
||||||
|
-- OverworldState is a singleton reused by StateStack. A title/load cycle must
|
||||||
|
-- clear this volatile latch too, or CONTINUE at the same map and cell inherits
|
||||||
|
-- the cancelled sight suppression from the previous session.
|
||||||
|
local Camera = require("src.render.Camera")
|
||||||
|
local Collision = require("src.world.Collision")
|
||||||
|
local Encounter = require("src.world.Encounter")
|
||||||
|
local ScriptRunner = require("src.script.ScriptRunner")
|
||||||
|
local oldCameraNew, oldCollisionLoad = Camera.new, Collision.load
|
||||||
|
local oldEncounterLoad, oldRunnerNew = Encounter.load, ScriptRunner.new
|
||||||
|
local oldGameModule = package.loaded["src.core.Game"]
|
||||||
|
local oldScriptsModule = package.loaded["data.scripts.init"]
|
||||||
|
Camera.new = function() return {} end
|
||||||
|
Collision.load = function() end
|
||||||
|
Encounter.load = function() end
|
||||||
|
ScriptRunner.new = function() return {} end
|
||||||
|
package.loaded["src.core.Game"] = {
|
||||||
|
data = {}, save = { lastOutdoor = "FIX_ROUTE" },
|
||||||
|
}
|
||||||
|
package.loaded["data.scripts.init"] = {}
|
||||||
|
local lifecycle = setmetatable({
|
||||||
|
cancelledTrainerSight = {
|
||||||
|
npcId = "FIX_ROUTE_obj_1", playerX = 0, playerY = 0,
|
||||||
|
},
|
||||||
|
setMap = function() end,
|
||||||
|
refreshStandingOnWarp = function() end,
|
||||||
|
}, { __index = OW })
|
||||||
|
lifecycle:enter("FIX_ROUTE", 0, 0, "down", { via = "boot" })
|
||||||
|
T.eq(lifecycle.cancelledTrainerSight, nil,
|
||||||
|
"fresh overworld entry clears a cancelled trainer sight latch")
|
||||||
|
Camera.new, Collision.load = oldCameraNew, oldCollisionLoad
|
||||||
|
Encounter.load, ScriptRunner.new = oldEncounterLoad, oldRunnerNew
|
||||||
|
package.loaded["src.core.Game"] = oldGameModule
|
||||||
|
package.loaded["data.scripts.init"] = oldScriptsModule
|
||||||
|
|
||||||
if realMusic ~= nil then package.loaded["src.core.Music"] = realMusic
|
if realMusic ~= nil then package.loaded["src.core.Music"] = realMusic
|
||||||
else package.loaded["src.core.Music"] = nil end
|
else package.loaded["src.core.Music"] = nil end
|
||||||
if realBattle ~= nil then package.loaded["src.battle.BattleState"] = realBattle
|
if realBattle ~= nil then package.loaded["src.battle.BattleState"] = realBattle
|
||||||
|
|||||||
@@ -68,4 +68,25 @@ T.eq(out.continue({ playerPartyIndices = { 1 } }), false,
|
|||||||
T.eq(calls, 1, "a duplicate resume cannot start a second battle")
|
T.eq(calls, 1, "a duplicate resume cannot start a second battle")
|
||||||
run.release()
|
run.release()
|
||||||
|
|
||||||
|
local cancelRun = T.sdk.loadMods({ "mods/scope_probe" }, {
|
||||||
|
fs = T.sdk.memfs(FIXTURE),
|
||||||
|
})
|
||||||
|
local starts, cancels = 0, 0
|
||||||
|
OW.prepareTrainerBattle(game, {
|
||||||
|
trainerClass = "OPP_FIX_YOUNGSTER", partyIndex = 2,
|
||||||
|
mapId = "FIX_ROUTE", npcId = "TRAINER_7",
|
||||||
|
}, function()
|
||||||
|
starts = starts + 1
|
||||||
|
end, function()
|
||||||
|
cancels = cancels + 1
|
||||||
|
end)
|
||||||
|
local cancelOut = cancelRun.loader.exports.scope_probe or {}
|
||||||
|
T.eq(cancelOut.continue({ cancel = true }), true,
|
||||||
|
"the retained continuation can cancel a deferred encounter")
|
||||||
|
T.eq(starts, 0, "cancelling never constructs a trainer battle")
|
||||||
|
T.eq(cancels, 1, "cancelling invokes the encounter's completion callback")
|
||||||
|
T.eq(cancelOut.continue(), false,
|
||||||
|
"a cancelled continuation remains one-shot")
|
||||||
|
cancelRun.release()
|
||||||
|
|
||||||
T.finish("trainer_before_battle")
|
T.finish("trainer_before_battle")
|
||||||
|
|||||||
Reference in New Issue
Block a user