From 43158d724be166c986bd93a213eefc063b68518e Mon Sep 17 00:00:00 2001 From: johnjohto Date: Mon, 27 Jul 2026 13:38:17 -0400 Subject: [PATCH] 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 --- src/world/OverworldController.lua | 13 +++++++++++- tests/mod_world_tests.lua | 33 +++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/world/OverworldController.lua b/src/world/OverworldController.lua index 10adb599..e2101b85 100644 --- a/src/world/OverworldController.lua +++ b/src/world/OverworldController.lua @@ -1241,7 +1241,7 @@ end -- (constants.hmBadges; distinct from constants.hmMoves, the forget gate). -- Gen 1 allows field use from fainted party members (party menu + name -- 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 badge = gate and gate.badge if badge and not Game.save.inventory[badge] then @@ -1255,6 +1255,17 @@ function OverworldState:partyKnows(moveId) return nil 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 -- (item_effects.asm ItemUseGoodRod .RandomLoop / ReadSuperRodData): an -- odd random byte is no bite; otherwise a 2-bit pick rerolls until it diff --git a/tests/mod_world_tests.lua b/tests/mod_world_tests.lua index 1dc32844..caf7ae31 100644 --- a/tests/mod_world_tests.lua +++ b/tests/mod_world_tests.lua @@ -574,6 +574,39 @@ do check(blocked == false and why == "tile", "a walled step still reports 'tile'") 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 -- warp.destination reroutes one door without owning the warp table local warpDef = Data.maps.PALLET_TOWN.warps[1]