feat(mods): expose Fly and Softboiled field actions

This commit is contained in:
AverageConsumer
2026-08-16 14:57:39 +02:00
parent 1151c188a7
commit a3a20a07e1
5 changed files with 165 additions and 25 deletions
+2 -16
View File
@@ -628,22 +628,8 @@ function PartyMenu:update(dt)
end
if self.softboiledFrom then
local user = party[self.softboiledFrom]
local heal = math.floor(user.stats.hp / 5)
if mon == user or mon.hp <= 0 or mon.hp >= mon.stats.hp
or user.hp <= heal then
self.softboiledFrom = nil
local TextBox = require("src.render.TextBox")
self.game.stack:push(TextBox.new(self.game, Strings("It won't have\nany effect.")))
else
user.hp = user.hp - heal
mon.hp = math.min(mon.stats.hp, mon.hp + heal)
self.softboiledFrom = nil
require("src.core.Sound").play(self.game.data, "Heal_HP")
local def = self.game.data.pokemon[mon.species]
local TextBox = require("src.render.TextBox")
self.game.stack:push(TextBox.new(self.game,
Strings("%s's HP\nwas restored!", mon.nickname or def.name)))
end
self.softboiledFrom = nil
self.game.overworld:useSoftboiledFieldMove(user, mon)
elseif self.swapFrom then
if self.swapFrom ~= self.index then
party[self.swapFrom], party[self.index] = party[self.index], party[self.swapFrom]
+17
View File
@@ -827,6 +827,23 @@ function OverworldState:useStrengthFieldMove(mon, onClose)
return true
end
function OverworldState:useSoftboiledFieldMove(user, target)
local heal = user and user.stats and math.floor(user.stats.hp / 5) or 0
if not user or not user.stats or not target or not target.stats
or target == user or target.hp <= 0
or target.hp >= target.stats.hp or user.hp <= heal then
Game.stack:push(TextBox.new(Game, Strings("It won't have\nany effect.")))
return false
end
user.hp = user.hp - heal
target.hp = math.min(target.stats.hp, target.hp + heal)
require("src.core.Sound").play(Game.data, "Heal_HP")
local def = Game.data.pokemon[target.species]
Game.stack:push(TextBox.new(Game,
Strings("%s's HP\nwas restored!", target.nickname or def.name)))
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
+91 -2
View File
@@ -37,6 +37,57 @@ local function validPartySlot(party, slot)
and party[slot] ~= nil
end
local function outside(game, ow)
return Map.isOutside(ow.map.def,
FieldDefaults.field(game.data, "outsideTilesets"))
end
local function knows(mon, moveId)
for _, move in ipairs(mon.moves or {}) do
if move.id == moveId then return true end
end
return false
end
local function monInfo(game, mon, slot)
local def = game.data.pokemon[mon.species] or {}
return { slot = slot, species = mon.species,
name = mon.nickname or def.name or mon.species, level = mon.level,
hp = mon.hp, maxHp = mon.stats and mon.stats.hp or mon.hp }
end
local function softboiledSources(game)
local party, sources = game.save.party or {}, {}
for sourceSlot, source in ipairs(party) do
local heal = source.stats and math.floor(source.stats.hp / 5) or 0
if knows(source, "SOFTBOILED") and source.hp > heal then
local info = monInfo(game, source, sourceSlot)
info.targets = {}
for targetSlot, target in ipairs(party) do
if target ~= source and target.hp > 0 and target.stats
and target.hp < target.stats.hp then
info.targets[#info.targets + 1] = monInfo(game, target, targetSlot)
end
end
if #info.targets > 0 then sources[#sources + 1] = info end
end
end
return sources
end
local function flyDestinationAvailable(game, mapId)
local field, save = game.data.field or {}, game.save
for _, id in ipairs(field.flyOrder or {}) do
if id == mapId then
local def = game.data.maps and game.data.maps[id]
return not not (save.visited and save.visited[id]
and field.flyWarps and field.flyWarps[id]
and def and Map.isFlyTown(def))
end
end
return false
end
function WorldAPI.new(game, modId)
return setmetatable({ game = game, modId = modId }, WorldAPI)
end
@@ -141,10 +192,14 @@ function WorldAPI:availableFieldActions()
and ow:partyKnows("DIG") then
out[#out + 1] = { id = "dig", label = "DIG" }
end
if ow:partyKnows("TELEPORT") and Map.isOutside(ow.map.def,
FieldDefaults.field(game.data, "outsideTilesets")) then
if ow:partyKnows("TELEPORT") and outside(game, ow) then
out[#out + 1] = { id = "teleport", label = "TELEPORT" }
end
local sources = softboiledSources(game)
if #sources > 0 then
out[#out + 1] = { id = "softboiled", label = "SOFTBOILED",
sources = sources }
end
return out
end
@@ -187,10 +242,44 @@ function WorldAPI:useFieldAction(id, opts)
elseif id == "dig" or id == "teleport" then
ow:beginTeleportOut()
return true
elseif id == "softboiled" then
local sourceSlot = opts and tonumber(opts.sourceSlot)
local targetSlot = opts and tonumber(opts.targetSlot)
local allowed
for _, source in ipairs(found.sources or {}) do
if source.slot == sourceSlot then
for _, target in ipairs(source.targets or {}) do
if target.slot == targetSlot then allowed = true break end
end
end
end
if not allowed then return nil, "softboiled target unavailable" end
if ow:useSoftboiledFieldMove(game.save.party[sourceSlot],
game.save.party[targetSlot]) then return true end
end
return nil, "field action unavailable"
end
-- FLY needs a destination choice, so it is exposed separately from the
-- immediate actions above. The request is still checked against the same
-- visited-town list as the native Town Map picker before the world may warp.
function WorldAPI:canFly()
local game, ow = self.game, self:overworld()
return not not (ow and ow.map and outside(game, ow) and ow:partyKnows("FLY"))
end
function WorldAPI:flyTo(mapId)
local game, ow = self.game, self:overworld()
if not ow then return nil, NO_OVERWORLD end
if not self:canFly() then return nil, "fly unavailable" end
if not acceptsMenuInput(game, ow) then return nil, "world is busy" end
if not flyDestinationAvailable(game, mapId) then
return nil, "destination unavailable"
end
ow:flyTo(mapId)
return true
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).