Merge pull request #820 from johnjohto/fix-gym-bag-full-797

Gym leaders skip the TM hand-over when the bag is full
This commit is contained in:
bryanthaboi
2026-08-04 16:38:11 -04:00
committed by GitHub
5 changed files with 219 additions and 34 deletions
+37 -14
View File
@@ -18,6 +18,26 @@ local M = {
VIRIDIAN_GYM = { city = "VIRIDIAN CITY", leader = "GIOVANNI", badge = "EARTHBADGE" },
}
-- The originals' middle branch shared by every leader's text_asm: beaten
-- but EVENT_GOT_TM* unset means the bag was full when the victory script
-- ran GiveItem, so talking to the leader re-runs the ReceiveTM script.
-- Returns true when the retry took over the talk. A save from before
-- #797 already holds the TM without the flag; treat the owned TM as
-- received so those saves fall through to the advice text instead of
-- collecting a second copy.
local function retryTmGive(game, ow, victoryKey, done)
local reward = require("data.scripts.victories")[victoryKey]
if not (reward and reward.gotFlag) then return false end
if game.save.flags[reward.gotFlag] then return false end
local owned = game.save.inventory and game.save.inventory[reward.item] or 0
if owned > 0 then
game.save.flags[reward.gotFlag] = true
return false
end
ow:offerGymTm(reward, done)
return true
end
-- scripts/PewterGym.asm PewterGymBrockText (text_asm): CheckEvent
-- EVENT_BEAT_BROCK branches his dialogue. Before the badge he prints
-- _PewterGymBrockPreBattleText and engages the leader battle
@@ -25,12 +45,14 @@ local M = {
-- badge/TM34 rewards and EVENT_BEAT_BROCK come from
-- data/scripts/victories.lua OPP_BROCK#1). After the badge his
-- .afterBeat branch prints _PewterGymBrockPostBattleAdviceText ("Go to
-- the GYM in CERULEAN..."). The original's middle branch (beat but
-- TM34 not yet handed over, CheckEventReuseA EVENT_GOT_TM34) is
-- unreachable in the port: the TM is granted with the victory.
-- the GYM in CERULEAN..."). The middle branch (beat but TM34 not yet
-- handed over, CheckEventReuseA EVENT_GOT_TM34 -> call
-- PewterGymScriptReceiveTM34) retries the TM give when the bag was full
-- at the victory (#797).
M.PEWTER_GYM.talk = {
TEXT_PEWTERGYM_BROCK = function(game, ow, npc, done)
if game.save.flags.EVENT_BEAT_BROCK then
if retryTmGive(game, ow, "OPP_BROCK#1", done) then return end
local TextBox = require("src.render.TextBox")
game.stack:push(TextBox.new(game,
game.data.text._PewterGymBrockPostBattleAdviceText
@@ -48,16 +70,17 @@ M.PEWTER_GYM.talk = {
-- (engageTrainer shows that same pre-battle text via resolveText; the
-- badge/TM rewards and the beat flag come from data/scripts/victories.lua)
-- -- and once beaten print the post-battle advice text. As with Brock,
-- the originals' middle branch (beaten but the TM not yet handed over,
-- CheckEventReuseA EVENT_GOT_TM*) is unreachable in the port: the TM is
-- granted with the victory.
-- the middle branch (beaten but the TM not yet handed over,
-- CheckEventReuseA EVENT_GOT_TM*) retries the TM give when the bag was
-- full at the victory.
-- afterAdvice, when given, takes over `done`: it is handed (game, ow, npc,
-- done) and must call done() itself once whatever it's doing (e.g. a fade
-- around a HideObject) finishes, rather than having it invoked
-- automatically. Only Giovanni's farewell uses this.
local function leaderTalk(beatFlag, adviceLabel, fallback, afterAdvice)
local function leaderTalk(beatFlag, adviceLabel, fallback, afterAdvice, victoryKey)
return function(game, ow, npc, done)
if game.save.flags[beatFlag] then
if victoryKey and retryTmGive(game, ow, victoryKey, done) then return end
local TextBox = require("src.render.TextBox")
local finish = done
if afterAdvice then
@@ -79,42 +102,42 @@ end
M.CERULEAN_GYM.talk = {
TEXT_CERULEANGYM_MISTY = leaderTalk("EVENT_BEAT_MISTY",
"_CeruleanGymMistyTM11ExplanationText",
"TM11 teaches\nBUBBLEBEAM!"),
"TM11 teaches\nBUBBLEBEAM!", nil, "OPP_MISTY#1"),
}
-- scripts/VermilionGym.asm VermilionGymLTSurgeText .got_tm24_already
M.VERMILION_GYM.talk = {
TEXT_VERMILIONGYM_LT_SURGE = leaderTalk("EVENT_BEAT_LT_SURGE",
"_VermilionGymLTSurgePostBattleAdviceText",
"A little word of\nadvice, kid!"),
"A little word of\nadvice, kid!", nil, "OPP_LT_SURGE#1"),
}
-- scripts/CeladonGym.asm CeladonGymErikaText .afterBeat
M.CELADON_GYM.talk = {
TEXT_CELADONGYM_ERIKA = leaderTalk("EVENT_BEAT_ERIKA",
"_CeladonGymErikaPostBattleAdviceText",
"You are cataloging\nPOKéMON? I must\nsay I'm impressed."),
"You are cataloging\nPOKéMON? I must\nsay I'm impressed.", nil, "OPP_ERIKA#1"),
}
-- scripts/FuchsiaGym.asm FuchsiaGymKogaText .afterBeat
M.FUCHSIA_GYM.talk = {
TEXT_FUCHSIAGYM_KOGA = leaderTalk("EVENT_BEAT_KOGA",
"_FuchsiaGymKogaPostBattleAdviceText",
"When afflicted by\nTOXIC, POKéMON\nsuffer more and\nmore as battle\nprogresses!"),
"When afflicted by\nTOXIC, POKéMON\nsuffer more and\nmore as battle\nprogresses!", nil, "OPP_KOGA#1"),
}
-- scripts/SaffronGym.asm SaffronGymSabrinaText .afterBeat
M.SAFFRON_GYM.talk = {
TEXT_SAFFRONGYM_SABRINA = leaderTalk("EVENT_BEAT_SABRINA",
"_SaffronGymSabrinaPostBattleAdviceText",
"Everyone has\npsychic power!\nPeople just don't\nrealize it!"),
"Everyone has\npsychic power!\nPeople just don't\nrealize it!", nil, "OPP_SABRINA#1"),
}
-- scripts/CinnabarGym.asm CinnabarGymBlaineText .afterBeat
M.CINNABAR_GYM.talk = {
TEXT_CINNABARGYM_BLAINE = leaderTalk("EVENT_BEAT_BLAINE",
"_CinnabarGymBlainePostBattleAdviceText",
"FIRE BLAST is the\nultimate fire\ntechnique!"),
"FIRE BLAST is the\nultimate fire\ntechnique!", nil, "OPP_BLAINE#1"),
}
-- scripts/ViridianGym.asm ViridianGymGiovanniText .afterBeat: after the
@@ -142,7 +165,7 @@ M.VIRIDIAN_GYM.talk = {
"VIRIDIAN_GYM", "VIRIDIANGYM_GIOVANNI")
end
end, done))
end),
end, "OPP_GIOVANNI#3"),
}
return M
+50 -8
View File
@@ -18,6 +18,16 @@
-- script). Leaders are not def_trainers entries, so engageTrainer has
-- no header.won -- checkVictoryRewards shows this chain instead of a
-- synthetic "received badge/TM" stub.
--
-- Gym entries split the TM hand-over out of `dialogue`, mirroring the
-- originals' GiveItem check (`call GiveItem` / `jr nc, .BagFull`):
-- `tmPre` is the ReceiveTM script's lead-in (badge info / "Wait! Take
-- this!"), shown at the victory and again when a beaten leader retries
-- the hand-over; `tmDialogue` shows only when the TM actually goes in
-- the bag; `noRoom` is the "make room" line shown instead when the bag
-- is full; `gotFlag` (pokered's EVENT_GOT_TM*) is set only on a
-- successful give, which is what makes the leader's talk script retry
-- later (gyms.lua).
local function range(prefix, first, last)
local t = {}
@@ -33,6 +43,8 @@ return {
-- escort NPC and the first Route 22 rival stay gone after the badge.
["OPP_BROCK#1"] = { badge = "BOULDERBADGE", flag = "EVENT_BEAT_BROCK",
item = "TM_BIDE",
gotFlag = "EVENT_GOT_TM34",
noRoom = "_PewterGymTM34NoRoomText",
deactivate = { "EVENT_BEAT_PEWTER_GYM_TRAINER_0" },
hide = {
{ "PEWTER_CITY", "PEWTERCITY_YOUNGSTER" },
@@ -41,69 +53,99 @@ return {
dialogue = {
"_PewterGymBrockReceivedBoulderBadgeText",
"_PewterGymBrockBoulderBadgeInfoText",
"_PewterGymBrockWaitTakeThisText",
},
tmPre = { "_PewterGymBrockWaitTakeThisText" },
tmDialogue = {
"_PewterGymReceivedTM34Text",
"_TM34ExplanationText",
} },
["OPP_MISTY#1"] = { badge = "CASCADEBADGE", flag = "EVENT_BEAT_MISTY",
item = "TM_BUBBLEBEAM",
gotFlag = "EVENT_GOT_TM11",
noRoom = "_CeruleanGymMistyTM11NoRoomText",
deactivate = range("EVENT_BEAT_CERULEAN_GYM_TRAINER_", 0, 1),
dialogue = {
"_CeruleanGymMistyReceivedCascadeBadgeText",
"_CeruleanGymMistyCascadeBadgeInfoText",
},
tmPre = { "_CeruleanGymMistyCascadeBadgeInfoText" },
tmDialogue = {
"_CeruleanGymMistyReceivedTM11Text",
} },
["OPP_LT_SURGE#1"] = { badge = "THUNDERBADGE", flag = "EVENT_BEAT_LT_SURGE",
item = "TM_THUNDERBOLT",
gotFlag = "EVENT_GOT_TM24",
noRoom = "_VermilionGymLTSurgeTM24NoRoomText",
deactivate = range("EVENT_BEAT_VERMILION_GYM_TRAINER_", 0, 2),
dialogue = {
"_VermilionGymLTSurgeReceivedThunderBadgeText",
"_VermilionGymLTSurgeThunderBadgeInfoText",
},
tmPre = { "_VermilionGymLTSurgeThunderBadgeInfoText" },
tmDialogue = {
"_VermilionGymLTSurgeReceivedTM24Text",
"_TM24ExplanationText",
} },
["OPP_ERIKA#1"] = { badge = "RAINBOWBADGE", flag = "EVENT_BEAT_ERIKA",
item = "TM_MEGA_DRAIN",
gotFlag = "EVENT_GOT_TM21",
noRoom = "_CeladonGymTM21NoRoomText",
deactivate = range("EVENT_BEAT_CELADON_GYM_TRAINER_", 0, 6),
dialogue = {
"_CeladonGymErikaReceivedRainbowBadgeText",
"_CeladonGymRainbowBadgeInfoText",
},
tmPre = { "_CeladonGymRainbowBadgeInfoText" },
tmDialogue = {
"_CeladonGymReceivedTM21Text",
"_TM21ExplanationText",
} },
["OPP_KOGA#1"] = { badge = "SOULBADGE", flag = "EVENT_BEAT_KOGA",
item = "TM_TOXIC",
gotFlag = "EVENT_GOT_TM06",
noRoom = "_FuchsiaGymKogaTM06NoRoomText",
deactivate = range("EVENT_BEAT_FUCHSIA_GYM_TRAINER_", 0, 5),
dialogue = {
"_FuchsiaGymKogaReceivedSoulBadgeText",
"_FuchsiaGymKogaSoulBadgeInfoText",
},
tmPre = { "_FuchsiaGymKogaSoulBadgeInfoText" },
tmDialogue = {
"_FuchsiaGymKogaReceivedTM06Text",
"_FuchsiaGymKogaTM06ExplanationText",
} },
["OPP_SABRINA#1"] = { badge = "MARSHBADGE", flag = "EVENT_BEAT_SABRINA",
item = "TM_PSYWAVE",
gotFlag = "EVENT_GOT_TM46",
noRoom = "_SaffronGymSabrinaTM46NoRoomText",
deactivate = range("EVENT_BEAT_SAFFRON_GYM_TRAINER_", 0, 6),
dialogue = {
"_SaffronGymSabrinaReceivedMarshBadgeText",
"_SaffronGymSabrinaMarshBadgeInfoText",
},
tmPre = { "_SaffronGymSabrinaMarshBadgeInfoText" },
tmDialogue = {
"_SaffronGymSabrinaReceivedTM46Text",
"_TM46ExplanationText",
} },
["OPP_BLAINE#1"] = { badge = "VOLCANOBADGE", flag = "EVENT_BEAT_BLAINE",
item = "TM_FIRE_BLAST",
gotFlag = "EVENT_GOT_TM38",
noRoom = "_CinnabarGymBlaineTM38NoRoomText",
deactivate = range("EVENT_BEAT_CINNABAR_GYM_TRAINER_", 0, 6),
dialogue = {
"_CinnabarGymBlaineReceivedVolcanoBadgeText",
"_CinnabarGymBlaineVolcanoBadgeInfoText",
},
tmPre = { "_CinnabarGymBlaineVolcanoBadgeInfoText" },
tmDialogue = {
"_CinnabarGymBlaineReceivedTM38Text",
"_CinnabarGymBlaineTM38ExplanationText",
} },
["OPP_GIOVANNI#3"] = { badge = "EARTHBADGE", flag = "EVENT_BEAT_GIOVANNI",
item = "TM_FISSURE",
gotFlag = "EVENT_GOT_TM27",
noRoom = "_ViridianGymGiovanniTM27NoRoomText",
deactivate = range("EVENT_BEAT_VIRIDIAN_GYM_TRAINER_", 0, 7),
dialogue = {
"_ViridianGymGiovanniReceivedEarthBadgeText",
"_ViridianGymGiovanniEarthBadgeInfoText",
},
tmPre = { "_ViridianGymGiovanniEarthBadgeInfoText" },
tmDialogue = {
"_ViridianGymGiovanniReceivedTM27Text",
"_ViridianGymGiovanniTM27ExplanationText",
} },
+6 -2
View File
@@ -234,8 +234,12 @@ What was ported from pokered's engine code and where it came from.
pre-battle text and engages the leader battle (badge/TM via
data/scripts/victories.lua); post-badge talk prints the leader's
post-battle advice text (Misty's is her TM11 explanation). The
originals' middle branch (beaten but TM not handed over) is
unreachable since the TM is granted with the victory. Giovanni's
originals' middle branch (beaten but TM not handed over,
CheckEventReuseA EVENT_GOT_TM*) is ported too: the victory's GiveItem
goes through the bag's capacity check, a full bag shows the leader's
"make room" text instead of the received lines and leaves
EVENT_GOT_TM* unset, and talking to the leader re-runs the ReceiveTM
script until the TM goes in (#797). Giovanni's
farewell (`ViridianGymGiovanniText` .afterBeat) hides him inside a
fade-to-black/fade-in Transition matching ViridianGym.asm's
GBFadeOutToBlack → HideObject → GBFadeInFromBlack, persisted
+66 -6
View File
@@ -3008,6 +3008,22 @@ function OverworldState:engageTrainer(npc, onDone)
end))
end
-- Shared GiveItem step for the victory rewards (pokered home/give.asm):
-- the item goes through the bag's capacity check, and only a successful
-- add sets the reward's gotFlag (EVENT_GOT_TM*) and copies the item name
-- into wStringBuffer for the "{RAM:wStringBuffer}" received texts.
local function giveVictoryItem(reward)
if not require("src.inventory.Bag").add(Game.save, reward.item, 1, Game.data) then
return false
end
if reward.gotFlag then
Game.save.flags[reward.gotFlag] = true
end
local idef = Game.data.items[reward.item]
Game.stringBuffer = idef and idef.name or reward.item
return true
end
-- Badges/items awarded after specific battles (data/scripts/victories.lua).
-- `deactivate` retires unfought gym/dojo trainers the way the originals'
-- SetEvent / SetEventRange do after the leader victory.
@@ -3036,12 +3052,13 @@ function OverworldState:checkVictoryRewards(trainerClass, partyIndex)
if reward.badge then
Game.save.inventory[reward.badge] = 1
end
local tmGiven = false
if reward.item then
local inv = Game.save.inventory
inv[reward.item] = (inv[reward.item] or 0) + 1
local idef = Game.data.items[reward.item]
-- GiveItem -> CopyToStringBuffer for "{RAM:wStringBuffer}" received texts
Game.stringBuffer = idef and idef.name or reward.item
-- pokered GiveItem (home/give.asm): AddItemToInventory first, and a
-- full bag (jr nc, .BagFull) skips the received lines for the "make
-- room" text, leaving EVENT_GOT_TM* unset so the leader's talk script
-- retries the hand-over later (offerGymTm via gyms.lua)
tmGiven = giveVictoryItem(reward)
end
local lines = {}
if reward.dialogue then
@@ -3051,13 +3068,29 @@ function OverworldState:checkVictoryRewards(trainerClass, partyIndex)
table.insert(lines, text[label])
end
end
if reward.item then
for _, label in ipairs(reward.tmPre or {}) do
if text[label] and text[label] ~= "" then
table.insert(lines, text[label])
end
end
if tmGiven then
for _, label in ipairs(reward.tmDialogue or {}) do
if text[label] and text[label] ~= "" then
table.insert(lines, text[label])
end
end
elseif reward.noRoom and text[reward.noRoom] and text[reward.noRoom] ~= "" then
table.insert(lines, text[reward.noRoom])
end
end
elseif reward.badge or reward.item then
if reward.badge then
local name = Game.data.items[reward.badge] and Game.data.items[reward.badge].name
or reward.badge
table.insert(lines, Strings("%s received\nthe %s!", Game.save.player.name, name))
end
if reward.item then
if tmGiven then
local name = Game.stringBuffer or reward.item
table.insert(lines, Strings("%s received\n%s!", Game.save.player.name, name))
end
@@ -3068,6 +3101,33 @@ function OverworldState:checkVictoryRewards(trainerClass, partyIndex)
self:runVictoryHook()
end
-- A beaten leader re-running their ReceiveTM script when the bag was full
-- at the victory (pokered's middle branch, e.g. PewterGymBrockText
-- CheckEventReuseA EVENT_GOT_TM34 -> call PewterGymScriptReceiveTM34).
-- The script's lead-in lines (tmPre: badge info / "Wait! Take this!")
-- show again, then the same GiveItem check decides between the received
-- lines and the "make room" text.
function OverworldState:offerGymTm(reward, done)
local text = Game.data.text or {}
local lines = {}
local function addLine(label)
if label and text[label] and text[label] ~= "" then
table.insert(lines, text[label])
end
end
for _, label in ipairs(reward.tmPre or {}) do addLine(label) end
if giveVictoryItem(reward) then
for _, label in ipairs(reward.tmDialogue or {}) do addLine(label) end
else
addLine(reward.noRoom)
end
if #lines > 0 then
Game.stack:push(TextBox.new(Game, table.concat(lines, "\f"), done))
elseif done then
done()
end
end
-- pokered reloads the map after every battle, re-running the map
-- script (e.g. LoreleiShowOrHideExitBlock); this hook is the port's
-- equivalent so seals/toggles refresh without leaving the map
+60 -4
View File
@@ -183,7 +183,7 @@ do
new = function(game, text, done) return { text = text, onDone = done } end,
}
local function driveLeader(mapId, textConst, beatFlag)
local function driveLeader(mapId, textConst, beatFlag, gotFlag)
local pushed, engaged
local game = {
save = { flags = {} },
@@ -198,6 +198,9 @@ do
mapId .. " leader talk (no badge) engages the leader battle")
engaged, pushed = nil, nil
game.save.flags[beatFlag] = true
-- the advice/farewell branch is pokered's .afterBeat, reached only
-- once EVENT_GOT_TM* is set (the TM went into the bag)
if gotFlag then game.save.flags[gotFlag] = true end
local state = {}
script(game, ow, { def = {} }, function() state.doneCalled = true end)
check(pushed and not engaged,
@@ -206,17 +209,17 @@ do
end
local _, box = driveLeader("CERULEAN_GYM", "TEXT_CERULEANGYM_MISTY",
"EVENT_BEAT_MISTY")
"EVENT_BEAT_MISTY", "EVENT_GOT_TM11")
eq(box and box.text, Data.text._CeruleanGymMistyTM11ExplanationText,
"Misty (beaten) shows the TM11 explanation text")
_, box = driveLeader("CINNABAR_GYM", "TEXT_CINNABARGYM_BLAINE",
"EVENT_BEAT_BLAINE")
"EVENT_BEAT_BLAINE", "EVENT_GOT_TM38")
eq(box and box.text, Data.text._CinnabarGymBlainePostBattleAdviceText,
"Blaine (beaten) shows his post-battle advice text")
local game, gbox, state = driveLeader("VIRIDIAN_GYM", "TEXT_VIRIDIANGYM_GIOVANNI",
"EVENT_BEAT_GIOVANNI")
"EVENT_BEAT_GIOVANNI", "EVENT_GOT_TM27")
eq(gbox and gbox.text, Data.text._ViridianGymGiovanniPostBattleAdviceText,
"Giovanni (beaten) shows his farewell text")
@@ -392,6 +395,59 @@ do
check(cinnabarNpc and ow:trainerDefeated(cinnabarNpc),
"unfought Cinnabar trainer is defeated via seeded header event")
-- #797: a full bag at the victory skips the TM hand-over (pokered's
-- `call GiveItem` / `jr nc, .BagFull`): badge and beat flag still land,
-- but EVENT_GOT_TM34 stays unset and the "make room" line replaces the
-- received/explanation texts. Talking to Brock afterwards re-runs the
-- ReceiveTM script and grants the TM once there is room.
while Game.stack:top() do Game.stack:pop() end
Game.save = SaveData.newGame()
Game.save.flags = {}
Game.save.inventory = {}
Game.save.defeatedTrainers = {}
local Bag = require("src.inventory.Bag")
for i = 1, Bag.capacity(Data) do
Bag.add(Game.save, "FULLBAG_" .. i, 1, Data)
end
eq(Bag.slots(Game.save), Bag.capacity(Data), "bag is full before Brock")
Game.stack:push(OW, "PEWTER_GYM", 4, 13, "up")
ow = Game.stack:top()
ow:checkVictoryRewards("OPP_BROCK", 1)
local fullBagText = stackedDialogue()
check(Game.save.flags.EVENT_BEAT_BROCK,
"full bag: Brock victory still sets EVENT_BEAT_BROCK")
check(Game.save.inventory.BOULDERBADGE == 1,
"full bag: Brock victory still awards BOULDERBADGE")
check(not Game.save.flags.EVENT_GOT_TM34,
"full bag: EVENT_GOT_TM34 stays unset (bag_full branch)")
check(Game.save.inventory.TM_BIDE == nil,
"full bag: TM34 is not forced into the bag")
check(fullBagText:find("room", 1, true) ~= nil,
"full bag: victory dialogue shows Brock's make-room line")
check(fullBagText:find("BIDE", 1, true) == nil,
"full bag: received/explanation texts are skipped")
-- make room, then talk to Brock: the middle branch re-runs ReceiveTM34
while Game.stack:top() do Game.stack:pop() end
Bag.remove(Game.save, "FULLBAG_1", 1)
local brockTalk = init.talkScript("PEWTER_GYM", "TEXT_PEWTERGYM_BROCK")
check(brockTalk ~= nil, "Brock's talk script is registered")
brockTalk(Game, ow, { def = {} }, function() end)
local retryText = stackedDialogue()
check(retryText:find("Wait!", 1, true) ~= nil,
"retry: ReceiveTM34 lead-in (Wait! Take this!) shows again")
check(retryText:find("BIDE", 1, true) ~= nil,
"retry: received/explanation texts show once the TM fits")
eq(Game.save.inventory.TM_BIDE, 1, "retry: TM34 goes into the bag")
check(Game.save.flags.EVENT_GOT_TM34, "retry: EVENT_GOT_TM34 is set")
-- once the TM is handed over, Brock falls back to his advice text
while Game.stack:top() do Game.stack:pop() end
brockTalk(Game, ow, { def = {} }, function() end)
local adviceText = stackedDialogue()
check(adviceText:find("CERULEAN", 1, true) ~= nil,
"after the TM: Brock shows his post-battle advice text")
while Game.stack:top() do Game.stack:pop() end
end