-- The MAILBOX in the player's PC: _PlayerMailBoxMenu and MailboxPC -- (engine/pokemon/mail.asm). -- -- InitMail runs first and answers z when sMailboxCount is 0, which is the one -- branch that never opens a menu at all -- an empty MAILBOX is -- _EmptyMailboxText and nothing else. -- -- Layout, from the two headers: -- .TopMenuHeader menu_coords 8, 1, 18, 10 -- a scrolling menu, four rows, -- each printed by MailboxPC_PrintMailAuthor, i.e. the -- AUTHOR of the letter and not its message -- .SubMenuHeader menu_coords 0, 0, 13, 9 with STATICMENU_CURSOR, so its -- four labels start at (2,2) and step two rows -- -- The submenu's four rows are .Jumptable's four routines: -- -- READ MAIL ReadMailMessage, which is the Gen2MailRead screen -- PUT IN PACK "message will be lost. OK?" -> ReceiveItem, then -- DeleteMailFromPC. The letter is destroyed and the -- STATIONERY goes back in the bag, which is why the question -- is asked before the bag is even checked -- ATTACH MAIL the party list, refusing an EGG and a mon that is already -- holding anything, then MoveMailFromPCToParty. The cart -- LOOPS on both refusals (`jr .try_again`) rather than backing -- out, so the list comes straight back up -- CANCEL `ret` -- -- Drawn over whatever opened the PC, so this state is not opaque. local Bag = require("src.inventory.Bag") local Breeding = require("src.core.gen2.Breeding") local Chrome = require("src.ui.gen2.Chrome") local CommonText = require("src.core.gen2.CommonText") local Mail = require("src.core.gen2.Mail") local Screens = require("src.ui.Screens") local MailboxMenu = {} MailboxMenu.__index = MailboxMenu MailboxMenu.isOpaque = false local LIST_X, LIST_Y, LIST_W, LIST_H = 8, 1, 11, 10 -- InitScrollingMenu lays its rows two inside the box's corner and steps two. local ROW_X, ROW_Y, ROW_STEP = LIST_X + 2, LIST_Y + 2, 2 local VISIBLE_ROWS = 4 local SUB_X, SUB_Y, SUB_W, SUB_H = 0, 0, 14, 10 local SUB_LABEL_X, SUB_LABEL_Y = SUB_X + 2, SUB_Y + 2 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 YESNO_X, YESNO_Y, YESNO_W, YESNO_H = 14, 7, 6, 5 local DOWN_ARROW = "\xe2\x96\xbc" local ARROW_X, ARROW_Y = 18, 17 -- .SubMenuData, verbatim. local SUB_ENTRIES = { { id = "read", label = "READ MAIL" }, { id = "pack", label = "PUT IN PACK" }, { id = "attach", label = "ATTACH MAIL" }, { id = "cancel", label = "CANCEL" }, } local function page(...) return { ... } end local function pages(...) return { ... } end -- data/text/common_2.asm's _EmptyMailboxText .. _MailMovedFromBoxText block. local TEXT = { empty = pages(page("There's no MAIL", "here.")), messageLost = pages(page("The MAIL's message", "will be lost. OK?")), packFull = pages(page("The PACK is full.")), putAway = pages(page("The cleared MAIL", "was put away.")), alreadyHolding = pages(page("It's already hold-", "ing an item.")), egg = pages(page("An EGG can't hold", "any MAIL.")), moved = pages(page("The MAIL was moved", "from the MAILBOX.")), } local LABELS = { empty = "_EmptyMailboxText", messageLost = "_MailMessageLostText", packFull = "_MailPackFullText", putAway = "_MailClearedPutAwayText", alreadyHolding = "_MailAlreadyHoldingItemText", egg = "_MailEggText", moved = "_MailMovedFromBoxText", } MailboxMenu.TEXT = TEXT MailboxMenu.LABELS = LABELS MailboxMenu.SUB_ENTRIES = SUB_ENTRIES 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 out[key] = list end end return out end -- opts: save, text (text.lua), onClose() function MailboxMenu.new(game, opts) opts = opts or {} local self = setmetatable({}, MailboxMenu) self.game = game self.save = opts.save or (game and game.save) self.textData = opts.text or (game and game.world and game.world.text) self.TEXT = extractedText(self.textData) self.onClose = opts.onClose -- wCurMessageIndex / wCurMessageScrollPosition, both reset by MailboxPC -- before its loop. self.index = 1 self.scroll = 0 self.submenu = nil self.message = nil self.confirm = nil self.picking = false -- InitMail's z branch: no menu, one line, gone. if Mail.mailboxCount(self.save) == 0 then self:say(self.TEXT.empty, function() self:close() end) end return self end function MailboxMenu:box() return Mail.mailbox(self.save) end function MailboxMenu:count() return Mail.mailboxCount(self.save) end function MailboxMenu:selected() return self:box()[self.index] end function MailboxMenu:close() if self.onClose then self.onClose() end end function MailboxMenu:say(list, onDone) self.message = { pages = list or {}, page = 1, onDone = onDone } end function MailboxMenu:ask(list, onYes, onNo) self.confirm = { pages = list or {}, page = 1, choice = 1, onYes = onYes, onNo = onNo } end function MailboxMenu:clampIndex() local total = self:count() if total == 0 then self.index, self.scroll = 1, 0 return end if self.index > total then self.index = total end if self.index < 1 then self.index = 1 end if self.index <= self.scroll then self.scroll = self.index - 1 elseif self.index > self.scroll + VISIBLE_ROWS then self.scroll = self.index - VISIBLE_ROWS end self.scroll = math.max(0, math.min(self.scroll, math.max(0, total - VISIBLE_ROWS))) end -- ------------------------------------------------------------- submenu rows function MailboxMenu:readMail() local game = self.game local entry = self:selected() if not (game and game.stack and entry) then return end self.picking = true Screens.push(game, "Gen2MailRead", { entry = entry, onClose = function() game.stack:pop() self.picking = false self.submenu = nil end, }) end -- .PutInPack. The yes/no comes first, then ReceiveItem -- and only a bag that -- accepted the stationery gets as far as DeleteMailFromPC, so a full PACK -- leaves the letter in the MAILBOX intact. function MailboxMenu:putInPack() self:ask(self.TEXT.messageLost, function() local entry = self:selected() if not entry then self.submenu = nil return end local data = self.game and self.game.data if not Bag.add(self.save, entry.type, 1, data) then return self:say(self.TEXT.packFull, function() self.submenu = nil end) end Mail.deleteFromPc(self.save, self.index) self:clampIndex() self:say(self.TEXT.putAway, function() self.submenu = nil if self:count() == 0 then -- The next .loop calls InitMail again, which now answers z. self:say(self.TEXT.empty, function() self:close() end) end end) end, function() -- `ret c`: the question was the whole thing. self.submenu = nil end) end -- .AttachMail's loop. Both refusals go back to the party list rather than out -- of the submenu, which is exactly what `jr .try_again` does. function MailboxMenu:attachMail() local game = self.game if not (game and game.stack) then self.submenu = nil return end self.picking = true Screens.push(game, "Gen2PartyMenu", { save = self.save, party = self.save.party, prompt = "choose", onChoose = function(slot, mon) game.stack:pop() self.picking = false if Breeding.isEgg(mon) then return self:say(self.TEXT.egg, function() self:attachMail() end) end if mon and mon.item then return self:say(self.TEXT.alreadyHolding, function() self:attachMail() end) end if not Mail.moveFromPcToParty(self.save, self.index, slot) then self.submenu = nil return end self:clampIndex() self:say(self.TEXT.moved, function() self.submenu = nil if self:count() == 0 then self:say(self.TEXT.empty, function() self:close() end) end end) end, onCancel = function() game.stack:pop() self.picking = false -- `.exit2` -> CloseSubmenu: back to the letter list, not out of the PC. self.submenu = nil end, }) end function MailboxMenu:chooseSub() local entry = SUB_ENTRIES[self.submenu.index] if not entry then return end if entry.id == "read" then return self:readMail() end if entry.id == "pack" then return self:putInPack() end if entry.id == "attach" then return self:attachMail() end self.submenu = nil end -- ------------------------------------------------------------------ update function MailboxMenu: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 MailboxMenu:updateConfirm(input) local confirm = self.confirm 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 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 MailboxMenu:updateSubmenu(input) local total = #SUB_ENTRIES if input:wasPressed("up") then self.submenu.index = self.submenu.index > 1 and self.submenu.index - 1 or total elseif input:wasPressed("down") then self.submenu.index = self.submenu.index < total and self.submenu.index + 1 or 1 elseif input:wasPressed("a") then self:chooseSub() elseif input:wasPressed("b") then self.submenu = nil end end function MailboxMenu:update(_dt) if self.picking then 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 if self.submenu then return self:updateSubmenu(input) end local total = self:count() if total == 0 then return self:close() end if input:wasPressed("up") then self.index = self.index > 1 and self.index - 1 or total self:clampIndex() elseif input:wasPressed("down") then self.index = self.index < total and self.index + 1 or 1 self:clampIndex() elseif input:wasPressed("a") then self.submenu = { index = 1 } elseif input:wasPressed("b") then -- .exit: PAD_B out of the scrolling menu ends _PlayerMailBoxMenu. self:close() end end -- -------------------------------------------------------------------- draw function MailboxMenu: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 MailboxMenu: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 MailboxMenu:drawList() Chrome.box(LIST_X, LIST_Y, LIST_W, LIST_H) local box = self:box() for row = 1, VISIBLE_ROWS do local i = row + self.scroll local entry = box[i] if entry then local ty = ROW_Y + (row - 1) * ROW_STEP if i == self.index then Chrome.cursor(ROW_X - 1, ty) end -- MailboxPC_PrintMailAuthor copies NAME_LENGTH - 1 bytes out of the -- struct's Author field and terminates it; a blank author draws a blank -- row rather than the message. Chrome.print(entry.author or "", ROW_X, ty) end end end function MailboxMenu:drawSubmenu() Chrome.box(SUB_X, SUB_Y, SUB_W, SUB_H) for row, entry in ipairs(SUB_ENTRIES) do local ty = SUB_LABEL_Y + (row - 1) * 2 if row == self.submenu.index then Chrome.cursor(SUB_LABEL_X - 1, ty) end Chrome.print(entry.label, SUB_LABEL_X, ty) end end function MailboxMenu: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 love.graphics.setColor(1, 1, 1, 1) return end if 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 love.graphics.setColor(1, 1, 1, 1) return end self:drawList() if self.submenu then self:drawSubmenu() end love.graphics.setColor(1, 1, 1, 1) end function MailboxMenu:draw() self:drawPanel() end return MailboxMenu