diff --git a/src/world/gen2/WorldAPI.lua b/src/world/gen2/WorldAPI.lua index b5f1dbd9..52bdcfe6 100644 --- a/src/world/gen2/WorldAPI.lua +++ b/src/world/gen2/WorldAPI.lua @@ -30,6 +30,7 @@ local MapOverview = require("src.world.MapOverview") local Bike = require("src.world.gen2.Bike") local FieldMoves = require("src.world.gen2.FieldMoves") local Permissions = require("src.world.gen2.Permissions") +local Mail = require("src.core.gen2.Mail") local WorldAPI = {} WorldAPI.__index = WorldAPI @@ -49,6 +50,11 @@ local FIELD_ACTIONS = { { id = "teleport", move = "TELEPORT" }, } +local function validPartySlot(party, slot) + return type(slot) == "number" and slot == math.floor(slot) + and party[slot] ~= nil +end + function WorldAPI.new(game, modId) return setmetatable({ game = game, modId = modId }, WorldAPI) end @@ -68,6 +74,32 @@ function WorldAPI:current() facing = p and p.facing } end +-- Keep the public party-ordering contract identical across generations. +-- Gen 2 stores mail by party slot, so it must move with the Pokemon just as +-- the native PartyMenu's SwitchPartyMons path does. +function WorldAPI:canReorderParty() + local world, game = self:overworld(), self.game + local party = game and game.save and game.save.party or {} + return #party > 1 and world ~= nil and world:acceptsMenuInput() +end + +function WorldAPI:reorderParty(fromSlot, toSlot) + local world, game = self:overworld(), self.game + if not world then return nil, NO_OVERWORLD end + if not world:acceptsMenuInput() then return nil, "world is busy" end + local party = game.save and game.save.party or {} + if not validPartySlot(party, fromSlot) + or not validPartySlot(party, toSlot) then + return nil, "invalid party slot" + end + if fromSlot ~= toSlot then + party[fromSlot], party[toSlot] = party[toSlot], party[fromSlot] + Mail.swapSlots(game.save, fromSlot, toSlot) + require("src.core.Sound").play(game.data, "Sfx_SwitchPokemon") + end + return true +end + local function itemLabel(game, id) local def = game and game.data and game.data.items and game.data.items[id] diff --git a/tests/modkit/cases/world_party_reorder_gen2.lua b/tests/modkit/cases/world_party_reorder_gen2.lua new file mode 100644 index 00000000..35a4b030 --- /dev/null +++ b/tests/modkit/cases/world_party_reorder_gen2.lua @@ -0,0 +1,59 @@ +-- Gen 2 parity for the public party-ordering contract. The fixture also +-- carries slot-based mail because a reorder must move that state with its mon. + +package.path = "./?.lua;./?/init.lua;" .. package.path +love = love or require("tests.love_stub") + +local T = require("tests.harness").suite("mod world party reorder gen2") +local WorldAPI = require("src.world.gen2.WorldAPI") + +local first = { species = "CHIKORITA" } +local second = { species = "CYNDAQUIL" } +local firstMail = { message = "FIRST" } +local secondMail = { message = "SECOND" } +local world = { map = { id = "NEW_BARK_TOWN" }, accepts = true } +function world:acceptsMenuInput() return self.accepts end + +local game = { + data = { audio = { sfx = {} } }, + save = { + party = { first, second }, + mail = { party = { firstMail, secondMail }, box = {} }, + }, + world = world, +} +local api = WorldAPI.new(game, "fixture") + +T.check(api:canReorderParty(), "idle free roam allows party reordering") + +local Sound = require("src.core.Sound") +local realPlay, played = Sound.play +Sound.play = function(_, name) played = name end +T.check(api:reorderParty(1, 2) == true, "valid slots reorder") +Sound.play = realPlay +T.check(game.save.party[1] == second and game.save.party[2] == first, + "the live party is swapped") +T.check(game.save.mail.party[1] == secondMail + and game.save.mail.party[2] == firstMail, + "slot-based mail follows its Pokemon") +T.eq(played, "Sfx_SwitchPokemon", "the native Gen 2 swap sound is used") + +local value, err = api:reorderParty(1.5, 2) +T.check(value == nil and err == "invalid party slot", + "non-integer slots are rejected") +value, err = api:reorderParty("1", 2) +T.check(value == nil and err == "invalid party slot", + "string slots are rejected") + +world.accepts = false +T.check(not api:canReorderParty(), "busy free roam blocks reordering") +value, err = api:reorderParty(1, 2) +T.check(value == nil and err == "world is busy", + "reordering refuses while the world owns input") + +game.world = nil +value, err = api:reorderParty(1, 2) +T.check(value == nil and err == "no overworld", + "reordering outside the overworld fails closed") + +T.finish()