Add a fieldmove.eligibility hook to partyKnows (#310)

Mods that widen field-move rules (use an HM without teaching it, a
rental mon) had to monkey-patch partyKnows, the one function every
field-move path funnels through. Wrap the vanilla check in a hook
instead: next_ is the whole badge-and-knows-move check, so a wrapper
that calls it first keeps vanilla answers winning and only fills the
cases vanilla denies. No hook, no behavior change.

Co-authored-by: johnjohto <johnjohto@users.noreply.github.com>
This commit is contained in:
johnjohto
2026-07-27 13:38:17 -04:00
committed by GitHub
parent f0a88ea473
commit 43158d724b
2 changed files with 45 additions and 1 deletions
+12 -1
View File
@@ -1241,7 +1241,7 @@ end
-- (constants.hmBadges; distinct from constants.hmMoves, the forget gate). -- (constants.hmBadges; distinct from constants.hmMoves, the forget gate).
-- Gen 1 allows field use from fainted party members (party menu + name -- Gen 1 allows field use from fainted party members (party menu + name
-- lookup for Cut/Surf messages); do not require mon.hp > 0 here. -- lookup for Cut/Surf messages); do not require mon.hp > 0 here.
function OverworldState:partyKnows(moveId) local function partyKnowsVanilla(moveId)
local gate = (FieldDefaults.constant(Game.data, "hmBadges") or {})[moveId] local gate = (FieldDefaults.constant(Game.data, "hmBadges") or {})[moveId]
local badge = gate and gate.badge local badge = gate and gate.badge
if badge and not Game.save.inventory[badge] then if badge and not Game.save.inventory[badge] then
@@ -1255,6 +1255,17 @@ function OverworldState:partyKnows(moveId)
return nil return nil
end end
function OverworldState:partyKnows(moveId)
-- a mod may unlock a field move another way (an HM in the bag, a rental
-- mon); next_ is the whole vanilla check, so calling it first keeps
-- vanilla answers winning
if Runtime.wantsHook("fieldmove.eligibility") then
return Runtime.call("fieldmove.eligibility", partyKnowsVanilla, moveId,
{ save = Game.save, data = Game.data })
end
return partyKnowsVanilla(moveId)
end
-- The rejection loop shared by the Good and Super Rods -- The rejection loop shared by the Good and Super Rods
-- (item_effects.asm ItemUseGoodRod .RandomLoop / ReadSuperRodData): an -- (item_effects.asm ItemUseGoodRod .RandomLoop / ReadSuperRodData): an
-- odd random byte is no bite; otherwise a 2-bit pick rerolls until it -- odd random byte is no bite; otherwise a 2-bit pick rerolls until it
+33
View File
@@ -574,6 +574,39 @@ do
check(blocked == false and why == "tile", "a walled step still reports 'tile'") check(blocked == false and why == "tile", "a walled step still reports 'tile'")
end end
do
-- fieldmove.eligibility: next_ is the whole vanilla partyKnows check,
-- so a wrapper can widen field-move rules without forking the engine
local save = {
inventory = { SOULBADGE = 1, THUNDERBADGE = 1, CASCADEBADGE = 1 },
party = { { species = "SQUIRTLE", moves = {} },
{ species = "PIDGEY", moves = { { id = "FLY" } } } },
}
local game = { save = save, data = Data }
check(bindGame(OW.partyKnows, game), "partyKnows binds a test Game")
check(OW:partyKnows("SURF") == nil, "no hook, no surf without a knower")
check(OW:partyKnows("FLY") == save.party[2], "the knower wins unwrapped")
withBuses(function(_, hooks)
local seen
hooks:wrap("fieldmove.eligibility", function(next_, moveId, ctx)
seen = { moveId = moveId, ctx = ctx }
local mon = next_(moveId, ctx)
if mon then return mon end -- vanilla first
if moveId == "SURF" then return ctx.save.party[1] end
return nil
end, 0, "rental")
check(OW:partyKnows("SURF") == save.party[1],
"the hook can offer a field move vanilla denies")
check(OW:partyKnows("FLY") == save.party[2],
"a vanilla knower still beats the hook")
check(OW:partyKnows("CUT") == nil,
"next_ still yields nil when the hook declines")
check(seen.moveId == "CUT" and seen.ctx.save == save
and seen.ctx.data == Data,
"the hook ctx carries moveId, save, and data")
end)
end
do do
-- warp.destination reroutes one door without owning the warp table -- warp.destination reroutes one door without owning the warp table
local warpDef = Data.maps.PALLET_TOWN.warps[1] local warpDef = Data.maps.PALLET_TOWN.warps[1]