-- 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; each cites its pokered source. 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 -- PalletTownOakWalksToPlayerScript: Oak appears at his object spot -- (8,5) and zigzags to one tile below the player (hNPCPlayerYDistance -- is pre-decremented before predef FindPathToPlayer). function escort.oakApproach(playerX) return escort.findPath(8, 5, playerX, 2) end -- RLEList_ProfOakWalkToLab (engine/overworld/auto_movement.asm): -- DOWN x5, LEFT, DOWN x5, RIGHT x3, UP -- Oak's last step lands on the -- lab door (12,11). (The trailing NPC_CHANGE_FACING is a march-in- -- place beat on the mat; here Oak just stands his final beat.) escort.oakSteps = { "down", "down", "down", "down", "down", "left", "down", "down", "down", "down", "down", "right", "right", "right", "up", } -- RLEList_PlayerWalkToLab decodes to UP x2, RIGHT x3, DOWN x5, LEFT, -- DOWN x6 and plays in REVERSE buffer order (wSimulatedJoypadStatesEnd -- grows downward): DOWN x6, LEFT, DOWN x5, RIGHT x3, UP x2 -- Oak's -- exact path one step behind. The 17th press (the second UP) is eaten -- by the door-warp frame (WarpFound clears hJoyHeld via EnterMap), so -- only 16 real steps happen; the walk ends on the door at (12,11). escort.playerSteps = { "down" } for _, d in ipairs(escort.oakSteps) do escort.playerSteps[#escort.playerSteps + 1] = d end M.PALLET_TOWN = { talk = require("data.scripts.pallet_town").talk, escort = escort, -- Oak stops you at the north row (PalletTownDefaultScript's -- `wYCoord == 1` check), walks up from (8,5), and leads you to his -- lab with the player one step behind (scripts/PalletTown.asm + -- PalletMovementScriptPointerTable in -- engine/overworld/auto_movement.asm), then the lab walk-in and the -- choose-mon exchange (scripts/OaksLab.asm OaksLabDefaultScript .. -- OaksLabOakChooseMonSpeechScript). onStep = function(game, ow, x, y) if y ~= 1 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 t = game.data.text local ctx = { save = game.save, game = game, overworld = ow } -- PalletTownDefaultScript: stop the player, turn them around -- (wPlayerMovingDirection = PLAYER_DIR_DOWN applies on the very -- next frame, before the text box opens) and strike up the "oak -- appears" theme (MUSIC_MEET_PROF_OAK) ow.player.facing = "down" Music.play(game.data, "Music_MeetProfOak") -- DelayFrames-style hold: the world pauses (input stays locked) -- for `frames` frames, then cb runs. Reuses the emote pause slot; -- with an `npc` the "!" bubble draws above it (EmotionBubble). local function hold(frames, npc, cb) ow.emote = { frames = frames, npc = npc, onDone = cb } end -- chain single-tile scriptMoves through a direction list 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 -- ---- Oak's Lab side (scripts/OaksLab.asm) ---------------------- -- OaksLabOakChooseMonSpeechScript: the fed-up / choose-mon / -- what-about-me / be-patient exchange, Delay3 between boxes 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", "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 -- entering the lab: the door Oak (OAKSLAB_OAK2, (5,10)) walks up 3 -- ahead of the player (OaksLabOakEntersLabScript OakEntryMovement), -- swaps for the desk Oak (OAKSLAB_OAK1, (5,2)), then the player -- walks up 8 from the mat (PlayerEntryMovementRLE) while the rival -- and Oak turn with them (OaksLabPlayerEntersLabScript / -- OaksLabFollowedOakScript) local function labWalkIn() local oak2 = ow:npcByIndex(8) local function swapOaks() Commands.hide_object(ctx, "OAKS_LAB", "OAKSLAB_OAK2") Commands.show_object(ctx, "OAKS_LAB", "OAKSLAB_OAK1") hold(3, nil, function() -- Delay3 Commands.face_object(ctx, 1, "down") -- rival watches you pass ow:scriptMove(ow.player, "up", 8, function() -- OaksLabFollowedOakScript: flags only after the walk-in, so a -- stray step on the door mat can't fire the "don't go away" -- push-up (oaks_lab.lua onStep) mid-cutscene. Outdoor escort -- still re-arms on F1 mid-escort -- these flags stay clear -- until the lab walk finishes. 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") -- res BIT_NO_MAP_MUSIC + PlayDefaultMusic: the lab theme -- only starts once the walk-in is done Music.playMap(game.data, "OAKS_LAB") chooseMonSpeech() end) end) end if oak2 then ow:scriptMove(oak2, "up", 3, swapOaks) else swapOaks() end end -- PalletMovementScript_Done + the door warp: Oak is hidden as the -- player steps into the doorway; PALLET_TOWN warp 3 -> OAKS_LAB -- warp 2 = (5,11), with the door SFX (WarpFound -> SFX_GO_INSIDE) 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 -- PalletMovementScript_WalkToLab: Oak's NPC movement and the -- player's simulated joypad run simultaneously, in lockstep; the -- player retraces Oak's path one step behind and follows him into -- the doorway on the final beat 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 -- RLEList_ProfOakWalkToLab's trailing NPC_CHANGE_FACING beat: -- Oak marches in place on the door mat while the player takes -- the final step up behind him (movement.asm ChangeFacingDirection) ow:marchInPlace(oak) end ow:scriptMove(ow.player, playerStep, 1, tick) end tick() end -- PalletMovementScript_OakMoveLeft/_PlayerMoveLeft: from the right -- tile (x == 11) Oak sidesteps left first, then the player follows -- left (wNumStepsToTake = wXCoord - 10), and only then both walk 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 -- ---- Pallet Town side (scripts/PalletTown.asm) ----------------- -- PalletTownOakWalksToPlayerScript: Oak appears at (8,5), faces up -- (SetSpriteFacingDirectionAndDelay + Delay3), then zigzags to the -- player; the "It's unsafe!" text follows and the escort begins local function oakAppearsAndWalks() Commands.show_object(ctx, "PALLET_TOWN", "PALLETTOWN_OAK") local oak = ow:npcByIndex(1) if oak then oak.facing = "up" end hold(6, nil, function() walkList(oak, escort.oakApproach(x), function() -- PalletTownOakNotSafeComeWithMeScript: the second text waits -- for a button, then the escort starts game.stack:push(TextBox.new(game, t._PalletTownOakItsUnsafeText or "OAK: It's unsafe!\nWild POKéMON\nlive in tall grass!", function() escortToLab(oak) end)) end) end) end -- The "Hey! Wait!" box ends without a button wait (auto), then the -- "!" bubble shows over the player WHILE the box is still on screen -- (PalletTownOakText: DelayFrames 10 then EmotionBubble, box not yet -- cleared). onOverlap sets the bubble during the box's last frames; -- the box pops after `overlap`, and the bubble's 60-frame hold then -- runs to Oak's appearance (the bubble is static while the box is up, -- since the overworld pauses under it, so 10 overlap + 50 = 60). game.stack:push(TextBox.new(game, t._PalletTownOakHeyWaitDontGoOutText or "OAK: Hey! Wait!\nDon't go out!", nil, { auto = { delay = 10, overlap = 10, onOverlap = function() 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 }, }, } M.BIKE_SHOP = { talk = { TEXT_BIKESHOP_CLERK = function(game, ow, npc, done) local TextBox = require("src.render.TextBox") if (game.save.inventory.BICYCLE or 0) > 0 then game.stack:push(TextBox.new(game, "How's the\nBICYCLE treating\nyou?", done)) elseif (game.save.inventory.BIKE_VOUCHER or 0) > 0 then game.save.inventory.BIKE_VOUCHER = nil game.save.inventory.BICYCLE = 1 game.stack:push(TextBox.new(game, ("Oh, that's a\nBIKE VOUCHER!\f%s exchanged\nit for a BICYCLE!") :format(game.save.player.name), done)) else game.stack:push(TextBox.new(game, "A BICYCLE costs\n¥1000000. Sorry,\nno instalments!", done)) 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 walks you back out. 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, "right", 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