-- The Day-Care conversation: deposit, withdraw, and the egg handover outside -- (engine/events/daycare.asm DayCareMan / DayCareLady / DayCareManOutside). -- -- There is no menu of the Day-Care's own on the cart. All three routines are -- PrintDayCareText + YesNoBox + SelectTradeOrDayCareMon, so this screen is a -- speech box, a yes/no box and a push of the party list -- which is why it -- reads as a phase machine rather than as a list with a cursor, and why every -- coordinate below is one of the shared ones: -- -- THIS SCREEN DRAWS OVER THE LIVE MAP AND MUST NEVER CLEAR THE FIELD. Both -- NPC scripts are `faceplayer / opentext / special DayCareMan / waitbutton / -- closetext` (maps/DayCare.asm DayCareManScript_Inside, DayCareLadyScript), -- and nothing the special reaches -- DayCareMan, DayCareLady, PrintDayCareText -- or YesNoBox -- blanks the screen, so the Day-Care room stays visible behind -- the textbox exactly the way any other `opentext` conversation does. Hence -- isOpaque = false (StateStack keeps the overworld in the frame), no -- drawsWidescreen/drawWidescreen (Game2:drawScene must fall through to -- world:draw + stack:drawCanvas at the plain integer letterbox fit) and no -- Chrome.clear in drawPanel: Chrome.box paints an opaque box on its own. This -- is the same shape every screen that overlays the map uses (BankOfMom, -- ScriptMenu, ElevatorMenu, MailMenu, StartMenu). -- -- Textbox `lb bc, 4, 18` at (0,12) -- a 20x6 box. TEXTBOX_INNERY is -- TEXTBOX_Y + 2 and LineChar targets INNERY + 2, so the two -- lines are at rows 14 and 16, TWO apart. -- YesNoBox `lb bc, SCREEN_WIDTH - 6, 7` -- a 6x5 box at (14,7). -- YesNoMenuHeader sets STATICMENU_CURSOR *and* -- STATICMENU_NO_TOP_SPACING, so GetMenuTextStartCoord puts -- YES at (16,8) and NO at (16,10) with the cursor column at -- 15. -- LoadBlinkingCursor the ▼ at (18,17) while a page waits for a button. -- -- The party list is opened through Screens ("Gen2PartyMenu") with -- PARTYMENUACTION_GIVE_MON, whose PartyMenuStrings row is ChooseAMonString -- -- the plain "Choose a #MON." prompt, not one of the item-flavoured ones. -- -- Every string is transcribed from data/text/common_1.asm's _DayCare* / -- _Breed* block, and paired with its pokegold label in LABELS below. No -- script bytecode points at any of them -- PrintDayCareText is asm -- so the -- extractor seeds its text walker at the block by name (RomExtractorGen2's -- NAMED_TEXT) and the screen prefers the cache's own characters, falling back -- to the transcription for a cache built before that seed. -- -- The MODEL is src/core/gen2/Breeding.lua -- every refusal, price, deposit and -- egg roll below is one call into it, so the conversation can be wrong about -- its layout without ever being wrong about the rules. local Breeding = require("src.core.gen2.Breeding") local Chrome = require("src.ui.gen2.Chrome") local CommonText = require("src.core.gen2.CommonText") local Screens = require("src.ui.Screens") local Sound = require("src.core.Sound") local DayCareMenu = {} DayCareMenu.__index = DayCareMenu DayCareMenu.isOpaque = false -- charmap.asm's currency glyph and the text-advance arrow (font code $ee), -- spelled the same way MartMenu spells them so both go through Font.split's -- charmap match rather than through four ASCII tiles. local YEN = "\xc2\xa5" local DOWN_ARROW = "\xe2\x96\xbc" -- ---------------------------------------------------------------- layout local TEXT_BOX_X, TEXT_BOX_Y, TEXT_BOX_W, TEXT_BOX_H = 0, 12, 20, 6 local TEXT_X, TEXT_Y, TEXT_LINE = 1, 14, 2 local ARROW_X, ARROW_Y = 18, 17 local YESNO_X, YESNO_Y, YESNO_W, YESNO_H = 14, 7, 6, 5 -- ------------------------------------------------------------------ sfx -- -- Named the pokegold way; Sound.GEN2_ALIASES is what maps the shared UI's own -- labels onto these. SFX_TRANSACTION rings in RetrieveMonFromDayCareMan -- BEFORE the money is taken (it is the mon coming back, not the payment), and -- SFX_GET_EGG in DayCareManOutside right after the egg lands in the party. local SFX_TRANSACTION = "Sfx_Transaction" local SFX_GET_EGG = "Sfx_GetEgg" -- `ld c, 120 / call DelayFrames` between _ReceivedEggText and -- _TakeGoodCareOfEggText: two full seconds of the jingle before he speaks -- again. local GET_EGG_FRAMES = 120 -- ----------------------------------------------------------------- text -- -- A "page" is one screenful: up to two lines, TEXT_LINE rows apart. A `para` -- in the ASM starts a new page; a `cont` scrolls one line, which shows as a -- page whose first line is the previous page's second. local function page(...) return { ... } end local function pages(...) return { ... } end local TEXT = { -- _DayCareManIntroText / _DayCareManIntroEggText. The "_EGG" one is the -- LONGER first-meeting script -- DayCareIntroText's `inc a` picks it the one -- time DAYCARE_INTRO_SEEN_F is clear, so it explains what eggs are rather -- than announcing that you have one. manIntro = pages( page("I'm the DAY-CARE", "MAN. Want me to"), page("MAN. Want me to", "raise a #MON?")), manIntroEgg = pages( page("I'm the DAY-CARE", "MAN. Do you know"), page("MAN. Do you know", "about EGGS?"), page("I was raising", "#MON with my"), page("#MON with my", "wife, you see."), page("We were shocked to", "find an EGG!"), page("How incredible is", "that?"), page("So, want me to", "raise a #MON?")), ladyIntro = pages( page("I'm the DAY-CARE", "LADY."), page("Should I raise a", "#MON for you?")), ladyIntroEgg = pages( page("I'm the DAY-CARE", "LADY. Do you know"), page("LADY. Do you know", "about EGGS?"), page("My husband and I", "were raising some"), page("were raising some", "#MON, you see."), page("We were shocked to", "find an EGG!"), page("How incredible", "could that be?"), page("Should I raise a", "#MON for you?")), -- DAYCARETEXT_WHICH_ONE, a `prompt`: it waits for a button and only then -- does SelectTradeOrDayCareMon open the party list over it. whichOne = pages(page("What should I", "raise for you?")), -- DayCareAskDepositPokemon's refusals, keyed by the Breeding.REFUSE_* the -- model hands back. lastMon = pages(page("Oh? But you have", "just one #MON.")), cantAcceptEgg = pages(page("Sorry, but I can't", "accept an EGG.")), removeMail = pages(page("Remove MAIL before", "you come see me.")), lastAliveMon = pages( page("If you give me", "that, what will"), page("that, what will", "you battle with?")), partyFull = pages(page("You have no room", "for it.")), notEnoughMoney = pages(page("You don't have", "enough money.")), ohFine = pages(page("Oh, fine then.")), comeAgain = pages(page("Come again.")), comeBackLater = pages(page("Come back for it", "later.")), -- _IllRaiseYourMonText: the nickname is a text_ram field, so it is spliced -- rather than being part of the string. deposit = function(name) return pages(page("OK. I'll raise", ("your %s."):format(name))) end, geniuses = function(name) return pages( page("Are we geniuses or", "what? Want to see"), page("what? Want to see", ("your %s?"):format(name))) end, -- _YourMonHasGrownText: `text_decimal wStringBuffer2 + 1, 1, 3` is the -- levels grown as a 3-wide left-aligned field, and -- `text_decimal wStringBuffer2 + 2, 3, 4` the price as a 4-wide one -- both -- LEFTALIGN, so neither is space padded. hasGrown = function(name, grown, price) return pages( page(("Your %s"):format(name), "has grown a lot."), page("By level, it's", ("grown by %d."):format(grown)), page("If you want your", "#MON back, it"), page("#MON back, it", ("will cost %s%d."):format(YEN, price))) end, -- _BackAlreadyText, the "grew nothing" branch. Its price is a LITERAL -- ¥100 in the string, not a decimal field, which is the same number -- GetPriceToRetrieveBreedmon computes for zero levels grown. backAlready = function(name) return pages( page("Huh? Back already?", ("Your %s"):format(name)), page("needs a little", "more time with us."), page("If you want your", "#MON back, it"), page("#MON back, it", ("will cost %s100."):format(YEN))) end, withdraw = pages(page("Perfect! Here's", "your #MON.")), gotBack = function(player, name) return pages(page(("%s got back"):format(player), ("%s."):format(name))) end, -- DayCareManOutside. notYet = pages(page("Not yet…")), foundAnEgg = pages( page("Ah, it's you!"), page("We were raising", "your #MON, and"), page("my goodness, were", "we surprised!"), page("Your #MON had", "an EGG!"), page("We don't know how", "it got there, but"), page("your #MON had", "it. You want it?")), receivedEgg = function(player) return pages(page(("%s received"):format(player), "the EGG!")) end, takeGoodCare = pages(page("Take good care of", "it.")), illKeepIt = pages(page("Well then, I'll", "keep it. Thanks!")), noRoomForEgg = pages( page("You have no room", "in your party."), page("in your party.", "Come back later.")), } DayCareMenu.TEXT = TEXT -- ...and the data/text/common_1.asm label each of those entries transcribes. -- The extractor seeds its text walker at this whole block now -- (RomExtractorGen2's NAMED_TEXT), so the cache carries the cart's own -- characters and `self.TEXT` below prefers them; the transcription above is -- what a cache built before that seed falls back to. local LABELS = { manIntro = "_DayCareManIntroText", manIntroEgg = "_DayCareManIntroEggText", ladyIntro = "_DayCareLadyIntroText", ladyIntroEgg = "_DayCareLadyIntroEggText", whichOne = "_WhatShouldIRaiseText", lastMon = "_OnlyOneMonText", cantAcceptEgg = "_CantAcceptEggText", removeMail = "_RemoveMailText", lastAliveMon = "_LastHealthyMonText", partyFull = "_HaveNoRoomText", notEnoughMoney = "_NotEnoughMoneyText", ohFine = "_OhFineThenText", comeAgain = "_ComeAgainText", comeBackLater = "_ComeBackLaterText", deposit = "_IllRaiseYourMonText", geniuses = "_AreWeGeniusesText", hasGrown = "_YourMonHasGrownText", backAlready = "_BackAlreadyText", withdraw = "_PerfectHeresYourMonText", gotBack = "_GotBackMonText", notYet = "_NotYetText", foundAnEgg = "_FoundAnEggText", receivedEgg = "_ReceivedEggText", takeGoodCare = "_TakeGoodCareOfEggText", illKeepIt = "_IllKeepItThanksText", noRoomForEgg = "_NoRoomForEggText", } DayCareMenu.LABELS = LABELS -- The six entries that take arguments hand them to CommonText.fill in the -- order their string names its markers -- the text_ram nickname, then the -- levels grown, then the price -- so each formatted entry keeps the signature -- its transcription above already has. local FILL = { deposit = function(name) return { name } end, geniuses = function(name) return { name } end, hasGrown = function(name, grown, price) return { name, grown, price } end, backAlready = function(name) return { name } end, gotBack = function(player, name) return { name, player = player } end, receivedEgg = function(player) return { player = player } end, } -- TEXT with every entry the cache carries replaced by the extracted string. -- Anything the cache is missing falls through the metatable to the -- transcription, so a partial cache is a mix rather than a hole. local function extractedText(text) local out = setmetatable({}, { __index = TEXT }) for key, label in pairs(LABELS) do local list = CommonText.of(text, label) if list then local fill = FILL[key] if fill then out[key] = function(...) return CommonText.fill(list, fill(...)) end else out[key] = list end end end return out end function DayCareMenu:wantsFillScale() return true end -- The nickname the text_ram fields splice in: a nicknamed mon by its -- nickname, anything else by its species name. local function monName(mon) if not mon then return "#MON" end return mon.nickname or mon.name or mon.species or "#MON" end -- opts: save, side ("man" | "lady" | "outside"), text (text.lua, for the -- extracted strings), onClose(scriptVar) function DayCareMenu.new(game, opts) opts = opts or {} local self = setmetatable({}, DayCareMenu) self.game = game self.save = opts.save or (game and game.save) self.data = (game and game.data) or {} -- text.lua rides on the world, the way the Pokegear's phone strings do: -- this screen is pushed over the overworld and never without one. self.textData = opts.text or (game and game.world and game.world.text) self.TEXT = extractedText(self.textData) self.side = opts.side or "man" self.onClose = opts.onClose self.rng = opts.rng -- wScriptVar: DayCareManOutside is the only branch that writes one, and TRUE -- means "no room, ask again". self.scriptVar = 0 self.delay = 0 if self.side == "outside" then self:startOutside() elseif (Breeding.side(self.save, self.side) or {}).mon then self:startWithdraw() else self:startIntro() end return self end -- --------------------------------------------------------------- overlays -- -- `say` is PrintText plus the button wait every one of these strings ends on -- (`prompt` and `done` both park until A or B here, because the script that -- calls the special does `waitbutton` straight after). function DayCareMenu:say(list, onDone) self.message = { pages = list or {}, page = 1, onDone = onDone } end function DayCareMenu:ask(list, onYes, onNo) self.confirm = { pages = list or {}, page = 1, choice = 1, onYes = onYes, onNo = onNo } end function DayCareMenu:close() if self.onClose then self.onClose(self.scriptVar) end end -- `.print_text` then `.cancel`: the refusal, and then COME_AGAIN. function DayCareMenu:refuse(key) local list = self.TEXT[key] or self.TEXT.ohFine self:say(list, function() self:comeAgain() end) end function DayCareMenu:comeAgain() self:say(self.TEXT.comeAgain, function() self:close() end) end function DayCareMenu:playSfx(name) local sfx = self.data.audio and self.data.audio.sfx if sfx and sfx[Sound.resolve(self.data, name)] then Sound.play(self.data, name) end end -- PlayMonCry, which is silent for a species with no extracted cry rather than -- raising. function DayCareMenu:playCry(species) if not species then return end local cries = self.data.audio and self.data.audio.cries if cries and cries[species] then Sound.playCry(self.data, species) end end -- ----------------------------------------------------------------- intro -- -- DayCareIntroText: test DAYCARE_INTRO_SEEN_F, set it, and `inc a` to the -- longer script the one time it was clear. Declining goes straight to -- `.cancel` -- COME_AGAIN with no "Oh, fine then." in front of it, which is -- the difference between saying no here and cancelling the party list. function DayCareMenu:startIntro() local first = Breeding.takeIntro(self.save, self.side) local lady = self.side == "lady" local list if lady then list = first and self.TEXT.ladyIntroEgg or self.TEXT.ladyIntro else list = first and self.TEXT.manIntroEgg or self.TEXT.manIntro end self:ask(list, function() self:askDeposit() end, function() self:comeAgain() end) end function DayCareMenu:askDeposit() local ok, reason = Breeding.canOpenDeposit(self.save) if not ok then return self:refuse(reason) end self:say(self.TEXT.whichOne, function() self:openParty() end) end function DayCareMenu:openParty() local game = self.game -- No stack means no party list to open; back out the way cancelling it does -- rather than pretending a mon was handed over. if not (game and game.stack) then return self:comeAgain() end self.picking = true Screens.push(game, "Gen2PartyMenu", { party = self.save.party, -- PARTYMENUACTION_GIVE_MON's PartyMenuStrings row is ChooseAMonString. prompt = "choose", onChoose = function(index) game.stack:pop() self.picking = false self:chose(index) end, onCancel = function() game.stack:pop() self.picking = false -- .Declined self:refuse("ohFine") end, }) end function DayCareMenu:chose(index) local ok, reason = Breeding.canDeposit(self.data, self.save, self.side, index) if not ok then return self:refuse(reason) end local _, mon = Breeding.deposit(self.data, self.save, self.side, index, { rng = self.rng }) -- DayCare_DepositPokemonText: the line, the cry, and then COME_BACK_LATER. -- The deposit path `ret`s rather than falling into `.cancel`, so there is -- deliberately no COME_AGAIN after it. self:say(self.TEXT.deposit(monName(mon)), function() self:playCry(mon and mon.species) self:say(self.TEXT.comeBackLater, function() self:close() end) end) end -- -------------------------------------------------------------- withdraw -- -- DayCare_AskWithdrawBreedMon: no growth is ONE yes/no over _BackAlreadyText; -- any growth is TWO, "Are we geniuses" and then the price. function DayCareMenu:startWithdraw() local slot = Breeding.side(self.save, self.side) local _, _, grown = Breeding.levelGrowth(self.data, slot) local price = Breeding.retrievePrice(grown) local name = monName(slot.mon) self.grown, self.price = grown, price local decline = function() self:refuse("ohFine") end if grown == 0 then self:ask(self.TEXT.backAlready(name), function() self:takeMon() end, decline) return end self:ask(self.TEXT.geniuses(name), function() self:ask(self.TEXT.hasGrown(name, grown, price), function() self:takeMon() end, decline) end, decline) end function DayCareMenu:takeMon() local ok, reason = Breeding.canWithdraw(self.data, self.save, self.side) if not ok then return self:refuse(reason) end -- RetrieveMonFromDayCareMan rings the till and waits for it BEFORE -- GetBreedMon1LevelGrowth; the money changes hands afterwards, in -- DayCare_GetBackMonForMoney. self:playSfx(SFX_TRANSACTION) local _, mon = Breeding.withdraw(self.data, self.save, self.side) local player = (self.save.player and self.save.player.name) or "" self:say(self.TEXT.withdraw, function() self:playCry(mon and mon.species) self:say(self.TEXT.gotBack(player, monName(mon)), function() self:comeAgain() end) end) end -- --------------------------------------------------------------- outside -- -- DayCareManOutside. The party-space check happens AFTER the yes, which is -- what makes "You have no room in your party" a thing you can be told while -- already holding out your hands for the egg. function DayCareMenu:startOutside() local dc = Breeding.dayCare(self.save) if not (dc and dc.hasEgg) then return self:say(self.TEXT.notYet, function() self:close() end) end self:ask(self.TEXT.foundAnEgg, function() self:takeEgg() end, function() -- .Declined -> .Load0: wScriptVar stays FALSE and he keeps the egg. self:say(self.TEXT.illKeepIt, function() self:close() end) end) end function DayCareMenu:takeEgg() local ok, reason = Breeding.collectEgg(self.data, self.save, { rng = self.rng }) if not ok then -- .PartyFull is the ONE branch that sets wScriptVar to TRUE. if reason == Breeding.REFUSE_PARTY_FULL then self.scriptVar = 1 end return self:say(self.TEXT.noRoomForEgg, function() self:close() end) end local player = (self.save.player and self.save.player.name) or "" self:say(self.TEXT.receivedEgg(player), function() self:playSfx(SFX_GET_EGG) self.delay = GET_EGG_FRAMES self:say(self.TEXT.takeGoodCare, function() self:close() end) end) end -- ---------------------------------------------------------------- update function DayCareMenu:updateMessage(input) if not (input:wasPressed("a") or input:wasPressed("b")) then return end local message = self.message if message.page < #message.pages then message.page = message.page + 1 return end self.message = nil if message.onDone then message.onDone() end end function DayCareMenu:updateConfirm(input) local confirm = self.confirm -- The yes/no box only comes up on the string's LAST page. if confirm.page < #confirm.pages then if input:wasPressed("a") or input:wasPressed("b") then confirm.page = confirm.page + 1 end return end if input:wasPressed("up") or input:wasPressed("down") then confirm.choice = confirm.choice == 1 and 2 or 1 return end -- YesNoMenuHeader has no STATICMENU_DISABLE_B, so B is NO. if input:wasPressed("b") then self.confirm = nil if confirm.onNo then confirm.onNo() end return end if input:wasPressed("a") then local yes = confirm.choice == 1 self.confirm = nil if yes then if confirm.onYes then confirm.onYes() end elseif confirm.onNo then confirm.onNo() end end end function DayCareMenu:update(_dt) -- The party list is on top of the stack; it owns input until it pops. if self.picking then return end if self.delay > 0 then self.delay = self.delay - 1 return end local input = self.game and self.game.input if not input then return end if self.message then return self:updateMessage(input) end if self.confirm then return self:updateConfirm(input) end end -- ------------------------------------------------------------------ draw function DayCareMenu:drawTextBox(lines) Chrome.box(TEXT_BOX_X, TEXT_BOX_Y, TEXT_BOX_W, TEXT_BOX_H) for i, line in ipairs(lines or {}) do Chrome.print(line, TEXT_X, TEXT_Y + (i - 1) * TEXT_LINE) end end function DayCareMenu:drawYesNo(choice) Chrome.box(YESNO_X, YESNO_Y, YESNO_W, YESNO_H) Chrome.print("YES", YESNO_X + 2, YESNO_Y + 1) Chrome.print("NO", YESNO_X + 2, YESNO_Y + 3) Chrome.cursor(YESNO_X + 1, YESNO_Y + (choice == 1 and 1 or 3)) end function DayCareMenu:drawPanel() if self.message then self:drawTextBox(self.message.pages[self.message.page]) if self.message.page < #self.message.pages then Chrome.print(DOWN_ARROW, ARROW_X, ARROW_Y) end elseif self.confirm then self:drawTextBox(self.confirm.pages[self.confirm.page]) if self.confirm.page >= #self.confirm.pages then self:drawYesNo(self.confirm.choice) else Chrome.print(DOWN_ARROW, ARROW_X, ARROW_Y) end else self:drawTextBox(nil) end love.graphics.setColor(1, 1, 1, 1) end function DayCareMenu:draw() self:drawPanel() end return DayCareMenu