-- More hand-ported events: the Pallet Town intro, the thirsty Saffron -- gate guards, the Bike Voucher chain, fossils and the day-care. -- Registered via data/scripts/init.lua; Red/Blue cite pokered, Yellow -- cites pokeyellow (Pallet stop row, Oak spawn, walk RLE, and the -- wild-Pikachu beat before the lab escort). local GameVersion = require("src.core.GameVersion") local M = {} -- ------------------------------------------------------------------- -- Pallet Town intro (scripts/PalletTown.asm PalletTownOakHeyWaitScript): -- stepping toward the grass without a starter makes Oak stop you and -- take you to his lab (the walk cutscene is compressed into the warp). -- ------------------------------------------------------------------- -- Pure escort data, exposed on the map entry as `escort` for -- tests/parity_intro.lua. local escort = {} -- FindPathToPlayer (engine/overworld/pathfinding.asm): each step -- reduces whichever axis has more distance left; ties step X first -- (`ld a, e / cp d / jr c, .yDistanceGreater`, e = X left, d = Y left). function escort.findPath(fromX, fromY, toX, toY) local xdist, ydist = math.abs(toX - fromX), math.abs(toY - fromY) local xdir = toX > fromX and "right" or "left" local ydir = toY > fromY and "down" or "up" local xprog, yprog = 0, 0 local path = {} while xprog < xdist or yprog < ydist do if xdist - xprog >= ydist - yprog and xprog < xdist then xprog = xprog + 1 path[#path + 1] = xdir else yprog = yprog + 1 path[#path + 1] = ydir end end return path end -- Red: Oak object (8,5) -> one tile below the player at y=1. -- Yellow: Oak object (10,4); stop fires at y=0 (pokeyellow PalletTown). function escort.oakApproach(playerX) if GameVersion.isYellow() then return escort.findPath(10, 4, playerX, 1) end return escort.findPath(8, 5, playerX, 2) end -- RLEList_ProfOakWalkToLab (engine/overworld/auto_movement.asm). -- Yellow differs: first DOWN is x6 (Oak starts one tile farther north). local function buildOakSteps() if GameVersion.isYellow() then return { "down", "down", "down", "down", "down", "down", "left", "down", "down", "down", "down", "down", "right", "right", "right", "up", } end return { "down", "down", "down", "down", "down", "left", "down", "down", "down", "down", "down", "right", "right", "right", "up", } end escort.oakSteps = buildOakSteps() -- Player stays one step behind Oak (simplified reverse-RLE port). escort.playerSteps = { "down" } for _, d in ipairs(escort.oakSteps) do escort.playerSteps[#escort.playerSteps + 1] = d end local function npcNamed(ow, name) for _, n in ipairs(ow.npcs or {}) do if n.def and n.def.name == name then return n end end return nil end M.PALLET_TOWN = { talk = require("data.scripts.pallet_town").talk, escort = escort, -- Red: stop at y==1 from (8,5). Yellow: stop at y==0 from (10,4), -- then a wild Pikachu battle before the lab escort (pokeyellow -- PalletTownPikachuBattleScript). onStep = function(game, ow, x, y) local yellow = GameVersion.isYellow() local stopY = yellow and 0 or 1 if y ~= stopY or game.save.flags.EVENT_FOLLOWED_OAK_INTO_LAB or game.save.flags.EVENT_GOT_STARTER then return false end local TextBox = require("src.render.TextBox") local Commands = require("src.script.Commands") local Music = require("src.core.Music") local BattleState = require("src.battle.BattleState") local t = game.data.text local ctx = { save = game.save, game = game, overworld = ow } -- Red turns the player down; Yellow faces up at the north exit. ow.player.facing = yellow and "up" or "down" Music.play(game.data, "Music_MeetProfOak") local function hold(frames, npc, cb) ow.emote = { frames = frames, npc = npc, onDone = cb } end local function walkList(entity, steps, done) local i = 0 local function nextStep() i = i + 1 if not entity or not steps[i] then if done then done() end return end ow:scriptMove(entity, steps[i], 1, nextStep) end nextStep() end -- OaksLabOakChooseMonSpeechScript. Yellow's OakChooseMon text is -- the single-ball speech, not Red's "there are 3 POKéMON". local function chooseMonSpeech() local function say(key, fb, next) game.stack:push(TextBox.new(game, t[key] or fb, next)) end say("_OaksLabRivalFedUpWithWaitingText", "{RIVAL}: Gramps!\nI'm fed up with\nwaiting!", function() hold(3, nil, function() say("_OaksLabOakChooseMonText", yellow and "OAK: Look, {PLAYER}! Do\nyou see that ball\non the table?" or "OAK: Here, {PLAYER}!\fThere are 3\nPOKéMON here!\fYou can have one!\nChoose!", function() hold(3, nil, function() say("_OaksLabRivalWhatAboutMeText", "{RIVAL}: Hey!\nGramps! What\nabout me?", function() hold(3, nil, function() say("_OaksLabOakBePatientText", "OAK: Be patient!\n{RIVAL}, you can\nhave one too!", function() game.save.flags.EVENT_OAK_ASKED_TO_CHOOSE_MON = true end) end) end) end) end) end) end) end -- Door Oak is OAKSLAB_OAK2: Red index 8, Yellow index 6 (one ball). local function labWalkIn() local oak2 = npcNamed(ow, "OAKSLAB_OAK2") or ow:npcByIndex(yellow and 6 or 8) local function swapOaks() Commands.hide_object(ctx, "OAKS_LAB", "OAKSLAB_OAK2") Commands.show_object(ctx, "OAKS_LAB", "OAKSLAB_OAK1") hold(3, nil, function() Commands.face_object(ctx, 1, "down") ow:scriptMove(ow.player, "up", 8, function() game.save.flags.EVENT_FOLLOWED_OAK_INTO_LAB = true game.save.flags.EVENT_FOLLOWED_OAK_INTO_LAB_2 = true Commands.face_object(ctx, 1, "up") Music.playMap(game.data, "OAKS_LAB") chooseMonSpeech() end) end) end if oak2 then ow:scriptMove(oak2, "up", 3, swapOaks) else swapOaks() end end local function enterLab() Commands.hide_object(ctx, "PALLET_TOWN", "PALLETTOWN_OAK") Commands.show_object(ctx, "OAKS_LAB", "OAKSLAB_OAK2") ow.doorWarp = true ow:startWarpTo("OAKS_LAB", 5, 11, "up", labWalkIn, { keepMusic = true }) end local function walkToLab(oak) local i = 0 local function tick() i = i + 1 local playerStep = escort.playerSteps[i] if not playerStep then enterLab() return end if oak and escort.oakSteps[i] then ow:scriptMove(oak, escort.oakSteps[i], 1) elseif oak then ow:marchInPlace(oak) end ow:scriptMove(ow.player, playerStep, 1, tick) end tick() end local function escortToLab(oak) local numSteps = x - 10 if oak and numSteps > 0 then ow:scriptMove(oak, "left", numSteps, function() ow:scriptMove(ow.player, "left", numSteps, function() walkToLab(oak) end) end) else walkToLab(oak) end end -- After Oak reaches the player: Red goes straight to "It's unsafe!". -- Yellow (PalletTownOakGreetsPlayerScript..AfterPikachuBattleScript): -- ThatWasClose -> Oak faces the grass patch -> BATTLE_TYPE_PIKACHU -- (the old-man-style simulated battle where PROF.OAK throws the ball -- and always catches the lv5 Pikachu) -> Whew -> ComeWithMe. local function afterOakArrives(oak) if not yellow then game.stack:push(TextBox.new(game, t._PalletTownOakItsUnsafeText or "OAK: It's unsafe!\nWild POKéMON\nlive in tall grass!", function() escortToLab(oak) end)) return end local function comeWithMe() game.stack:push(TextBox.new(game, t._PalletTownOakComeWithMe or "OAK: Here, come with\nme!", function() escortToLab(oak) end)) end local function afterPikaBattle() if oak then oak.facing = "up" end game.stack:push(TextBox.new(game, t._PalletTownOakWhewText or "OAK: Whew...", comeWithMe)) end game.stack:push(TextBox.new(game, t._PalletTownOakThatWasCloseText or "OAK: That was\nclose!\fWild POKéMON live\nin tall grass!", function() -- Oak turns toward the horizontally adjacent grass (left exit -- looks right, right exit looks left -- the -- EVENT_PLAYER_AT_RIGHT_EXIT_TO_PALLET_TOWN branch) if oak then oak.facing = x == 10 and "right" or "left" end local battle = BattleState.newWild(game, "PIKACHU", 5) battle:makeOldManDemo("PROF.OAK") battle.onFinish = function() afterPikaBattle() end game.stack:push(battle) end)) end local function oakAppearsAndWalks() Commands.show_object(ctx, "PALLET_TOWN", "PALLETTOWN_OAK") local oak = npcNamed(ow, "PALLETTOWN_OAK") or ow:npcByIndex(1) if oak then oak.facing = "up" end hold(6, nil, function() walkList(oak, escort.oakApproach(x), function() afterOakArrives(oak) end) end) end game.stack:push(TextBox.new(game, t._PalletTownOakHeyWaitDontGoOutText or "OAK: Hey! Wait!\nDon't go out!", nil, { auto = { delay = 10, overlap = 10, onOverlap = function() -- .HeyWaitDontGoOutText turns the player to face down (toward -- the approaching Oak) before the exclamation bubble ow.player.facing = "down" ow.emote = { npc = ow.player, frames = 50, onDone = oakAppearsAndWalks } end } })) return true end, } -- ------------------------------------------------------------------- -- Saffron gate guards (scripts/Route5Gate.asm etc.): crossing the gate -- without having given them a drink gets you turned back; a drink from -- the bag (bought at Celadon's vending machines... or any mart that -- stocks them) opens all four gates. -- ------------------------------------------------------------------- local DRINKS = { "FRESH_WATER", "SODA_POP", "LEMONADE" } -- Hand over the first drink in the bag, if any. Mirrors RemoveGuardDrink -- (engine/items/inventory.asm), which walks the same three item ids and -- removes ONE, and the caller's BIT_GAVE_SAFFRON_GUARDS_DRINK. local function takeGuardDrink(game) for _, drink in ipairs(DRINKS) do if (game.save.inventory[drink] or 0) > 0 then game.save.inventory[drink] = game.save.inventory[drink] - 1 if game.save.inventory[drink] == 0 then game.save.inventory[drink] = nil end game.save.flags.EVENT_GAVE_GUARDS_DRINK = true return true end end return false end local function saffronGate(guardText, triggers, horizontal) return { talk = { [guardText] = function(game, ow, npc, done) local TextBox = require("src.render.TextBox") local t = game.data.text if game.save.flags.EVENT_GAVE_GUARDS_DRINK then game.stack:push(TextBox.new(game, t._SaffronGateGuardThanksForTheDrinkText or "Gee, that was\ntasty!", done)) return end if takeGuardDrink(game) then game.stack:push(TextBox.new(game, t._SaffronGateGuardImParchedText or "Whoa, boy!\nI'm parched!", function() game.stack:push(TextBox.new(game, (t._SaffronGateGuardYouCanGoOnThroughText or "You can go on\nthrough!"), done)) end)) return end game.stack:push(TextBox.new(game, t._SaffronGateGuardGeeImThirstyText or "Gee, I'm thirsty\nthough!", done)) end, }, -- the gate's trigger cells (each gate's PlayerInCoordsArray): -- without the drink flag you get walked back the way you came onStep = function(game, ow, x, y) local hit = false for _, c in ipairs(triggers) do if x == c[1] and y == c[2] then hit = true break end end if not hit then return false end if game.save.flags.EVENT_GAVE_GUARDS_DRINK then return false end local TextBox = require("src.render.TextBox") local t = game.data.text -- Stepping on the trigger WITH a drink hands it over right here. -- -- Route5GateDefaultScript (scripts/Route5Gate.asm) runs -- `farcall RemoveGuardDrink` before it decides anything: the coord -- trigger itself takes the drink and sets -- BIT_GAVE_SAFFRON_GUARDS_DRINK, and only a player carrying nothing -- gets the thirsty line and the walk-back. We had the removal on the -- guard's TALK handler only, so walking up with a FRESH_WATER in the -- bag was turned away and the four gates stayed shut unless you -- happened to talk to him -- which vanilla never requires. -- -- Saffron is the middle of the map, so this sealed it: every route -- through the city (Celadon <-> Lavender, Vermilion <-> Cerulean the -- short way) was unreachable, and the bot could not get to Lavender -- for the POKE_FLUTE at all. if takeGuardDrink(game) then game.stack:push(TextBox.new(game, t._SaffronGateGuardImParchedText or "Whoa, boy!\nI'm parched!", function() game.stack:push(TextBox.new(game, (t._SaffronGateGuardYouCanGoOnThroughText or "You can go on\nthrough!"))) end)) return true end local back if horizontal then back = ow.player.facing == "left" and "right" or "left" else back = ow.player.facing == "up" and "down" or "up" end game.stack:push(TextBox.new(game, t._SaffronGateGuardGeeImThirstyText or "Gee, I'm thirsty\nthough!\nThe road's closed.", function() ow:scriptMove(ow.player, back, 1) end)) return true end, } end M.ROUTE_5_GATE = saffronGate("TEXT_ROUTE5GATE_GUARD", { { 3, 3 }, { 4, 3 } }) M.ROUTE_6_GATE = saffronGate("TEXT_ROUTE6GATE_GUARD", { { 3, 2 }, { 4, 2 } }) M.ROUTE_7_GATE = saffronGate("TEXT_ROUTE7GATE_GUARD", { { 3, 3 }, { 3, 4 } }, true) M.ROUTE_8_GATE = saffronGate("TEXT_ROUTE8GATE_GUARD", { { 2, 3 }, { 2, 4 } }, true) -- ------------------------------------------------------------------- -- Bike Voucher chain (scripts/PokemonFanClub.asm, BikeShop.asm) -- ------------------------------------------------------------------- M.POKEMON_FAN_CLUB = { talk = { TEXT_POKEMONFANCLUB_CHAIRMAN = { { "face_player" }, -- 1 { "check_flag", "EVENT_RECEIVED_BIKE_VOUCHER" }, -- 2 { "jump_if_true", 9 }, -- 3 { "show_text", "_PokemonFanClubChairmanIntroText" }, -- 4 { "show_text", "_PokemonFanClubChairmanStoryText" }, -- 5 -- give-then-print like scripts/PokemonFanClub.asm (GiveItem -- fills wStringBuffer; the received text reads it) { "give_item", "BIKE_VOUCHER", 1, false }, -- 6 { "show_text", "_PokemonFanClubReceivedBikeVoucherText" }, -- 7 { "set_flag", "EVENT_RECEIVED_BIKE_VOUCHER" }, -- 8 { "show_text", "_PokemonFanClubExplainBikeVoucherText" }, -- 9 }, }, } -- BikeShopClerkText (scripts/BikeShop.asm) runs three ways: the BICYCLE is -- already yours, you are carrying the BIKE VOUCHER, or you get the sales -- pitch. The pitch draws its own window (TextBoxBorder hlcoord 0,0, b=4 -- c=15) holding BikeShopMenuText and BikeShopMenuPrice, and leaves it up -- while the clerk keeps talking in the bottom box: the original never -- erases it before TextScriptEnd (#568). local BikeShopWindow = {} BikeShopWindow.__index = BikeShopWindow function BikeShopWindow.new(game, footer, onChoose) local self = setmetatable({}, BikeShopWindow) self.game = game self.onChoose = onChoose self.index = 1 self.active = true -- BikeShopClerkDoYouLikeItText stays on screen under the window for as -- long as the menu is up. The text box pushed on top types it out and -- pops itself; this copy of its last page takes over from there, so the -- bottom box never blanks between the pitch and the answer. Paginated -- with the themed column budget so the copy breaks where the box did. local TextBox = require("src.render.TextBox") local box = require("src.ui.Theme").textBox or {} local pages = TextBox.paginate(TextBox.substitute(game, footer), box.maxCols) self.footer = pages[#pages] return self end function BikeShopWindow:update() -- one answer only: the boxes pushed by onChoose sit on top of this -- state, but a second A on the same frame must not fire it twice if not self.active then return end local input = self.game.input -- HandleMenuInput with wMenuWrappingEnabled clear: the two rows clamp if input:wasPressed("up") then self.index = 1 elseif input:wasPressed("down") then self.index = 2 elseif input:wasPressed("a") or input:wasPressed("b") then local cancelled = input:wasPressed("b") -- bit B_PAD_B -> .cancel require("src.core.Sound").play(self.game.data, "Press_AB") self.active = false self.onChoose(not cancelled and self.index == 1) end end function BikeShopWindow:draw() local Font = require("src.render.Font") local Strings = require("src.core.Strings") local Theme = require("src.ui.Theme") Font.drawBox(0, 0, 17, 6) love.graphics.setColor(0, 0, 0, 1) local bike = self.game.data.items.BICYCLE Font.draw(bike and bike.name or "BICYCLE", 16, 16) -- hlcoord 2, 2 Font.draw("¥1000000", 64, 24) -- hlcoord 8, 3 Font.draw(Strings("CANCEL"), 16, 32) -- `next` skips a row -- wTopMenuItemX 1, wTopMenuItemY 2, rows two apart Font.drawCode(Theme.cursor, 8, self.index == 1 and 16 or 32) if self.footer then -- same geometry TextBox resolves against, so a themed box matches local box = Theme.textBox or {} local tx, ty = box.tx or 0, box.ty or 12 love.graphics.setColor(1, 1, 1, 1) Font.drawBox(tx, ty, box.tw or 20, box.th or 6) love.graphics.setColor(0, 0, 0, 1) for i, line in ipairs(self.footer) do Font.draw(line, (tx + 1) * 8, (ty + 2 * i) * 8) end end love.graphics.setColor(1, 1, 1, 1) end M.BIKE_SHOP = { talk = { TEXT_BIKESHOP_CLERK = function(game, ow, npc, done) local Bag = require("src.inventory.Bag") local Flags = require("src.script.Flags") local TextBox = require("src.render.TextBox") local t = game.data.text local function say(text, after, opts) game.stack:push(TextBox.new(game, text, after, opts)) end -- CheckEvent EVENT_GOT_BICYCLE. Saves made before the clerk started -- setting the event still have the bike in the bag, so either counts. if (game.save.inventory.BICYCLE or 0) > 0 or Flags.get(game.save, "EVENT_GOT_BICYCLE") then say(t._BikeShopClerkHowDoYouLikeYourBicycleText, done) return end -- .dontHaveBike: IsItemInBag BIKE_VOUCHER if (game.save.inventory.BIKE_VOUCHER or 0) > 0 then say(t._BikeShopClerkOhThatsAVoucherText, function() -- GiveItem's `jr nc, .BagFull`: the voucher is only spent once -- the BICYCLE is actually in the bag if not Bag.add(game.save, "BICYCLE", 1) then say(t._BikeShopBagFullText, done) return end Bag.remove(game.save, "BIKE_VOUCHER", 1) Flags.set(game.save, "EVENT_GOT_BICYCLE") -- BikeShopExchangedVoucherText carries sound_get_key_item; the -- map runs EnableAutoTextBoxDrawing, so the box still waits for -- a button once the jingle has played (auto.wait, #247) say(t._BikeShopExchangedVoucherText, done, { auto = { wait = true, sound = function() return require("src.core.Sound").play(game.data, "Get_Key_Item") end }, }) end) return end -- .dontHaveVoucher: welcome, then the BICYCLE/CANCEL window say(t._BikeShopClerkWelcomeText, function() local pitch = t._BikeShopClerkDoYouLikeItText game.stack:push(BikeShopWindow.new(game, pitch, function(bought) local function comeAgain() say(t._BikeShopComeAgainText, function() game.stack:pop() -- the window, still up under the text done() end) end if bought then -- a million is out of anyone's reach: BikeShopCantAffordText say(t._BikeShopCantAffordText, comeAgain) else comeAgain() end end)) -- PrintText BikeShopClerkDoYouLikeItText, then straight into -- HandleMenuInput: the box types out and hands over without -- waiting, leaving the window's copy of the line on screen say(pitch, nil, { auto = { delay = 0 } }) end) end, }, } -- ------------------------------------------------------------------- -- Fossils (scripts/MtMoonB2F.asm, Museum1F.asm, -- CinnabarLabFossilRoom.asm): pick one Mt Moon fossil, revive them at -- the Cinnabar lab (the wait is skipped). -- ------------------------------------------------------------------- -- The Super Nerd (object index 1) claims both fossils and blocks the way -- to them. In scripts/MtMoonB2F.asm he isn't a sight-line trainer: his -- header carries no range, so MtMoonB2FDefaultScript force-triggers the -- battle the instant the player steps onto (13,8) -- the chokepoint tile -- to his right -- and reaching for a fossil intercepts you too. local function superNerdBeaten(ow) local nerd = ow:npcByIndex(1) return not nerd or ow:trainerDefeated(nerd) end local function engageSuperNerd(game, ow, onDone) local nerd = ow:npcByIndex(1) if not nerd or ow:trainerDefeated(nerd) then if onDone then onDone() end return end nerd:facePlayer(ow.player) ow:engageTrainer(nerd, onDone) end -- MtMoonB2FMoveSuperNerdScript: player-near-dome coords walk RIGHT then -- UP (MoveRight falls through into MoveUp); near-helix walks UP only. local function mtMoonNerdWalk(px, py, itemId) if (px == 12 and py == 7) or (px == 11 and py == 6) or (px == 12 and py == 5) then return { "right", "up" } end if (px == 13 and py == 7) or (px == 14 and py == 6) or (px == 14 and py == 5) then return { "up" } end return (itemId == "DOME_FOSSIL") and { "right", "up" } or { "up" } end -- scripts/MtMoonB2F.asm Dome/Helix fossil text_asm + MoveSuperNerd + -- SuperNerdTakesOtherFossil: pick one, hide it, walk the nerd to the -- other, "All right. Then this is mine!", hide the other. local function mtMoonFossil(itemId, otherName, gotFlag) return function(game, ow, npc, done) local TextBox = require("src.render.TextBox") local t = game.data.text local flags = game.save.flags if flags.EVENT_GOT_DOME_FOSSIL or flags.EVENT_GOT_HELIX_FOSSIL then done() return end if not superNerdBeaten(ow) then engageSuperNerd(game, ow, done) return end local want = (itemId == "DOME_FOSSIL") and (t._MtMoonB2FDomeFossilYouWantText or "You want the\nDOME FOSSIL?") or (t._MtMoonB2FHelixFossilYouWantText or "You want the\nHELIX FOSSIL?") game.stack:push(TextBox.new(game, want, nil, { choice = function(yes) if not yes then done() return end if not require("src.inventory.Bag").add(game.save, itemId, 1) then game.stack:push(TextBox.new(game, t._MtMoonB2FYouHaveNoRoomText or "Look, you've got\nno room for this.", done)) return end local idef = game.data.items[itemId] game.stringBuffer = idef and idef.name or itemId require("src.core.Sound").play(game.data, "Get_Key_Item") local dirs = mtMoonNerdWalk(ow.player.cellX, ow.player.cellY, itemId) game.stack:push(TextBox.new(game, t._MtMoonB2FReceivedFossilText or ("{PLAYER} got the\n" .. game.stringBuffer .. "!"), function() local Commands = require("src.script.Commands") Commands.hide_object( { save = game.save, overworld = ow, game = game }, "MT_MOON_B2F", npc.def.name) flags[gotFlag] = true ow.runner:run({ { "walk_npc", 1, dirs }, { "text_opts", { auto = true } }, { "show_text", "_MtMoonB2FSuperNerdThenThisIsMineText" }, { "play_sound", "Get_Key_Item" }, { "hide_object", "MT_MOON_B2F", otherName }, }, { onDone = done }) end)) end })) end end M.MT_MOON_B2F = { -- MtMoonB2FDefaultScript forces the Super Nerd battle when the player -- steps onto (13,8), the tile beside him guarding the fossils. onStep = function(game, ow, x, y) if x == 13 and y == 8 and not superNerdBeaten(ow) then engageSuperNerd(game, ow, nil) return true end return false end, talk = { TEXT_MTMOONB2F_DOME_FOSSIL = mtMoonFossil( "DOME_FOSSIL", "MTMOONB2F_HELIX_FOSSIL", "EVENT_GOT_DOME_FOSSIL"), TEXT_MTMOONB2F_HELIX_FOSSIL = mtMoonFossil( "HELIX_FOSSIL", "MTMOONB2F_DOME_FOSSIL", "EVENT_GOT_HELIX_FOSSIL"), }, } -- The ticket clerk (scripts/Museum1F.asm Museum1FScientist1Text): -- Y50, once. Declining at the rope shoves the player one tile SOUTH back off -- the exhibit rope they crossed heading north (#151); the museum floor has no -- ledges, so a plain scriptMove("down",1) is the correct primitive. local function museumClerk(game, ow, done, onDecline) local TextBox = require("src.render.TextBox") local ChoiceBox = require("src.ui.ChoiceBox") if game.save.flags.EVENT_BOUGHT_MUSEUM_TICKET then game.stack:push(TextBox.new(game, "Take your time,\nand enjoy it all!", done)) return end game.stack:push(TextBox.new(game, "It's ¥50 for a\nchild's ticket.\fWould you like to\ncome in?", function() game.stack:push(ChoiceBox.new(game, function(yes) if yes and game.save.money >= 50 then game.save.money = game.save.money - 50 game.save.flags.EVENT_BOUGHT_MUSEUM_TICKET = true game.stack:push(TextBox.new(game, "Right, ¥50!\nThank you!", done)) elseif yes then game.stack:push(TextBox.new(game, "You don't have\nenough money.", onDecline or done)) else game.stack:push(TextBox.new(game, "Come again!", onDecline or done)) end end)) end)) end M.MUSEUM_1F = { -- crossing the rope at (9,4)/(10,4) without a ticket calls the clerk -- over (Museum1FDefaultScript's coordinate check) onStep = function(game, ow, x, y) if y == 4 and (x == 9 or x == 10) and not game.save.flags.EVENT_BOUGHT_MUSEUM_TICKET then museumClerk(game, ow, nil, function() ow:scriptMove(ow.player, "down", 1) end) return true end return false end, talk = { TEXT_MUSEUM1F_SCIENTIST1 = function(game, ow, npc, done) museumClerk(game, ow, done) end, -- The OLD AMBER display object is plain flavor; the scientist -- (TEXT_MUSEUM1F_SCIENTIST2, data/scripts/flavor/museum_1f.lua) is who -- hands it over and hides this object, per scripts/Museum1F.asm. TEXT_MUSEUM1F_OLD_AMBER = function(game, ow, npc, done) local TextBox = require("src.render.TextBox") game.stack:push(TextBox.new(game, game.data.text._Museum1FOldAmberText or "The OLD AMBER.", done)) end, }, } local FOSSIL_MONS = { HELIX_FOSSIL = "OMANYTE", DOME_FOSSIL = "KABUTO", OLD_AMBER = "AERODACTYL", } -- Deterministic scan order mirrors FossilsList (scripts/ -- CinnabarLabFossilRoom.asm lines 43-47): DOME_FOSSIL, HELIX_FOSSIL, -- OLD_AMBER (the old pairs()-order loop this replaced was undefined). local FOSSIL_ORDER = { "DOME_FOSSIL", "HELIX_FOSSIL", "OLD_AMBER" } -- fills the {PLAYER}/{RAM:...} placeholders in the extracted text -- verbatim (text/CinnabarLabFossilRoom.asm); TextBox itself only knows -- how to substitute {RAM:wStringBuffer}, so this has to happen first. -- RAM placeholders resolve by buffer name from subs (SeesFossilText -- reads both wNameBuffer and wStringBuffer), falling back to subs.ram. local function fillFossilText(s, subs) s = s:gsub("{PLAYER}", subs.player or "") s = s:gsub("{RAM:([^}]*)}", function(name) return subs[name] or subs.ram or "" end) return s end M.CINNABAR_LAB_FOSSIL_ROOM = { talk = { -- Fossil revival quest (scripts/CinnabarLabFossilRoom.asm -- CinnabarLabFossilRoomScientist1Text lines 49-99, deposit flow in -- engine/events/cinnabar_lab.asm GiveFossilToCinnabarLab): deposit -- a fossil -> pending for the rest of this visit -> ready once the -- player leaves and re-enters the CINNABAR_ISLAND overworld map -- (M.CINNABAR_ISLAND.onEnter in data/scripts/story5.lua clears -- EVENT_LAB_STILL_REVIVING_FOSSIL there, mirroring CinnabarIsland. -- asm line 6) -> hand over the mon and reset the whole quest so a -- second fossil can be deposited later. -- -- The deposit itself follows GiveFossilToCinnabarLab: a bordered -- top-left menu of every carried fossil (A/B watched; B backs out), -- then SeesFossilText with a Yes/No confirm; both cancel paths -- (B on the menu, NO on the confirm) share ComeAgainText. TEXT_CINNABARLABFOSSILROOM_SCIENTIST1 = function(game, ow, npc, done) local TextBox = require("src.render.TextBox") local t = game.data.text local f = game.save.flags local subs = { player = game.save.player.name } if f.EVENT_GAVE_FOSSIL_TO_LAB then if f.EVENT_LAB_STILL_REVIVING_FOSSIL then -- .check_done_reviving -> still pending this visit game.stack:push(TextBox.new(game, t._CinnabarLabFossilRoomScientist1GoForAWalkText or "I take a little\ntime!\fYou go for walk a\nlittle while!", done)) return end -- STILL_REVIVING was cleared (CINNABAR_ISLAND was reloaded -- since the deposit): .done_reviving, lines 72-83 local species = game.save.labFossilMon subs.ram = species and game.data.pokemon[species] and game.data.pokemon[species].name or "" f.EVENT_LAB_HANDING_OVER_FOSSIL_MON = true game.stack:push(TextBox.new(game, fillFossilText( t._CinnabarLabFossilRoomScientist1FossilIsBackToLifeText or "Where were you?\fYour fossil is\nback to life!\fIt was {RAM:x}\nlike I think!", subs), function() if species then local Commands = require("src.script.Commands") local ctx = { save = game.save, game = game, overworld = ow } Commands.give_pokemon(ctx, species, 30) if not ctx.lastCheck then -- GivePokemon failed (party+box full): pokered's -- `jr nc, .done` leaves the quest pending so the -- scientist re-offers the mon next visit instead of -- destroying it. game.stack:push(TextBox.new(game, t._BoxIsFullText or "Box is full!", done)) return end end game.save.labFossilMon = nil f.EVENT_GAVE_FOSSIL_TO_LAB = nil f.EVENT_LAB_STILL_REVIVING_FOSSIL = nil f.EVENT_LAB_HANDING_OVER_FOSSIL_MON = nil done() end)) return end -- No fossil deposited yet: the intro always plays first (.Text), -- then either the fossil-select menu (GiveFossilToCinnabarLab) -- or NoFossilsText. game.stack:push(TextBox.new(game, t._CinnabarLabFossilRoomScientist1Text or "Hiya!\fI am important\ndoctor!\fI study here rare\nPOKéMON fossils!\fYou! Have you a\nfossil for me?", function() -- Lab4Script_GetFossilsInBag: every carried fossil, in -- FossilsList order local carried = {} for _, fossil in ipairs(FOSSIL_ORDER) do if (game.save.inventory[fossil] or 0) > 0 then carried[#carried + 1] = fossil end end if #carried == 0 then game.stack:push(TextBox.new(game, t._CinnabarLabFossilRoomScientist1NoFossilsText or "No! Is too bad!", done)) return end -- .cancelledGivingFossil: B on the menu and NO on the -- confirm both land here local function comeAgain() game.stack:push(TextBox.new(game, t._CinnabarLabFossilRoomScientist1ComeAgainText or "Aiyah! You come\nagain!", done)) end local items = {} for _, fossil in ipairs(carried) do items[#items + 1] = { label = game.data.items[fossil].name, onSelect = function() -- LoadFossilItemAndMonName: wNameBuffer = item name, -- wStringBuffer = mon name; then .ScientistSeesFossilText -- with YesNoChoice (cursor starts on YES) local species = FOSSIL_MONS[fossil] local def = game.data.pokemon[species] subs.wNameBuffer = game.data.items[fossil].name subs.wStringBuffer = def and def.name or species game.stack:push(TextBox.new(game, fillFossilText( t._CinnabarLabFossilRoomScientist1SeesFossilText or "Oh! That is\n{RAM:wNameBuffer}!\fIt is fossil of\n{RAM:wStringBuffer}, a\nPOKéMON that is\nalready extinct!\fMy Resurrection\nMachine will make\nthat POKéMON live\nagain!", subs), nil, { choice = function(yes) if not yes then comeAgain() return end -- YES: TakesFossilText, RemoveItemByID, GoForAWalk2, -- SetEvents GAVE_FOSSIL_TO_LAB + STILL_REVIVING require("src.inventory.Bag").remove(game.save, fossil, 1) game.save.labFossilMon = species f.EVENT_GAVE_FOSSIL_TO_LAB = true f.EVENT_LAB_STILL_REVIVING_FOSSIL = true game.stack:push(TextBox.new(game, fillFossilText( t._CinnabarLabFossilRoomScientist1TakesFossilText or "So! You hurry and\ngive me that!\f{PLAYER} handed\nover {RAM:wNameBuffer}!", subs), function() game.stack:push(TextBox.new(game, t._CinnabarLabFossilRoomScientist1GoForAWalkText2 or "I take a little\ntime!\fYou go for walk a\nlittle while!", done)) end)) end })) end, } end -- GiveFossilToCinnabarLab's menu: TextBoxBorder at 0,0 -- (interior width $d, height 2 per fossil), A|B watched local Menu = require("src.ui.Menu") game.stack:push(Menu.new(game, items, { tx = 0, ty = 0, tw = 15, onCancel = comeAgain })) end)) end, -- the other scientist trades SAILOR: Ponyta -> Seel -- (scripts/CinnabarLabFossilRoom.asm TRADE_FOR_SAILOR) TEXT_CINNABARLABFOSSILROOM_SCIENTIST2 = { { "face_player" }, { "trade", 4, "EVENT_TRADED_PONYTA_FOR_SEEL" }, }, }, } -- ------------------------------------------------------------------- -- Day-care (scripts/Daycare.asm): the boarded Pokémon earns 1 exp per -- step; the fee is ¥100 plus ¥100 per level gained. -- -- #118: do not raise mon.level until a paid retrieve (pokered reverts -- wDayCareMonBoxLevel on .leaveMonInDayCare). Fold pending steps into -- mon.exp once and clear them so a second talk cannot re-apply the same -- walk. Fill {RAM:wNameBuffer}/{RAM:wDayCareMonName}/{NUM:...} here -- -- TextBox.TOKENS.RAM only knows wStringBuffer. -- ------------------------------------------------------------------- local function fillDaycareText(s, subs) s = s:gsub("{PLAYER}", subs.player or "") s = s:gsub("{RAM:([^}]*)}", function(name) return subs[name] or "" end) -- extractor NUM spans may include flags after a comma; keep the name s = s:gsub("{NUM:([%w_]+)[^}]*}", function(name) return tostring(subs[name] or "0") end) return s end M.DAYCARE = { talk = { TEXT_DAYCARE_GENTLEMAN = function(game, ow, npc, done) local TextBox = require("src.render.TextBox") local t = game.data.text local dc = game.save.daycare local playerName = game.save.player and game.save.player.name or "RED" local function monName(mon) local def = game.data.pokemon[mon.species] return mon.nickname or (def and def.name) or mon.species end if dc and dc.mon then local Growth = require("src.pokemon.Growth") local Stats = require("src.pokemon.Stats") local Party = require("src.pokemon.Party") local mon = dc.mon local def = game.data.pokemon[mon.species] -- Apply deferred step-exp once (OverworldController only bumps -- daycare.steps). Clearing prevents double-count on re-talk. mon.exp = (mon.exp or 0) + (dc.steps or 0) dc.steps = 0 -- depositLevel mirrors wDayCareMonBoxLevel: fee baseline that -- must survive a declined retrieve. Fall back to mon.level for -- older saves that predate the field. if dc.depositLevel == nil then dc.depositLevel = mon.level end local startLevel = dc.depositLevel local newLevel = Growth.levelForExp(def and def.growthRate, mon.exp) if newLevel >= 100 then newLevel = 100 if def then mon.exp = Growth.expForLevel(def.growthRate, 100) end end local levelsGrown = math.max(0, newLevel - startLevel) local fee = 100 + levelsGrown * 100 local name = monName(mon) local subs = { player = playerName, wNameBuffer = name, wDayCareMonName = name, wDayCareNumLevelsGrown = levelsGrown, wDayCareTotalCost = fee, } local statusText = levelsGrown > 0 and (t._DaycareGentlemanMonHasGrownText or "Your {RAM:wNameBuffer}\nhas grown a lot!\fBy level, it's\ngrown by {NUM:wDayCareNumLevelsGrown, 1, 3}!\fAren't I great?") or (t._DaycareGentlemanMonNeedsMoreTimeText or "Back already?\nYour {RAM:wNameBuffer}\nneeds some more\ntime with me.") game.stack:push(TextBox.new(game, fillDaycareText(statusText, subs), function() if #game.save.party >= Party.MAX then game.stack:push(TextBox.new(game, t._DaycareGentlemanNoRoomForMonText or "You have no room\nfor this POKéMON!", done)) return end game.stack:push(TextBox.new(game, fillDaycareText( t._DaycareGentlemanOweMoneyText or "You owe me ¥{NUM:wDayCareTotalCost, 2 | LEADING_ZEROES | LEFT_ALIGN}\nfor the return\nof this POKéMON.", subs), nil, { choice = function(yes) if not yes then -- .leaveMonInDayCare: revert any transient level bump mon.level = startLevel game.stack:push(TextBox.new(game, (t._DaycareGentlemanAllRightThenText or "All right then,\n") .. (t._DaycareGentlemanComeAgainText or "come again."), done)) return end if (game.save.money or 0) < fee then mon.level = startLevel game.stack:push(TextBox.new(game, t._DaycareGentlemanNotEnoughMoneyText or "Hey, you don't\nhave enough ¥!", done)) return end game.save.money = game.save.money - fee mon.level = newLevel if def then mon.stats = Stats.calc(def, mon.level, mon.dvs, mon.statExp) mon.hp = mon.stats.hp -- Daycare.asm: WriteMonMoves with wLearningMovesFromDayCare local Pokemon = require("src.pokemon.Pokemon") Pokemon.learnMovesFromDayCare( game.data, mon, def, startLevel, newLevel) end table.insert(game.save.party, mon) game.save.daycare = nil game.stack:push(TextBox.new(game, t._DaycareGentlemanHeresYourMonText or "Thank you! Here's\nyour POKéMON!", function() game.stack:push(TextBox.new(game, fillDaycareText( t._DaycareGentlemanGotMonBackText or "{PLAYER} got\n{RAM:wDayCareMonName} back!", subs), done)) end)) end })) end)) return end game.stack:push(TextBox.new(game, t._DaycareGentlemanIntroText or "I run a DAYCARE.\nWould you like me\nto raise one of\nyour POKéMON?", nil, { choice = function(yes) if not yes then game.stack:push(TextBox.new(game, t._DaycareGentlemanComeAgainText or "come again.", done)) return end if #game.save.party < 2 then game.stack:push(TextBox.new(game, t._DaycareGentlemanOnlyHaveOneMonText or "You only have one\nPOKéMON with you.", done)) return end game.stack:push(TextBox.new(game, t._DaycareGentlemanWhichMonText or "Which POKéMON\nshould I raise?", function() local PartyMenu = require("src.ui.PartyMenu") game.stack:push(PartyMenu.new(game, { pickOnly = true, onSwitch = function(mon) for i, m in ipairs(game.save.party) do if m == mon then table.remove(game.save.party, i) break end end local name = monName(mon) -- depositLevel = wDayCareMonBoxLevel at deposit time game.save.daycare = { mon = mon, steps = 0, depositLevel = mon.level, } game.stack:push(TextBox.new(game, fillDaycareText( t._DaycareGentlemanWillLookAfterMonText or "Fine, I'll look\nafter {RAM:wNameBuffer}\nfor a while.", { player = playerName, wNameBuffer = name }), function() game.stack:push(TextBox.new(game, t._DaycareGentlemanComeSeeMeInAWhileText or "Come see me in\na while.", done)) end)) end, })) end)) end })) end, }, } return M