-- A sandboxed mod can defer an ordinary trainer engagement and later resume -- it with a battle-local player-party scope, using only public mod surfaces. package.path = "./?.lua;./?/init.lua;" .. package.path love = love or require("tests.love_stub") local T = require("tests.modkit") local OW = require("src.world.OverworldController") local FIXTURE = { ["mods/scope_probe/manifest.json"] = [[{ "id": "scope_probe", "name": "Scope Probe", "version": "1.0.0", "entry": "main.lua", "api": 2 }]], ["mods/scope_probe/main.lua"] = [[ local mod = ... mod.hooks:wrap("trainer.before_battle", function(next, game, context, continue) mod.exports.game = game mod.exports.context = context mod.exports.continue = continue return true end) ]], } local vanilla = T.sdk.loadNone({}) local vanillaCalls, vanillaOptions = 0 OW.prepareTrainerBattle({ id = "game" }, { trainerClass = "OPP_FIX_YOUNGSTER", partyIndex = 1, mapId = "FIX_ROUTE", npcId = "TRAINER_1", }, function(options) vanillaCalls, vanillaOptions = vanillaCalls + 1, options end) T.eq(vanillaCalls, 1, "no mod starts the trainer battle exactly once") T.eq(vanillaOptions, nil, "no mod supplies no battle-local party scope") vanilla.release() local run = T.sdk.loadMods({ "mods/scope_probe" }, { fs = T.sdk.memfs(FIXTURE), }) T.eq(#run.errors, 0, "the public preparation probe loads clean (" .. tostring(run.errors[1]) .. ")") local game = { id = "live-game" } local calls, options = 0 OW.prepareTrainerBattle(game, { trainerClass = "OPP_FIX_YOUNGSTER", partyIndex = 2, mapId = "FIX_ROUTE", npcId = "TRAINER_7", }, function(value) calls, options = calls + 1, value end) T.eq(calls, 0, "a claiming public hook defers battle construction") local out = run.loader.exports.scope_probe or {} T.check(out.game == game, "the hook receives the live game") T.same(out.context, { trainerClass = "OPP_FIX_YOUNGSTER", partyIndex = 2, mapId = "FIX_ROUTE", npcId = "TRAINER_7", }, "the hook receives data-only trainer identity context") T.eq(out.continue({ playerPartyIndices = { 2, 4 } }), true, "the retained continuation resumes the deferred battle") T.eq(calls, 1, "resume constructs the battle exactly once") T.same(options, { playerPartyIndices = { 2, 4 } }, "ordered eligible indices cross the public seam unchanged") T.eq(out.continue({ playerPartyIndices = { 1 } }), false, "the continuation refuses a second invocation") T.eq(calls, 1, "a duplicate resume cannot start a second battle") 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")