mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-22 21:46:51 +02:00
416 lines
13 KiB
Lua
416 lines
13 KiB
Lua
-- 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
|