diff --git a/data/scripts/gyms.lua b/data/scripts/gyms.lua index c94484e3..ddf126c8 100644 --- a/data/scripts/gyms.lua +++ b/data/scripts/gyms.lua @@ -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 diff --git a/data/scripts/victories.lua b/data/scripts/victories.lua index b1aeaa9f..dc71e796 100644 --- a/data/scripts/victories.lua +++ b/data/scripts/victories.lua @@ -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", } }, diff --git a/docs/behavior-porting-notes.md b/docs/behavior-porting-notes.md index 8693bcef..d26affce 100644 --- a/docs/behavior-porting-notes.md +++ b/docs/behavior-porting-notes.md @@ -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 diff --git a/src/world/OverworldController.lua b/src/world/OverworldController.lua index 85a7a5e1..6b2678a7 100644 --- a/src/world/OverworldController.lua +++ b/src/world/OverworldController.lua @@ -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 diff --git a/tests/parity_A.lua b/tests/parity_A.lua index 7c8bd473..7c15caa0 100644 --- a/tests/parity_A.lua +++ b/tests/parity_A.lua @@ -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