mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-18 19:54:21 +02:00
G2 support
This commit is contained in:
@@ -0,0 +1,399 @@
|
||||
-- GiveTakePartyMonItem (engine/pokemon/mon_menu.asm): the GIVE / TAKE menu the
|
||||
-- party submenu's ITEM row opens, and the two routines behind it.
|
||||
--
|
||||
-- It is here because MAIL cannot be reached without it. Attaching a letter to
|
||||
-- a mon is GivePartyItem -> ComposeMailMessage and nothing else on the cart --
|
||||
-- the PACK has no "use" for mail (ItemAttributes gives it ITEMMENU_NOUSE in
|
||||
-- both menus), the MAILBOX only ever MOVES a letter that already exists, and
|
||||
-- `givepokemail` is one scripted gift on Route 35. So the compose keyboard's
|
||||
-- only door is this one.
|
||||
--
|
||||
-- Drawn over the party list (`menu_coords 12, 12, SCREEN_WIDTH - 1,
|
||||
-- SCREEN_HEIGHT - 1`), so this state is not opaque.
|
||||
--
|
||||
-- GIVE .GiveItem: DepositSellPack, then
|
||||
-- - a KEY_ITEM or an untossable item is ItemCantHeldText and the PACK
|
||||
-- comes straight back (`jr .next` -> `.loop`)
|
||||
-- - an empty-handed mon takes it (GiveItemToPokemon), and if the item
|
||||
-- is MAIL the compose keyboard opens on top (GivePartyItem's
|
||||
-- `farcall ItemIsMail / call ComposeMailMessage`)
|
||||
-- - a mon already holding MAIL is refused with
|
||||
-- _PokemonRemoveMailText, BEFORE the swap question is asked -- so
|
||||
-- there is no way to knock a letter off a mon by accident
|
||||
-- - anything else is the swap question
|
||||
-- TAKE TakePartyItem: the item goes back to the bag, or
|
||||
-- _ItemStorageFullText when it will not fit
|
||||
--
|
||||
-- An EGG never gets here at all: GiveTakePartyMonItem's first two lines are
|
||||
-- `cp EGG / jr z, .cancel`.
|
||||
|
||||
local Bag = require("src.inventory.Bag")
|
||||
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 HeldItemMenu = {}
|
||||
HeldItemMenu.__index = HeldItemMenu
|
||||
HeldItemMenu.isOpaque = false
|
||||
|
||||
-- GiveTakeItemMenuData: menu_coords 12, 12, 19, 17, STATICMENU_CURSOR and no
|
||||
-- NO_TOP_SPACING, so GIVE is at (14,14) and TAKE two rows under it.
|
||||
local MENU_X, MENU_Y, MENU_W, MENU_H = 12, 12, 8, 6
|
||||
local MENU_LABEL_X, MENU_LABEL_Y = MENU_X + 2, MENU_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
|
||||
|
||||
local ENTRIES = {
|
||||
{ id = "give", label = "GIVE" },
|
||||
{ id = "take", label = "TAKE" },
|
||||
}
|
||||
|
||||
local function page(...) return { ... } end
|
||||
local function pages(...) return { ... } end
|
||||
|
||||
-- data/text/common_2.asm. The {STRBUF} markers are text_ram fields, spliced
|
||||
-- here rather than being part of the string.
|
||||
local TEXT = {
|
||||
hold = function(mon, item)
|
||||
return pages(page(("Made %s"):format(mon), ("hold %s."):format(item)))
|
||||
end,
|
||||
removeMail = pages(page("Please remove the", "MAIL first.")),
|
||||
notHolding = function(mon)
|
||||
return pages(page(("%s isn't"):format(mon), "holding anything."))
|
||||
end,
|
||||
storageFull = pages(page("Item storage space", "full.")),
|
||||
tookItem = function(item, mon)
|
||||
return pages(page(("Took %s"):format(item), ("from %s."):format(mon)))
|
||||
end,
|
||||
askSwap = function(mon, item)
|
||||
return pages(
|
||||
page(("%s is"):format(mon), "already holding"),
|
||||
page(("%s."):format(item), "Switch items?"))
|
||||
end,
|
||||
swapped = function(mon, old, new)
|
||||
return pages(
|
||||
page(("Took %s's"):format(mon), ("%s and"):format(old)),
|
||||
page("made it hold", ("%s."):format(new)))
|
||||
end,
|
||||
cantHold = pages(page("This item can't be", "held.")),
|
||||
}
|
||||
|
||||
local LABELS = {
|
||||
hold = "_PokemonHoldItemText",
|
||||
removeMail = "_PokemonRemoveMailText",
|
||||
notHolding = "_PokemonNotHoldingText",
|
||||
storageFull = "_ItemStorageFullText",
|
||||
tookItem = "_PokemonTookItemText",
|
||||
askSwap = "_PokemonAskSwapItemText",
|
||||
swapped = "_PokemonSwapItemText",
|
||||
cantHold = "_ItemCantHeldText",
|
||||
}
|
||||
|
||||
-- Each formatted entry's markers, in the order the ASM string names them.
|
||||
local FILL = {
|
||||
hold = function(mon, item) return { mon, item } end,
|
||||
notHolding = function(mon) return { mon } end,
|
||||
tookItem = function(item, mon) return { item, mon } end,
|
||||
askSwap = function(mon, item) return { mon, item } end,
|
||||
swapped = function(mon, old, new) return { mon, old, new } end,
|
||||
}
|
||||
|
||||
HeldItemMenu.TEXT = TEXT
|
||||
HeldItemMenu.LABELS = LABELS
|
||||
HeldItemMenu.ENTRIES = 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
|
||||
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
|
||||
|
||||
-- opts: save, slot, items (items.lua), text (text.lua), onClose()
|
||||
function HeldItemMenu.new(game, opts)
|
||||
opts = opts or {}
|
||||
local self = setmetatable({}, HeldItemMenu)
|
||||
self.game = game
|
||||
self.save = opts.save or (game and game.save)
|
||||
self.slot = opts.slot or 1
|
||||
self.items = opts.items or (game and game.data and game.data.items)
|
||||
self.textData = opts.text or (game and game.world and game.world.text)
|
||||
self.TEXT = extractedText(self.textData)
|
||||
self.onClose = opts.onClose
|
||||
self.index = 1
|
||||
self.busy = false
|
||||
return self
|
||||
end
|
||||
|
||||
function HeldItemMenu:mon()
|
||||
return self.save and self.save.party and self.save.party[self.slot]
|
||||
end
|
||||
|
||||
function HeldItemMenu:monName()
|
||||
local mon = self:mon()
|
||||
if not mon then return "#MON" end
|
||||
return mon.nickname or mon.name or mon.species or "#MON"
|
||||
end
|
||||
|
||||
function HeldItemMenu:itemName(id)
|
||||
local def = id and self.items and self.items[id]
|
||||
return (def and def.name) or id or "?"
|
||||
end
|
||||
|
||||
function HeldItemMenu:close()
|
||||
if self.onClose then self.onClose() end
|
||||
end
|
||||
|
||||
function HeldItemMenu:say(list, onDone)
|
||||
self.message = { pages = list or {}, page = 1, onDone = onDone }
|
||||
end
|
||||
|
||||
function HeldItemMenu:ask(list, onYes, onNo)
|
||||
self.confirm = { pages = list or {}, page = 1, choice = 1,
|
||||
onYes = onYes, onNo = onNo }
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------- GIVE
|
||||
|
||||
-- .GiveItem's `cp KEY_ITEM_POCKET` and CheckTossableItem: a key item or an
|
||||
-- item whose attributes forbid tossing cannot be held, and the PACK reopens
|
||||
-- rather than the menu closing.
|
||||
function HeldItemMenu:canHold(itemId)
|
||||
local def = itemId and self.items and self.items[itemId]
|
||||
if def and def.pocket == "KEY_ITEM" then return false end
|
||||
if def and def.canToss == false then return false end
|
||||
return true
|
||||
end
|
||||
|
||||
function HeldItemMenu:openPack()
|
||||
local game = self.game
|
||||
if not (game and game.stack) then return self:close() end
|
||||
self.busy = true
|
||||
Screens.push(game, "Gen2PackMenu", {
|
||||
save = self.save,
|
||||
-- DepositSellPack: the PACK is a chooser here, so a field item must not
|
||||
-- run its effect on the way past.
|
||||
give = true,
|
||||
onChoose = function(itemId)
|
||||
game.stack:pop()
|
||||
self.busy = false
|
||||
self:giveItem(itemId)
|
||||
end,
|
||||
onClose = function()
|
||||
game.stack:pop()
|
||||
self.busy = false
|
||||
-- `.quit`: backing out of the PACK ends the whole GIVE.
|
||||
self:close()
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
-- TryGiveItemToPartymon, in its own order.
|
||||
function HeldItemMenu:giveItem(itemId)
|
||||
local mon = self:mon()
|
||||
if not (mon and itemId) then return self:close() end
|
||||
if not self:canHold(itemId) then
|
||||
return self:say(self.TEXT.cantHold, function() self:openPack() end)
|
||||
end
|
||||
local held = mon.item
|
||||
if held and Mail.isMail(held) then
|
||||
-- .please_remove_mail: a `ret`, so the whole GIVE ends here.
|
||||
return self:say(self.TEXT.removeMail, function() self:close() end)
|
||||
end
|
||||
if not held then
|
||||
Bag.remove(self.save, itemId, 1)
|
||||
mon.item = itemId
|
||||
local name = self:itemName(itemId)
|
||||
return self:say(self.TEXT.hold(self:monName(), name), function()
|
||||
self:composeIfMail(itemId)
|
||||
end)
|
||||
end
|
||||
-- .already_holding_item: the swap question, then ReceiveItemFromPokemon for
|
||||
-- the old one. A bag that cannot take it back puts the old item straight
|
||||
-- back on the mon (.bag_full), so nothing is ever destroyed.
|
||||
self:ask(self.TEXT.askSwap(self:monName(), self:itemName(held)), function()
|
||||
Bag.remove(self.save, itemId, 1)
|
||||
if not Bag.add(self.save, held, 1, self.game and self.game.data) then
|
||||
Bag.add(self.save, itemId, 1, self.game and self.game.data)
|
||||
return self:say(self.TEXT.storageFull, function() self:close() end)
|
||||
end
|
||||
mon.item = itemId
|
||||
self:say(self.TEXT.swapped(self:monName(), self:itemName(held),
|
||||
self:itemName(itemId)), function() self:composeIfMail(itemId) end)
|
||||
end, function() self:close() end)
|
||||
end
|
||||
|
||||
-- GivePartyItem's tail: `ld d, a / farcall ItemIsMail / call
|
||||
-- ComposeMailMessage`. The keyboard has no cancel -- the item is already on
|
||||
-- the mon by the time it opens -- so an empty message is a blank letter, not
|
||||
-- a refusal.
|
||||
function HeldItemMenu:composeIfMail(itemId)
|
||||
if not Mail.isMail(itemId) then return self:close() end
|
||||
local game = self.game
|
||||
if not (game and game.stack) then return self:close() end
|
||||
self.busy = true
|
||||
Screens.push(game, "Gen2MailCompose", {
|
||||
onDone = function(message)
|
||||
game.stack:pop()
|
||||
self.busy = false
|
||||
Mail.compose(self.save, self.slot, message, self:mon(), itemId)
|
||||
self:close()
|
||||
end,
|
||||
})
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------- TAKE
|
||||
|
||||
function HeldItemMenu:takeItem()
|
||||
local mon = self:mon()
|
||||
if not mon then return self:close() end
|
||||
local held = mon.item
|
||||
if not held then
|
||||
return self:say(self.TEXT.notHolding(self:monName()),
|
||||
function() self:close() end)
|
||||
end
|
||||
if not Bag.add(self.save, held, 1, self.game and self.game.data) then
|
||||
return self:say(self.TEXT.storageFull, function() self:close() end)
|
||||
end
|
||||
mon.item = nil
|
||||
-- Taking a mail item back leaves no letter behind it: the struct has nowhere
|
||||
-- to live once the stationery is gone. The MAIL row's own TAKE is the path
|
||||
-- that asks about the PC first (src/ui/gen2/MailMenu.lua); this one is only
|
||||
-- reachable for a mon that is NOT holding mail, because the submenu shows
|
||||
-- MAIL instead of ITEM in that case.
|
||||
Mail.clear(self.save, self.slot)
|
||||
self:say(self.TEXT.tookItem(self:itemName(held), self:monName()),
|
||||
function() self:close() end)
|
||||
end
|
||||
|
||||
function HeldItemMenu:choose()
|
||||
local entry = ENTRIES[self.index]
|
||||
if not entry then return end
|
||||
if entry.id == "give" then return self:openPack() end
|
||||
self:takeItem()
|
||||
end
|
||||
|
||||
-- ------------------------------------------------------------------ update
|
||||
|
||||
function HeldItemMenu: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 HeldItemMenu: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 HeldItemMenu:update(_dt)
|
||||
-- The PACK or the keyboard is on top of the stack; it owns input.
|
||||
if self.busy 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
|
||||
|
||||
local total = #ENTRIES
|
||||
if input:wasPressed("up") then
|
||||
self.index = self.index > 1 and self.index - 1 or total
|
||||
elseif input:wasPressed("down") then
|
||||
self.index = self.index < total and self.index + 1 or 1
|
||||
elseif input:wasPressed("a") then
|
||||
self:choose()
|
||||
elseif input:wasPressed("b") then
|
||||
self:close()
|
||||
end
|
||||
end
|
||||
|
||||
-- -------------------------------------------------------------------- draw
|
||||
|
||||
function HeldItemMenu: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 HeldItemMenu: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 HeldItemMenu: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
|
||||
Chrome.box(MENU_X, MENU_Y, MENU_W, MENU_H)
|
||||
for row, entry in ipairs(ENTRIES) do
|
||||
local ty = MENU_LABEL_Y + (row - 1) * 2
|
||||
if row == self.index then Chrome.cursor(MENU_LABEL_X - 1, ty) end
|
||||
Chrome.print(entry.label, MENU_LABEL_X, ty)
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
function HeldItemMenu:draw()
|
||||
self:drawPanel()
|
||||
end
|
||||
|
||||
return HeldItemMenu
|
||||
Reference in New Issue
Block a user