-- Gift NPCs and in-game trades. Each cites its pokered source. local M = {} local function text(game) return game.data.text end local function push(game, s, done) local TextBox = require("src.render.TextBox") game.stack:push(TextBox.new(game, s, done)) end -- fill the extracted text placeholders ({RAM:...}, {PLAYER}) local function fill(s, subs) s = s:gsub("{PLAYER}", subs.player or "") s = s:gsub("{RAM:[^}]*}", function() return subs.ram or "" end) return s end -- One-time item gift, following the original text_asm flow: -- pre text (optional) -> GiveItem (bag-full refusal keeps the flag -- unset, talk again after making room) -> received text -> optional -- explanation; repeat visits get the already text. local function gift(opts) return function(game, ow, npc, done) local t = text(game) local itemName = game.data.items[opts.item].name local subs = { ram = itemName, player = game.save.player.name } local function say(label, fallback, cb) push(game, fill(t[label] or fallback, subs), cb) end if game.save.flags[opts.flag] then say(opts.already or opts.explain, "It's a useful\nitem, isn't it?", done) return end local function give() if not require("src.inventory.Bag").add(game.save, opts.item, 1) then say(opts.noRoom, "You have no room\nfor this item!", done) return end game.save.flags[opts.flag] = true local idef = game.data.items[opts.item] require("src.core.Sound").play(game.data, (idef and idef.keyItem) and "Get_Key_Item" or "Get_Item1") say(opts.received, "{PLAYER} received\n{RAM:}!", function() if opts.explain then say(opts.explain, "", done) else done() end end) end if opts.pre then say(opts.pre, "", give) else give() end end end -- Coin Case (scripts/CeladonDiner.asm, the busted gym guide) M.CELADON_DINER = { talk = { TEXT_CELADONDINER_GYM_GUIDE = gift({ flag = "EVENT_GOT_COIN_CASE", item = "COIN_CASE", pre = "_CeladonDinerGymGuideImFlatOutBustedText", received = "_CeladonDinerGymGuideReceivedCoinCaseText", noRoom = "_CeladonDinerGymGuideCoinCaseNoRoomText", already = "_CeladonDinerGymGuideWinItBackText", }), }, } -- TM18 Counter (scripts/CeladonMart3F.asm, the TV-game-shop clerk) M.CELADON_MART_3F = { talk = { TEXT_CELADONMART3F_CLERK = gift({ flag = "EVENT_GOT_TM18", item = "TM_COUNTER", pre = "_CeladonMart3FClerkTM18PreReceiveText", received = "_CeladonMart3FClerkReceivedTM18Text", explain = "_CeladonMart3FClerkTM18ExplanationText", noRoom = "_CeladonMart3FClerkTM18NoRoomText", }), }, } -- TM39 Swift (scripts/Route12Gate2F.asm) M.ROUTE_12_GATE_2F = { talk = { TEXT_ROUTE12GATE2F_BRUNETTE_GIRL = gift({ flag = "EVENT_GOT_TM39", item = "TM_SWIFT", pre = "_Route12Gate2FBrunetteGirlYouCanHaveThisText", received = "_Route12Gate2FBrunetteGirlReceivedTM39Text", explain = "_Route12Gate2FBrunetteGirlTM39ExplanationText", noRoom = "_Route12Gate2FBrunetteGirlTM39NoRoomText", }), }, } -- TM41 Softboiled (scripts/CeladonCity.asm, Gramps3) M.CELADON_CITY = { talk = { TEXT_CELADONCITY_GRAMPS3 = gift({ flag = "EVENT_GOT_TM41", item = "TM_SOFTBOILED", pre = "_CeladonCityGramps3Text", received = "_CeladonCityGramps3ReceivedTM41Text", explain = "_CeladonCityGramps3TM41ExplanationText", noRoom = "_CeladonCityGramps3TM41NoRoomText", }), }, } -- TM35 Metronome (scripts/CinnabarLabMetronomeRoom.asm) M.CINNABAR_LAB_METRONOME_ROOM = { talk = { TEXT_CINNABARLABMETRONOMEROOM_SCIENTIST1 = gift({ flag = "EVENT_GOT_TM35", item = "TM_METRONOME", pre = "_CinnabarLabMetronomeRoomScientist1Text", received = "_CinnabarLabMetronomeRoomScientist1ReceivedTM35Text", explain = "_CinnabarLabMetronomeRoomScientist1TM35ExplanationText", noRoom = "_CinnabarLabMetronomeRoomScientist1TM35NoRoomText", }), }, } -- TM42 Dream Eater (scripts/ViridianCity.asm, the fisher; no pre text) M.VIRIDIAN_CITY = { talk = { TEXT_VIRIDIANCITY_FISHER = gift({ flag = "EVENT_GOT_TM42", item = "TM_DREAM_EATER", received = "_ViridianCityFisherReceivedTM42Text", explain = "_ViridianCityFisherTM42ExplanationText", noRoom = "_ViridianCityFisherTM42NoRoomText", }), }, } -- TM36 Selfdestruct (scripts/SilphCo2F.asm, the rescued worker) M.SILPH_CO_2F = { talk = { TEXT_SILPHCO2F_SILPH_WORKER_F = gift({ flag = "EVENT_GOT_TM36", item = "TM_SELFDESTRUCT", received = "_SilphCo2FSilphWorkerFReceivedTM36Text", explain = "_SilphCo2FSilphWorkerFTM36ExplanationText", noRoom = "_SilphCo2FSilphWorkerFTM36NoRoomText", }), }, } -- Free POTION sample (scripts/Route1.asm; the original burns the flag -- even on a full bag -- we keep the port's kinder halt-and-retry) M.ROUTE_1 = { talk = { TEXT_ROUTE1_YOUNGSTER1 = gift({ flag = "EVENT_GOT_POTION_SAMPLE", item = "POTION", pre = "_Route1Youngster1MartSampleText", received = "_Route1Youngster1GotPotionText", noRoom = "_Route1Youngster1NoRoomText", already = "_Route1Youngster1AlsoGotPokeballsText", }), }, } -- The three in-game trades the port was missing (data/events/trades.asm -- indices are 1-based in data/generated/field.lua trades) M.ROUTE_11_GATE_2F = { talk = { TEXT_ROUTE11GATE2F_YOUNGSTER = { { "face_player" }, { "trade", 1, "EVENT_TRADED_NIDORINO_FOR_NIDORINA" }, -- TERRY }, }, } M.ROUTE_18_GATE_2F = { talk = { TEXT_ROUTE18GATE2F_YOUNGSTER = { { "face_player" }, { "trade", 6, "EVENT_TRADED_SLOWBRO_FOR_LICKITUNG" }, -- MARC }, }, } M.UNDERGROUND_PATH_ROUTE_5 = { talk = { TEXT_UNDERGROUNDPATHROUTE5_LITTLE_GIRL = { { "face_player" }, { "trade", 10, "EVENT_TRADED_NIDORAN_M_FOR_NIDORAN_F" }, -- SPOT }, }, } -- ===================================================================== -- Progression gates and rival ambushes -- ===================================================================== local function inCoords(coords, x, y) for _, c in ipairs(coords) do if x == c[1] and y == c[2] then return true end end return false end -- coordinate block: show a line and push the player back one step local function stepGate(opts) return function(game, ow, x, y) if not inCoords(opts.coords, x, y) then return false end if not opts.blocked(game) then return false end require("src.core.Sound").play(game.data, "Denied") push(game, text(game)[opts.text] or opts.fallback, function() ow:scriptMove(ow.player, opts.push, 1) end) return true end end -- Viridian Gym stays locked until the seven other badges are earned -- (scripts/ViridianCity.asm ViridianCityCheckGymOpenScript: wObtained- -- Badges == ~EARTHBADGE at (32,8) shoves the player off the door) local SEVEN_BADGES = { "BOULDERBADGE", "CASCADEBADGE", "THUNDERBADGE", "RAINBOWBADGE", "SOULBADGE", "MARSHBADGE", "VOLCANOBADGE" } local viridianGymLock = stepGate({ coords = { { 32, 8 } }, blocked = function(game) for _, b in ipairs(SEVEN_BADGES) do if not game.save.inventory[b] then return true end end return false end, text = "_ViridianCityGymLockedText", fallback = "The GYM's doors\nare locked...", push = "down", }) -- story.lua's VIRIDIAN_CITY module owns the sleeping-old-man block; -- chain it behind the gym lock (the registry keeps one onStep per map) local viridianOldManStep = require("data.scripts.story").VIRIDIAN_CITY.onStep M.VIRIDIAN_CITY.onStep = function(game, ow, x, y) if viridianGymLock(game, ow, x, y) then return true end return viridianOldManStep(game, ow, x, y) end -- Cinnabar Gym needs the SECRET KEY (scripts/CinnabarIsland.asm) M.CINNABAR_ISLAND = { onStep = stepGate({ coords = { { 18, 4 } }, blocked = function(game) return not game.save.inventory.SECRET_KEY end, text = "_CinnabarIslandDoorIsLockedText", fallback = "The door is\nlocked...", push = "down", }), -- CinnabarIsland_Script line 6: ResetEvent EVENT_LAB_STILL_REVIVING_ -- FOSSIL on every (re)load of this map -- OverworldState:setMap runs -- onEnter on every entry, not just the first, so this fires both -- when the player walks out of the fossil lab back onto the island -- and on any later re-entry (boat, Mansion exit, etc.), matching the -- oracle's every-load map script. Paired with the deposit/pending/ -- ready state machine in data/scripts/story2.lua's -- TEXT_CINNABARLABFOSSILROOM_SCIENTIST1. onEnter = function(game, ow) if game.save.flags.EVENT_LAB_STILL_REVIVING_FOSSIL then game.save.flags.EVENT_LAB_STILL_REVIVING_FOSSIL = nil end end, } -- Pewter's youngster stops you leaving east before Brock is beaten -- (scripts/PewterCity.asm PewterCityCheckPlayerLeavingEastScript; the -- original escorts you to the gym, we walk you back a step) M.PEWTER_CITY = { onStep = function(game, ow, x, y) if game.save.flags.EVENT_BEAT_BROCK then return false end if not inCoords({ { 35, 17 }, { 36, 17 }, { 37, 18 }, { 37, 19 } }, x, y) then return false end local t = text(game) push(game, t._PewterCityYoungsterYoureATrainerFollowMeText or "Hey! You're a\ntrainer, right?", function() push(game, t._PewterCityYoungsterGoTakeOnBrockText or "Go take on BROCK\nat the GYM first!", function() ow:scriptMove(ow.player, "left", 1) end) end) return true end, } -- Rival ambush: show the hidden rival, walk him up to the player, run -- the battle rows, march him back and hide him. On a loss the walk is -- skipped (the blackout rebuilds the map mid-script). local function runAmbush(game, ow, rows, playerFacing) if ow.runner:isRunning() then return false end ow.player.facing = playerFacing -- the rival encounter sting (MUSIC_MEET_RIVAL); the battle music -- takes over and the map theme returns after the victory jingle require("src.core.Music").play(game.data, "Music_MeetRival") ow.runner:run(rows) return true end -- Route 22 rival, both visits (scripts/Route22.asm). pokered arms -- EVENT_ROUTE22_RIVAL_WANTS_BATTLE in Oak's lab (expired by Pewter -- Gym) and again in Viridian Gym; we derive the same windows from the -- surrounding story flags so old saves work too. local function route22Scene(n, objIndex, objName, oppClass, baseParty, beatFlag, py) return { { "show_object", "ROUTE_22", objName }, -- 1 { "move_npc_to", objIndex, 28, py }, -- 2 { "face_object", objIndex, "right" }, -- 3 { "show_text", "_Route22RivalBeforeBattleText" .. n }, -- 4 { "rival_battle", oppClass, baseParty }, -- 5 { "jump_if_false", 11 }, -- 6 { "set_flag", beatFlag }, -- 7 { "show_text", "_Route22Rival" .. n .. "DefeatedText" }, -- 8 { "show_text", "_Route22RivalAfterBattleText" .. n }, -- 9 { "move_npc_to", objIndex, 25, 5 }, -- 10 { "hide_object", "ROUTE_22", objName }, -- 11 } end M.ROUTE_22 = { onStep = function(game, ow, x, y) if not inCoords({ { 29, 4 }, { 29, 5 } }, x, y) then return false end local f = game.save.flags if f.EVENT_GOT_POKEDEX and not f.EVENT_BEAT_BROCK and not f.EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE then return runAmbush(game, ow, route22Scene(1, 1, "ROUTE22_RIVAL1", "OPP_RIVAL1", 4, "EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE", y), "left") end if f.EVENT_BEAT_GIOVANNI and not f.EVENT_BEAT_ROUTE22_RIVAL_2ND_BATTLE then return runAmbush(game, ow, route22Scene(2, 2, "ROUTE22_RIVAL2", "OPP_RIVAL2", 10, "EVENT_BEAT_ROUTE22_RIVAL_2ND_BATTLE", y), "left") end return false end, } -- Cerulean City: the Nugget Bridge rival ambush (CeruleanCityCoords2), -- the TM28 rocket thief with his forced-fight cells (Coords1), and the -- cave guard who steps aside once you are Champion. local function ceruleanRivalScene(px, py) return { { "show_object", "CERULEAN_CITY", "CERULEANCITY_RIVAL" }, -- 1 { "move_npc_to", 1, px, py - 1 }, -- 2 { "face_object", 1, "down" }, -- 3 { "show_text", "_CeruleanCityRivalPreBattleText" }, -- 4 { "rival_battle", "OPP_RIVAL1", 7 }, -- 5 { "jump_if_false", 10 }, -- 6 { "set_flag", "EVENT_BEAT_CERULEAN_RIVAL" }, -- 7 { "show_text", "_CeruleanCityRivalDefeatedText" }, -- 8 { "move_npc_to", 1, 20, 2 }, -- 9 { "hide_object", "CERULEAN_CITY", "CERULEANCITY_RIVAL" }, -- 10 } end -- scripts/CeruleanCity.asm CeruleanCityRocketText: fight the thief, -- then he returns TM28 (DIG) and hurries off local rocketRows = { { "face_player" }, -- 1 { "check_flag", "EVENT_GOT_TM28" }, -- 2 { "jump_if_true", 15 }, -- 3 { "check_flag", "EVENT_BEAT_CERULEAN_ROCKET_THIEF" }, -- 4 { "jump_if_true", 9 }, -- 5 { "show_text", "_CeruleanCityRocketText" }, -- 6 { "start_battle", "trainer", "OPP_ROCKET", 5 }, -- 7 { "jump_if_false", 16 }, -- 8 { "show_text", "_CeruleanCityRocketIllReturnTheTMText" }, -- 9 { "set_flag", "EVENT_BEAT_CERULEAN_ROCKET_THIEF" }, -- 10 { "give_item", "TM_DIG", 1, false }, -- 11 (row 13 prints) { "set_flag", "EVENT_GOT_TM28" }, -- 12 { "show_text", "_CeruleanCityRocketReceivedTM28Text" }, -- 13 { "show_text", "_CeruleanCityRocketIBetterGetMovingText" }, -- 14 { "hide_object", "CERULEAN_CITY", "CERULEANCITY_ROCKET" }, -- 15 } M.CERULEAN_CITY = { talk = { TEXT_CERULEANCITY_ROCKET = rocketRows, }, onEnter = function(game, ow) if game.save.flags.EVENT_BEAT_CHAMPION_RIVAL then local Commands = require("src.script.Commands") Commands.hide_object({ game = game, save = game.save, overworld = ow }, "CERULEAN_CITY", "CERULEANCITY_SUPER_NERD3") end end, onStep = function(game, ow, x, y) local f = game.save.flags if not f.EVENT_BEAT_CERULEAN_ROCKET_THIEF and inCoords({ { 30, 7 }, { 30, 9 } }, x, y) then if ow.runner:isRunning() then return false end local rocket = ow:npcByIndex(2) if rocket then ow.player.facing = y < 8 and "down" or "up" ow.runner:run(rocketRows, { npc = rocket }) return true end return false end if not f.EVENT_BEAT_CERULEAN_RIVAL and inCoords({ { 20, 6 }, { 21, 6 } }, x, y) then return runAmbush(game, ow, ceruleanRivalScene(x, y), "up") end return false end, } -- The Pewter museum's fossil exhibits (engine/events/hidden_events/ -- museum_fossils.asm: DisplayMonFrontSpriteInBox + the plaque text) M.MUSEUM_1F = { onInteract = function(game, ow, fx, fy) if ow.player.facing ~= "up" then return false end local PicBox = require("src.ui.PicBox") local t = text(game) if fx == 2 and fy == 3 then game.stack:push(PicBox.new(game, "assets/generated/battle/front/fossilaerodactyl.png", t._AerodactylFossilText or "AERODACTYL Fossil")) return true end if fx == 2 and fy == 6 then game.stack:push(PicBox.new(game, "assets/generated/battle/front/fossilkabutops.png", t._KabutopsFossilText or "KABUTOPS Fossil")) return true end return false end, } -- The Pewter Center's singing JIGGLYPUFF (scripts/PewterPokecenter.asm -- plays MUSIC_JIGGLYPUFF_SONG, then the map theme resumes) M.PEWTER_POKECENTER = { talk = { TEXT_PEWTERPOKECENTER_JIGGLYPUFF = function(game, ow, npc, done) require("src.core.Music").playOnce(game.data, "Music_JigglypuffSong") push(game, text(game)._PewterPokecenterJigglypuffText or "JIGGLYPUFF: Puu\npupuu!", done) end, }, } -- Cycling Road gate guards (scripts/Route16Gate1F.asm / -- Route18Gate1F.asm): without a BICYCLE in the bag the guard stops you -- on the west-side cells and walks you back local function bikeGateGuard(coords, stopText, explainText) return function(game, ow, x, y) if game.save.inventory.BICYCLE then return false end if not inCoords(coords, x, y) then return false end local t = text(game) push(game, t[stopText] or "Hey! Wait up!", function() push(game, t[explainText] or "You need a\nBICYCLE for\nCYCLING ROAD!", function() ow:scriptMove(ow.player, "up", 1) end) end) return true end end M.ROUTE_16_GATE_1F = { onStep = bikeGateGuard( { { 4, 7 }, { 4, 8 }, { 4, 9 }, { 4, 10 } }, "_Route16Gate1FGuardWaitUpText", "_Route16Gate1FGuardNoPedestriansAllowedText"), } M.ROUTE_18_GATE_1F = { onStep = bikeGateGuard( { { 4, 3 }, { 4, 4 }, { 4, 5 }, { 4, 6 } }, "_Route18Gate1FGuardExcuseMeText", "_Route18Gate1FGuardYouNeedABicycleText"), } -- Silph Co. 7F rival ambush (scripts/SilphCo7F.asm -- SilphCo7FDefaultScript: coords (3,2)/(3,3), the rival at (3,7) walks -- up, MUSIC_MEET_RIVAL, OPP_RIVAL2 parties 7-9 by starter, then he -- wishes you luck, walks off right and disappears; one-time via -- EVENT_BEAT_SILPH_CO_RIVAL) M.SILPH_CO_7F = { onStep = function(game, ow, x, y) if game.save.flags.EVENT_BEAT_SILPH_CO_RIVAL then return false end if not inCoords({ { 3, 2 }, { 3, 3 } }, x, y) then return false end return runAmbush(game, ow, { { "show_object", "SILPH_CO_7F", "SILPHCO7F_RIVAL" }, -- 1 { "show_text", "_SilphCo7FRivalText" }, -- 2 { "move_npc_to", 9, 3, y + 1 }, -- 3 { "face_object", 9, "up" }, -- 4 { "show_text", "_SilphCo7FRivalWaitedHereText" }, -- 5 { "rival_battle", "OPP_RIVAL2", 7 }, -- 6 { "jump_if_false", 12 }, -- 7 { "set_flag", "EVENT_BEAT_SILPH_CO_RIVAL" }, -- 8 { "show_text", "_SilphCo7FRivalDefeatedText" }, -- 9 { "show_text", "_SilphCo7FRivalGoodLuckToYouText" }, -- 10 { "move_npc_to", 9, 5, y + 1 }, -- 11 { "hide_object", "SILPH_CO_7F", "SILPHCO7F_RIVAL" }, -- 12 }, "down") end, } -- S.S. Anne 2F rival ambush (scripts/SSAnne2F.asm; coords 36/37,8) M.SS_ANNE_2F = { onStep = function(game, ow, x, y) if game.save.flags.EVENT_BEAT_SS_ANNE_RIVAL then return false end if not inCoords({ { 36, 8 }, { 37, 8 } }, x, y) then return false end local onLeft = x == 36 return runAmbush(game, ow, { { "show_object", "SS_ANNE_2F", "SSANNE2F_RIVAL" }, -- 1 { "move_npc_to", 2, 36, onLeft and 7 or 8 }, -- 2 { "face_object", 2, onLeft and "down" or "right" }, -- 3 { "show_text", "_SSAnne2FRivalText" }, -- 4 { "rival_battle", "OPP_RIVAL2", 1 }, -- 5 { "jump_if_false", 10 }, -- 6 { "set_flag", "EVENT_BEAT_SS_ANNE_RIVAL" }, -- 7 { "show_text", "_SSAnne2FRivalDefeatedText" }, -- 8 { "move_npc_to", 2, 36, 4 }, -- 9 { "hide_object", "SS_ANNE_2F", "SSANNE2F_RIVAL" }, -- 10 }, onLeft and "up" or "left") end, } return M