Files
gen1recomp/tests/modkit/cases/world_field_items.lua
T
2026-08-15 02:01:20 +02:00

174 lines
7.4 KiB
Lua

-- Contextual field actions share one public contract in both generations
-- while each engine keeps ownership of its own field-item and move paths.
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness").suite("mod world field actions")
local facingWater = false
local redCut, redSurf = false, "no_water"
local redMoves = {}
local redWorld = {
isOverworld = true,
map = { id = "ROUTE_1", def = { tileset = "OVERWORLD" } },
player = { moving = false, inputLocked = false, surfing = false },
runner = { isRunning = function() return false end },
scriptMoves = {},
bikeAllowed = function() return true end,
facingIsShoreOrWater = function() return facingWater end,
useCutFieldMove = function() return redCut and "ok" or "nothing" end,
useSurfFieldMove = function() return redSurf end,
partyKnows = function(_, move) return redMoves[move] end,
useBicycle = function(self) self.bikeUsed = true return true end,
useFishingRod = function(self, rod) self.rodUsed = rod return true end,
}
local redGame = {
data = { field = { outsideTilesets = { "OVERWORLD" } },
items = { OLD_ROD = { name = "OLD ROD" } } },
save = { player = { name = "RED" }, party = {},
inventory = { BICYCLE = 1, OLD_ROD = 1 } },
stack = { states = { redWorld } },
overworld = redWorld,
}
function redGame.stack:top() return self.states[#self.states] end
local RedAPI = require("src.world.WorldAPI")
local red = RedAPI.new(redGame, "fixture")
local RedWorld = require("src.world.OverworldController")
T.check(type(RedWorld.useBicycle) == "function"
and type(RedWorld.useFishingRod) == "function"
and type(RedWorld.useFlashFieldMove) == "function"
and type(RedWorld.useStrengthFieldMove) == "function"
and type(RedWorld.stopSurfing) == "function",
"Red keeps field-action execution in its world")
local actions = red:availableFieldActions()
T.eq(actions[1].id, "bicycle", "Red lists an owned usable bicycle")
T.check(red:useFieldAction("bicycle"), "Red accepts the listed bicycle")
T.check(redWorld.bikeUsed, "Red delegates to its world-owned bicycle path")
facingWater = true
actions = red:availableFieldActions()
T.eq(actions[2].rods[1].id, "OLD_ROD", "Red lists owned rods at water")
T.check(red:useFieldAction("fish", { rod = "OLD_ROD" }),
"Red accepts a listed rod")
T.eq(redWorld.rodUsed, "OLD_ROD", "Red delegates to its fishing path")
local used = redWorld.rodUsed
local ok, err = red:useFieldAction("fish", { rod = "SUPER_ROD" })
T.check(not ok and err == "fishing rod unavailable",
"Red rejects an unowned rod")
T.eq(redWorld.rodUsed, used, "a rejected Red rod changes nothing")
redWorld.player.moving = true
T.eq(#red:availableFieldActions(), 0, "Red hides actions while moving")
ok, err = red:useFieldAction("bicycle")
T.check(not ok and err == "world is busy",
"Red refuses a stale action while busy")
redWorld.player.moving = false
facingWater, redCut, redSurf = false, true, "ok"
redWorld.dark = true
for _, move in ipairs({ "STRENGTH", "FLASH", "TELEPORT" }) do
redMoves[move] = { species = "MEW", moves = { { id = move } } }
end
redWorld.player.facingCell = function() return 4, 5 end
redWorld.tryCut = function(self) self.cutUsed = true return true end
redWorld.trySurf = function(self) self.surfUsed = true end
redWorld.useStrengthFieldMove = function(self) self.strengthUsed = true return true end
redWorld.useFlashFieldMove = function(self) self.flashUsed = true return true end
redWorld.beginTeleportOut = function(self) self.teleportUsed = true end
actions = red:availableFieldActions()
local byId = {}
for _, action in ipairs(actions) do byId[action.id] = action end
T.check(byId.cut and byId.surf and byId.strength and byId.flash
and byId.teleport, "Red lists field moves that can start now")
for _, id in ipairs({ "cut", "surf", "strength", "flash", "teleport" }) do
T.check(red:useFieldAction(id), "Red accepts listed " .. id)
end
T.check(redWorld.cutUsed and redWorld.surfUsed and redWorld.strengthUsed
and redWorld.flashUsed and redWorld.teleportUsed,
"Red delegates every move to its overworld path")
redSurf = "dismount"
redWorld.player.surfing = true
redWorld.stopSurfing = function(self) self.dismounted = true end
T.check(red:useFieldAction("surf") and redWorld.dismounted,
"Red delegates the contextual SURF dismount")
redWorld.player.surfing, redSurf = false, "ok"
redCut = false
ok, err = red:useFieldAction("cut")
T.check(not ok and err == "field action unavailable",
"Red revalidates a stale field move")
redWorld.map.id = "ROCK_TUNNEL_1F"
redWorld.map.def.tileset = "CAVERN"
redMoves.DIG = { species = "MEW", moves = { { id = "DIG" } } }
redWorld.beginTeleportOut = function(self) self.digUsed = true end
byId = {}
for _, action in ipairs(red:availableFieldActions()) do byId[action.id] = action end
T.check(byId.dig and not byId.teleport,
"Red distinguishes dungeon DIG from outdoor TELEPORT")
T.check(red:useFieldAction("dig") and redWorld.digUsed,
"Red delegates DIG to its escape path")
local goldWorld = {
map = { id = "ROUTE_29", def = { environment = "ROUTE" } },
player = {}, playerState = "normal",
acceptsMenuInput = function() return true end,
playerCollision = function() return 0x00 end,
alwaysOnBike = function() return false end,
useFieldItem = function(self, item) self.itemUsed = item return "used" end,
squirtbottleTreeScript = function() return { { op = "end" } } end,
useFieldMove = function(self, move)
self.moveUsed = move
return { ok = true }
end,
}
local goldGame = {
data = { items = { OLD_ROD = { name = "OLD ROD" },
SQUIRTBOTTLE = { name = "SQUIRTBOTTLE" } } },
save = { inventory = { BICYCLE = 1, OLD_ROD = 1, SQUIRTBOTTLE = 1 },
player = { badges = { FOG = true } },
party = { { moves = { { id = "SURF" }, { id = "SWEET_SCENT" },
{ id = "TELEPORT" } } } } },
world = goldWorld,
}
goldWorld.fieldContext = function(_, mon) return {
save = goldGame.save, party = goldGame.save.party, mon = mon,
facing = "right", facingColl = 0x29, playerColl = 0,
environment = "ROUTE", playerState = "normal", alwaysOnBike = false,
dark = false, canEscapeRope = false,
} end
local GoldAPI = require("src.world.gen2.WorldAPI")
local gold = GoldAPI.new(goldGame, "fixture")
actions = gold:availableFieldActions()
byId = {}
for _, action in ipairs(actions) do byId[action.id] = action end
T.eq(actions[1].id, "bicycle", "Gold shares the bicycle action id")
T.eq(actions[2].id, "fish", "Gold preserves the original action order")
T.check(byId.surf and byId.sweet_scent and byId.teleport,
"Gold lists field moves through its generic dispatcher")
T.eq(byId.fish.rods[1].id, "OLD_ROD", "Gold shares the rod shape")
T.check(byId.squirtbottle,
"Gold lists the SquirtBottle only at its matching tree")
T.check(gold:useFieldAction("sweet_scent"),
"Gold accepts a listed field move")
T.eq(goldWorld.moveUsed, "SWEET_SCENT",
"Gold delegates moves to its own field-move path")
T.check(gold:useFieldAction("fish", { rod = "OLD_ROD" }),
"Gold accepts the same fishing request")
T.eq(goldWorld.itemUsed, "OLD_ROD",
"Gold delegates to its own field-item path")
used = goldWorld.itemUsed
ok, err = gold:useFieldAction("fish", { rod = "SUPER_ROD" })
T.check(not ok and err == "fishing rod unavailable",
"Gold rejects an unowned rod")
T.eq(goldWorld.itemUsed, used, "a rejected Gold rod changes nothing")
T.check(gold:useFieldAction("squirtbottle"),
"Gold accepts the contextual SquirtBottle")
T.eq(goldWorld.itemUsed, "SQUIRTBOTTLE",
"Gold delegates the SquirtBottle to its field-item path")
T.finish()