feat(mods): expose contextual field items

This commit is contained in:
AverageConsumer
2026-08-14 21:39:18 +02:00
parent 797a6bebfe
commit dcc388a942
8 changed files with 267 additions and 4 deletions
+2 -1
View File
@@ -737,7 +737,8 @@ COVERAGE["src.pokemon.Boxes"] = {
COVERAGE["src.world.WorldAPI"] = {
kind = "alias", target = "src.world.gen2.WorldAPI",
backed = "new __index overworld current mapOverview warpTo toggleObject replaceBlock "
.. "spawnNpc removeNpc npc queueScript invalidateMap",
.. "spawnNpc removeNpc npc queueScript invalidateMap "
.. "availableFieldActions useFieldAction",
warned = "setFlag getFlag",
absent = "",
notes = {
+27
View File
@@ -763,6 +763,27 @@ function OverworldState:bikeAllowed(mapId)
return false
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
-- data/maps/dungeon_maps.asm (field.dungeonTransitionMaps): singles plus
-- inclusive map-id ranges -- faithful to the original's omissions
@@ -1726,6 +1747,12 @@ function OverworldState:goFishing(rod)
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).
function OverworldState:flyTo(mapId)
local spot = Game.data.field.flyWarps[mapId]
+55
View File
@@ -15,6 +15,7 @@ local WorldAPI = {}
WorldAPI.__index = WorldAPI
local NO_OVERWORLD = "no overworld"
local RODS = { "OLD_ROD", "GOOD_ROD", "SUPER_ROD" }
local function acceptsMenuInput(game, ow)
local stack = game and game.stack
@@ -86,6 +87,60 @@ function WorldAPI:reorderParty(fromSlot, toSlot)
return true
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.
-- `rows` describes collision terrain; optional `tileRows` reduces each real
-- 8x8 map tile to its average Game Boy shade ("0" lightest, "3" darkest).
+75
View File
@@ -27,11 +27,15 @@ local Movement = require("src.script.gen2.Movement")
local Runtime = require("src.mods.Runtime")
local HiddenItems = require("src.world.gen2.HiddenItems")
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 = {}
WorldAPI.__index = WorldAPI
local NO_OVERWORLD = "no overworld"
local RODS = { "OLD_ROD", "GOOD_ROD", "SUPER_ROD" }
function WorldAPI.new(game, modId)
return setmetatable({ game = game, modId = modId }, WorldAPI)
@@ -52,6 +56,77 @@ function WorldAPI:current()
facing = p and p.facing }
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
-- visibility rules supplying the semantic markers.
function WorldAPI:mapOverview()