mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-15 07:41:21 +02:00
Merge pull request #1302 from AverageConsumer/feat/mod-field-items
feat(mods): expose contextual field items
This commit is contained in:
@@ -240,7 +240,7 @@ resolves to the weaker claim:
|
|||||||
| `warned` | present, answers nil or degrades, and names itself once with the mod attributed |
|
| `warned` | present, answers nil or degrades, and names itself once with the mod attributed |
|
||||||
| `absent` | deliberately not served; a nil read is the honest failure |
|
| `absent` | deliberately not served; a nil read is the honest failure |
|
||||||
|
|
||||||
Today that is 288 backed, 32 warned and 161 absent across the fifteen modules.
|
Today that is 291 backed, 32 warned and 161 absent across the fifteen modules.
|
||||||
`notes` keys are documentation topics rather than a member list -- dotted paths
|
`notes` keys are documentation topics rather than a member list -- dotted paths
|
||||||
(`save.money`), field names (`warpAt`), hook names (`hook ui.pc.items`) and
|
(`save.money`), field names (`warpAt`), hook names (`hook ui.pc.items`) and
|
||||||
bare topics (`identity`, `iteration`, `rawset`) all appear there. `members` is
|
bare topics (`identity`, `iteration`, `rawset`) all appear there. `members` is
|
||||||
@@ -486,6 +486,9 @@ has its own entry points for (`start_battle "wild" species level`, `warp`,
|
|||||||
**by name, before the first row runs**, so a mod never gets a half-run queue.
|
**by name, before the first row runs**, so a mod never gets a half-run queue.
|
||||||
`marchInPlace` still has no Gen 2 equivalent (the Gen 2 movement stream has no
|
`marchInPlace` still has no Gen 2 equivalent (the Gen 2 movement stream has no
|
||||||
byte for it) and returns `nil, reason` rather than approximating one.
|
byte for it) and returns `nil, reason` rather than approximating one.
|
||||||
|
`availableFieldActions` and `useFieldAction` expose the same contextual
|
||||||
|
bicycle and fishing records in both games. Each engine keeps ownership of its
|
||||||
|
inventory, terrain, surfing, bike, and fishing rules.
|
||||||
|
|
||||||
**Hooks and events that fire on Gold.** Every name below is the Gen 1 name
|
**Hooks and events that fire on Gold.** Every name below is the Gen 1 name
|
||||||
carrying the Gen 1 payload keys, because Gold's call sites reuse them rather
|
carrying the Gen 1 payload keys, because Gold's call sites reuse them rather
|
||||||
|
|||||||
@@ -147,6 +147,20 @@ Companion UIs and alternate party screens can call
|
|||||||
operation is accepted only during idle overworld play; menus, movement,
|
operation is accepted only during idle overworld play; menus, movement,
|
||||||
scripts, battles, and transitions leave the party untouched.
|
scripts, battles, and transitions leave the party untouched.
|
||||||
|
|
||||||
|
## Contextual field items
|
||||||
|
|
||||||
|
`mod.world:availableFieldActions()` returns the field items that can start at
|
||||||
|
the player's current position. Red and Gold currently expose `bicycle` and
|
||||||
|
`fish`; fishing rows include the owned rods that are valid choices. The list
|
||||||
|
is empty while the world is busy, while riding states or terrain forbid an
|
||||||
|
action, or when the required item is not owned.
|
||||||
|
|
||||||
|
Call `mod.world:useFieldAction(id, opts)` to perform a listed action through
|
||||||
|
the active game's own field-item path. Fishing accepts `{ rod = "OLD_ROD" }`
|
||||||
|
and chooses automatically when only one rod is available. Invalid, stale, and
|
||||||
|
busy requests return `nil` plus a reason without changing game state. Mods do
|
||||||
|
not need generation-specific bike, collision, or fishing logic.
|
||||||
|
|
||||||
## Rendering pipelines
|
## Rendering pipelines
|
||||||
|
|
||||||
Most registries hand the engine *content*. `render_pipelines` hands it
|
Most registries hand the engine *content*. `render_pipelines` hands it
|
||||||
|
|||||||
@@ -325,7 +325,7 @@ This is not a dev-mode feature; it installs on any Gold boot that has mods.
|
|||||||
| `src.pokemon.Boxes` | facade | over `src/core/gen2/Boxes.lua` | 22 / 0 / 0 |
|
| `src.pokemon.Boxes` | facade | over `src/core/gen2/Boxes.lua` | 22 / 0 / 0 |
|
||||||
| `src.battle.BattleState` | facade | over `src/ui/gen2/BattleState.lua` | 16 / 2 / 39 |
|
| `src.battle.BattleState` | facade | over `src/ui/gen2/BattleState.lua` | 16 / 2 / 39 |
|
||||||
| `src.ui.PartyMenu` | facade | over `src/ui/gen2/PartyMenu.lua` | 15 / 2 / 16 |
|
| `src.ui.PartyMenu` | facade | over `src/ui/gen2/PartyMenu.lua` | 15 / 2 / 16 |
|
||||||
| `src.world.WorldAPI` | alias | `src/world/gen2/WorldAPI.lua` | 12 / 2 / 0 |
|
| `src.world.WorldAPI` | alias | `src/world/gen2/WorldAPI.lua` | 15 / 2 / 0 |
|
||||||
| `src.world.PikachuFollower` | alias | `src/world/gen2/Follower.lua` | 10 / 0 / 11 |
|
| `src.world.PikachuFollower` | alias | `src/world/gen2/Follower.lua` | 10 / 0 / 11 |
|
||||||
| `src.script.ScriptRunner` | facade | over `src/script/gen2/Vm.lua` | 10 / 7 / 1 |
|
| `src.script.ScriptRunner` | facade | over `src/script/gen2/Vm.lua` | 10 / 7 / 1 |
|
||||||
| `src.ui.OptionsMenu` | facade | over `src/ui/gen2/OptionsMenu.lua` | 8 / 0 / 1 |
|
| `src.ui.OptionsMenu` | facade | over `src/ui/gen2/OptionsMenu.lua` | 8 / 0 / 1 |
|
||||||
@@ -774,7 +774,7 @@ profile to test in, and `POKEPORT_DEV=1` adds the console and `F5` hot reload.
|
|||||||
|
|
||||||
- **Coverage is partial and will stay partial.** 15 Gen 1 modules are served
|
- **Coverage is partial and will stay partial.** 15 Gen 1 modules are served
|
||||||
out of a much larger engine, and within those 15 the coverage table records
|
out of a much larger engine, and within those 15 the coverage table records
|
||||||
288 backed members against 32 warned and 161 absent. The absent ones are not
|
291 backed members against 32 warned and 161 absent. The absent ones are not
|
||||||
a backlog; most are absent because there is no honest Gen 2 answer, and each
|
a backlog; most are absent because there is no honest Gen 2 answer, and each
|
||||||
one carries its reason. The counts move as the adapter learns something: a
|
one carries its reason. The counts move as the adapter learns something: a
|
||||||
member that turns out to answer nil is demoted from backed to warned or
|
member that turns out to answer nil is demoted from backed to warned or
|
||||||
|
|||||||
@@ -737,7 +737,8 @@ COVERAGE["src.pokemon.Boxes"] = {
|
|||||||
COVERAGE["src.world.WorldAPI"] = {
|
COVERAGE["src.world.WorldAPI"] = {
|
||||||
kind = "alias", target = "src.world.gen2.WorldAPI",
|
kind = "alias", target = "src.world.gen2.WorldAPI",
|
||||||
backed = "new __index overworld current mapOverview warpTo toggleObject replaceBlock "
|
backed = "new __index overworld current mapOverview warpTo toggleObject replaceBlock "
|
||||||
.. "spawnNpc removeNpc npc queueScript invalidateMap",
|
.. "spawnNpc removeNpc npc queueScript invalidateMap "
|
||||||
|
.. "availableFieldActions useFieldAction",
|
||||||
warned = "setFlag getFlag",
|
warned = "setFlag getFlag",
|
||||||
absent = "",
|
absent = "",
|
||||||
notes = {
|
notes = {
|
||||||
|
|||||||
@@ -763,6 +763,27 @@ function OverworldState:bikeAllowed(mapId)
|
|||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Field-item entry points keep presentation and state transitions in the
|
||||||
|
-- owning world instead of asking a supported facade to reproduce either one.
|
||||||
|
function OverworldState:useBicycle()
|
||||||
|
local name = Game.save.player.name
|
||||||
|
if Game.save.onBike then
|
||||||
|
if Game.save.forcedBike then return false end
|
||||||
|
Game.save.onBike = false
|
||||||
|
require("src.core.Music").playMap(Game.data, self.map.id, false)
|
||||||
|
Game.stack:push(TextBox.new(Game,
|
||||||
|
Strings("%s got off\nthe BICYCLE.", name)))
|
||||||
|
elseif self:bikeAllowed(self.map.id) and not self.player.surfing then
|
||||||
|
Game.save.onBike = true
|
||||||
|
require("src.core.Music").playMap(Game.data, self.map.id, true)
|
||||||
|
Game.stack:push(TextBox.new(Game,
|
||||||
|
Strings("%s got on\nthe BICYCLE!", name)))
|
||||||
|
else
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
-- The battle transition's dungeon wipe uses the explicit map lists in
|
-- The battle transition's dungeon wipe uses the explicit map lists in
|
||||||
-- data/maps/dungeon_maps.asm (field.dungeonTransitionMaps): singles plus
|
-- data/maps/dungeon_maps.asm (field.dungeonTransitionMaps): singles plus
|
||||||
-- inclusive map-id ranges -- faithful to the original's omissions
|
-- inclusive map-id ranges -- faithful to the original's omissions
|
||||||
@@ -1726,6 +1747,12 @@ function OverworldState:goFishing(rod)
|
|||||||
end))
|
end))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function OverworldState:useFishingRod(rod)
|
||||||
|
if self.player.surfing or not self:facingIsShoreOrWater() then return false end
|
||||||
|
self:goFishing(rod)
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
-- Fly to a visited town (called from the party menu).
|
-- Fly to a visited town (called from the party menu).
|
||||||
function OverworldState:flyTo(mapId)
|
function OverworldState:flyTo(mapId)
|
||||||
local spot = Game.data.field.flyWarps[mapId]
|
local spot = Game.data.field.flyWarps[mapId]
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ local WorldAPI = {}
|
|||||||
WorldAPI.__index = WorldAPI
|
WorldAPI.__index = WorldAPI
|
||||||
|
|
||||||
local NO_OVERWORLD = "no overworld"
|
local NO_OVERWORLD = "no overworld"
|
||||||
|
local RODS = { "OLD_ROD", "GOOD_ROD", "SUPER_ROD" }
|
||||||
|
|
||||||
local function acceptsMenuInput(game, ow)
|
local function acceptsMenuInput(game, ow)
|
||||||
local stack = game and game.stack
|
local stack = game and game.stack
|
||||||
@@ -86,6 +87,60 @@ function WorldAPI:reorderParty(fromSlot, toSlot)
|
|||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Contextual field-item shortcuts. Only actions that can start immediately
|
||||||
|
-- are listed; callers receive copied labels and never inspect world internals.
|
||||||
|
function WorldAPI:availableFieldActions()
|
||||||
|
local game, ow, out = self.game, self:overworld(), {}
|
||||||
|
if not (game and game.save and ow and ow.map and ow.player)
|
||||||
|
or not acceptsMenuInput(game, ow) then return out end
|
||||||
|
local save, inventory = game.save, game.save.inventory or {}
|
||||||
|
local items = game.data and game.data.items or {}
|
||||||
|
|
||||||
|
if (inventory.BICYCLE or 0) > 0 and not ow.player.surfing
|
||||||
|
and not (save.onBike and save.forcedBike)
|
||||||
|
and (save.onBike or ow:bikeAllowed(ow.map.id)) then
|
||||||
|
out[#out + 1] = { id = "bicycle",
|
||||||
|
label = save.onBike and "BIKE OFF" or "BICYCLE" }
|
||||||
|
end
|
||||||
|
|
||||||
|
if not ow.player.surfing and ow:facingIsShoreOrWater() then
|
||||||
|
local rods = {}
|
||||||
|
for _, id in ipairs(RODS) do
|
||||||
|
if (inventory[id] or 0) > 0 then
|
||||||
|
local def = items[id]
|
||||||
|
rods[#rods + 1] = { id = id, label = def and def.name or id }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #rods > 0 then
|
||||||
|
out[#out + 1] = { id = "fish", label = "FISH", rods = rods }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
function WorldAPI:useFieldAction(id, opts)
|
||||||
|
local game, ow = self.game, self:overworld()
|
||||||
|
if not ow then return nil, NO_OVERWORLD end
|
||||||
|
if not acceptsMenuInput(game, ow) then return nil, "world is busy" end
|
||||||
|
local found
|
||||||
|
for _, action in ipairs(self:availableFieldActions()) do
|
||||||
|
if action.id == id then found = action break end
|
||||||
|
end
|
||||||
|
if not found then return nil, "field action unavailable" end
|
||||||
|
|
||||||
|
if id == "bicycle" then
|
||||||
|
if ow:useBicycle() then return true end
|
||||||
|
elseif id == "fish" then
|
||||||
|
local rod = opts and opts.rod
|
||||||
|
if not rod and #found.rods == 1 then rod = found.rods[1].id end
|
||||||
|
for _, choice in ipairs(found.rods) do
|
||||||
|
if choice.id == rod and ow:useFishingRod(rod) then return true end
|
||||||
|
end
|
||||||
|
return nil, "fishing rod unavailable"
|
||||||
|
end
|
||||||
|
return nil, "field action unavailable"
|
||||||
|
end
|
||||||
|
|
||||||
-- A compact, read-only view of the active map for minimaps and companion UIs.
|
-- A compact, read-only view of the active map for minimaps and companion UIs.
|
||||||
-- `rows` describes collision terrain; optional `tileRows` reduces each real
|
-- `rows` describes collision terrain; optional `tileRows` reduces each real
|
||||||
-- 8x8 map tile to its average Game Boy shade ("0" lightest, "3" darkest).
|
-- 8x8 map tile to its average Game Boy shade ("0" lightest, "3" darkest).
|
||||||
|
|||||||
@@ -27,11 +27,15 @@ local Movement = require("src.script.gen2.Movement")
|
|||||||
local Runtime = require("src.mods.Runtime")
|
local Runtime = require("src.mods.Runtime")
|
||||||
local HiddenItems = require("src.world.gen2.HiddenItems")
|
local HiddenItems = require("src.world.gen2.HiddenItems")
|
||||||
local MapOverview = require("src.world.MapOverview")
|
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 WorldAPI = {}
|
local WorldAPI = {}
|
||||||
WorldAPI.__index = WorldAPI
|
WorldAPI.__index = WorldAPI
|
||||||
|
|
||||||
local NO_OVERWORLD = "no overworld"
|
local NO_OVERWORLD = "no overworld"
|
||||||
|
local RODS = { "OLD_ROD", "GOOD_ROD", "SUPER_ROD" }
|
||||||
|
|
||||||
function WorldAPI.new(game, modId)
|
function WorldAPI.new(game, modId)
|
||||||
return setmetatable({ game = game, modId = modId }, WorldAPI)
|
return setmetatable({ game = game, modId = modId }, WorldAPI)
|
||||||
@@ -52,6 +56,77 @@ function WorldAPI:current()
|
|||||||
facing = p and p.facing }
|
facing = p and p.facing }
|
||||||
end
|
end
|
||||||
|
|
||||||
|
local function itemLabel(game, id)
|
||||||
|
local def = game and game.data and game.data.items
|
||||||
|
and game.data.items[id]
|
||||||
|
return (def and def.name) or id
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The same field-item contract as Gen 1, resolved through Gold's own bike,
|
||||||
|
-- collision and fishing rules.
|
||||||
|
function WorldAPI:availableFieldActions()
|
||||||
|
local world, game, out = self:overworld(), self.game, {}
|
||||||
|
if not (world and game and game.save and world.map and world.player)
|
||||||
|
or not world:acceptsMenuInput() then return out end
|
||||||
|
local inventory = game.save.inventory or {}
|
||||||
|
|
||||||
|
if (inventory.BICYCLE or 0) > 0 then
|
||||||
|
local bike = Bike.tryBike({
|
||||||
|
state = world.playerState,
|
||||||
|
environment = world.map.def and world.map.def.environment,
|
||||||
|
collision = world:playerCollision(),
|
||||||
|
alwaysOnBike = world:alwaysOnBike(),
|
||||||
|
})
|
||||||
|
if bike == "mount" or bike == "dismount" then
|
||||||
|
out[#out + 1] = { id = "bicycle",
|
||||||
|
label = bike == "dismount" and "BIKE OFF" or "BICYCLE" }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local context = world:fieldContext()
|
||||||
|
if not FieldMoves.isSurfing(world.playerState)
|
||||||
|
and Permissions.isWater(context.facingColl) then
|
||||||
|
local rods = {}
|
||||||
|
for _, id in ipairs(RODS) do
|
||||||
|
if (inventory[id] or 0) > 0 then
|
||||||
|
rods[#rods + 1] = { id = id, label = itemLabel(game, id) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if #rods > 0 then
|
||||||
|
out[#out + 1] = { id = "fish", label = "FISH", rods = rods }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return out
|
||||||
|
end
|
||||||
|
|
||||||
|
function WorldAPI:useFieldAction(id, opts)
|
||||||
|
local world = self:overworld()
|
||||||
|
if not world then return nil, NO_OVERWORLD end
|
||||||
|
if not world:acceptsMenuInput() then return nil, "world is busy" end
|
||||||
|
local found
|
||||||
|
for _, action in ipairs(self:availableFieldActions()) do
|
||||||
|
if action.id == id then found = action break end
|
||||||
|
end
|
||||||
|
if not found then return nil, "field action unavailable" end
|
||||||
|
|
||||||
|
if id == "bicycle" then
|
||||||
|
local outcome = world:useFieldItem("BICYCLE")
|
||||||
|
if outcome and outcome ~= "nowhere" then return true end
|
||||||
|
elseif id == "fish" then
|
||||||
|
local rod = opts and opts.rod
|
||||||
|
if not rod and #found.rods == 1 then rod = found.rods[1].id end
|
||||||
|
for _, choice in ipairs(found.rods) do
|
||||||
|
if choice.id == rod then
|
||||||
|
local outcome = world:useFieldItem(rod)
|
||||||
|
if outcome and outcome ~= "nowhere" then return true end
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
return nil, "fishing rod unavailable"
|
||||||
|
end
|
||||||
|
return nil, "field action unavailable"
|
||||||
|
end
|
||||||
|
|
||||||
-- The same read-only minimap contract as Gen 1, with Gold's object/event
|
-- The same read-only minimap contract as Gen 1, with Gold's object/event
|
||||||
-- visibility rules supplying the semantic markers.
|
-- visibility rules supplying the semantic markers.
|
||||||
function WorldAPI:mapOverview()
|
function WorldAPI:mapOverview()
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
-- Contextual bicycle and fishing actions share one public contract in both
|
||||||
|
-- generations while each engine keeps ownership of its own field-item path.
|
||||||
|
|
||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local T = require("tests.harness").suite("mod world field items")
|
||||||
|
|
||||||
|
local facingWater = false
|
||||||
|
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,
|
||||||
|
useBicycle = function(self) self.bikeUsed = true return true end,
|
||||||
|
useFishingRod = function(self, rod) self.rodUsed = rod return true end,
|
||||||
|
}
|
||||||
|
local redGame = {
|
||||||
|
data = { 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",
|
||||||
|
"Red keeps field-item 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")
|
||||||
|
|
||||||
|
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,
|
||||||
|
fieldContext = function() return { facingColl = 0x20 } end,
|
||||||
|
useFieldItem = function(self, item) self.itemUsed = item return "used" end,
|
||||||
|
}
|
||||||
|
local goldGame = {
|
||||||
|
data = { items = { OLD_ROD = { name = "OLD ROD" } } },
|
||||||
|
save = { inventory = { BICYCLE = 1, OLD_ROD = 1 } },
|
||||||
|
world = goldWorld,
|
||||||
|
}
|
||||||
|
|
||||||
|
local GoldAPI = require("src.world.gen2.WorldAPI")
|
||||||
|
local gold = GoldAPI.new(goldGame, "fixture")
|
||||||
|
actions = gold:availableFieldActions()
|
||||||
|
T.eq(actions[1].id, "bicycle", "Gold shares the bicycle action id")
|
||||||
|
T.eq(actions[2].rods[1].id, "OLD_ROD", "Gold shares the rod shape")
|
||||||
|
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.finish()
|
||||||
Reference in New Issue
Block a user