mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-19 04:06:10 +02:00
G2 support
This commit is contained in:
@@ -0,0 +1,340 @@
|
||||
-- The Pokemon PC's top menu (engine/pokemon/bills_pc_top.asm _BillsPC).
|
||||
--
|
||||
-- WITHDRAW POKéMON / DEPOSIT POKéMON / CHANGE BOX /
|
||||
-- MOVE POKéMON W/O MAIL / MAIL BOX / SEE YA!
|
||||
--
|
||||
-- MAIL BOX is the item PC's PLAYERSPCITEM_MAIL_BOX row rather than one of
|
||||
-- _BillsPC's five; it is here because this port used to fold both PCs into
|
||||
-- one menu, and a directly-constructed PcMenu still folds them. The
|
||||
-- Pokecenter's whose-PC menu (src/ui/gen2/CenterPcMenu.lua) opens this as
|
||||
-- BILL's PC with `bills = true`, which shows the cart's own five rows and
|
||||
-- leaves the MAIL BOX to <PLAYER>'s PC (src/ui/gen2/ItemPcMenu.lua).
|
||||
--
|
||||
-- .MenuHeader is menu_coords 0, 0, 19, 17 -- the menu owns the whole screen,
|
||||
-- with "What do you want to do?" in a text box along the bottom. Choosing an
|
||||
-- entry pushes BoxMenu (the withdraw/deposit list) or the box picker.
|
||||
--
|
||||
-- The assembled rows run through the ui.pc.items hook, the same name and the
|
||||
-- same (game, items) payload the Gen 1 PC uses
|
||||
-- (src/world/OverworldController.lua openPC), with SEE YA! appended after it
|
||||
-- the way that site appends LOG OFF.
|
||||
|
||||
local Boxes = require("src.core.gen2.Boxes")
|
||||
local Chrome = require("src.ui.gen2.Chrome")
|
||||
local Logger = require("src.core.Logger")
|
||||
local Mail = require("src.core.gen2.Mail")
|
||||
local Runtime = require("src.mods.Runtime")
|
||||
local Screens = require("src.ui.Screens")
|
||||
local Strings = require("src.core.Strings")
|
||||
|
||||
-- _PCMonHoldingMailText (data/text/common_2.asm), the refusal
|
||||
-- BillsPC_MovePKMNMenu prints instead of opening the list. Two pages, because
|
||||
-- the ASM has a `para` in the middle of it. Declared up here and looked up at
|
||||
-- the call site, so Strings.source is what puts both in the catalog.
|
||||
local MON_HOLDING_MAIL = {
|
||||
Strings.source("There is a POKéMON\nholding MAIL."),
|
||||
Strings.source("Please remove the\nMAIL."),
|
||||
}
|
||||
|
||||
local PcMenu = {}
|
||||
PcMenu.__index = PcMenu
|
||||
PcMenu.isOpaque = true
|
||||
|
||||
-- .strings, verbatim. <PK> and <MN> are real font glyphs (codes $e1/$e2) and
|
||||
-- Font.split matches charmap sequences, so writing them the way the ASM does
|
||||
-- draws two tiles rather than seven -- which is the only reason "MOVE <PK><MN>
|
||||
-- W/O MAIL" fits inside a 20-tile screen.
|
||||
local ENTRIES = {
|
||||
{ id = "withdraw", label = "WITHDRAW <PK><MN>" },
|
||||
{ id = "deposit", label = "DEPOSIT <PK><MN>" },
|
||||
{ id = "changebox", label = "CHANGE BOX" },
|
||||
{ id = "move", label = "MOVE <PK><MN> W/O MAIL" },
|
||||
-- PLAYERSPCITEM_MAIL_BOX (engine/events/pokecenter_pc.asm), which BOTH
|
||||
-- .WhichPC lists carry: the MAILBOX is on the item PC in a Pokecenter and in
|
||||
-- the bedroom alike, unlike DECORATION below. It sits here because this
|
||||
-- port folds the item PC's menu into the storage one.
|
||||
{ id = "mailbox", label = "MAIL BOX" },
|
||||
{ id = "seeya", label = "SEE YA!" },
|
||||
}
|
||||
|
||||
-- PLAYERSPCITEM_DECORATION, the one row the bedroom's PC has that a
|
||||
-- Pokecenter's does not (engine/events/pokecenter_pc.asm: PLAYERSPC_HOUSE
|
||||
-- carries it, PLAYERSPC_NORMAL does not). It belongs to the item PC's menu on
|
||||
-- the cart, which this port folds into the storage menu the same way both PCs
|
||||
-- are folded -- so it hangs off the same list, gated on `house`.
|
||||
local DECORATION = { id = "decoration", label = "DECORATION" }
|
||||
|
||||
-- The exit row. It is a member of ENTRIES (it is one of _BillsPC's five), but
|
||||
-- the list is assembled without it and it is put back on the end AFTER the
|
||||
-- ui.pc.items hook has run, exactly as the Gen 1 site appends LOG OFF
|
||||
-- (src/world/OverworldController.lua openPC): a mod may add, drop or reorder
|
||||
-- anything it likes and still cannot orphan the way out.
|
||||
local EXIT_ID = "seeya"
|
||||
|
||||
-- ui.pc.items identity: an unhooked build hands its own list back.
|
||||
local function sameItems(_, items) return items end
|
||||
|
||||
function PcMenu:wantsFillScale() return true end
|
||||
function PcMenu:drawsWidescreen() return true end
|
||||
|
||||
-- opts: save, house (the bedroom's PC, which also does decorations), events
|
||||
-- (the wEventFlags bitfield the decoration menu reads ownership from),
|
||||
-- bills (_BillsPC's own five rows, no MAIL BOX: what the whose-PC
|
||||
-- menu's BILL's PC entry opens, src/ui/gen2/CenterPcMenu.lua),
|
||||
-- onClose(changedDecorations)
|
||||
function PcMenu.new(game, opts)
|
||||
opts = opts or {}
|
||||
local self = setmetatable({}, PcMenu)
|
||||
self.game = game
|
||||
self.save = opts.save or (game and game.save)
|
||||
self.onClose = opts.onClose
|
||||
self.house = opts.house and true or false
|
||||
self.events = opts.events
|
||||
-- The folded MAIL BOX row belongs to the item PC, and the whose-PC menu
|
||||
-- reaches that through <PLAYER>'s PC (src/ui/gen2/ItemPcMenu.lua), so
|
||||
-- BILL's PC shows the cart's own five rows. The bedroom's PC keeps it: the
|
||||
-- MAILBOX is on both .WhichPC lists.
|
||||
local dropMailbox = opts.bills and not self.house
|
||||
local entries = {}
|
||||
for _, entry in ipairs(ENTRIES) do
|
||||
local drop = entry.id == EXIT_ID
|
||||
or (dropMailbox and entry.id == "mailbox")
|
||||
if not drop then entries[#entries + 1] = entry end
|
||||
end
|
||||
if self.house then
|
||||
entries[#entries + 1] = DECORATION
|
||||
end
|
||||
-- Same hook name and same (game, items) payload as the Gen 1 PC menu
|
||||
-- (src/world/OverworldController.lua openPC), so one mod source can add a
|
||||
-- row to both generations' PCs. Unguarded, like that site: the list is
|
||||
-- built once per session, not per frame. A hook that answers with anything
|
||||
-- but a table is degraded to the vanilla list.
|
||||
local hooked = Runtime.call("ui.pc.items", sameItems, game, entries)
|
||||
if type(hooked) == "table" then
|
||||
entries = hooked
|
||||
else
|
||||
Logger.error("ui.pc.items returned %s; keeping the vanilla items",
|
||||
type(hooked))
|
||||
end
|
||||
-- SEE YA! goes back on last: it is the row B lands on, and TURN OFF sits at
|
||||
-- the bottom of the cart's house list too.
|
||||
for _, entry in ipairs(ENTRIES) do
|
||||
if entry.id == EXIT_ID then entries[#entries + 1] = entry end
|
||||
end
|
||||
self.entries = entries
|
||||
-- wChangedDecorations, carried out to `special PlayersHousePC` so its
|
||||
-- script's `iftrue` can reload the map.
|
||||
self.changedDecorations = false
|
||||
self.index = 1
|
||||
self.message = nil
|
||||
-- Whether clearing the message also logs off. .CheckCanUsePC's does (the PC
|
||||
-- never opened at all); BillsPC_MovePKMNMenu's mail refusal does not -- its
|
||||
-- `.quit` returns with the carry clear, which drops back into the _BillsPC
|
||||
-- loop and redraws this menu.
|
||||
self.messageCloses = true
|
||||
-- .CheckCanUsePC: an empty party gets the "You'll need a POKéMON" line and
|
||||
-- the PC never opens. Kept here rather than at the call site so every route
|
||||
-- into the PC (the overworld script, a driver, a mod) gets the same gate.
|
||||
local ok, reason = Boxes.canUsePc(self.save)
|
||||
if not ok then self.message = reason end
|
||||
return self
|
||||
end
|
||||
|
||||
-- A refusal that leaves the PC open: the message replaces the menu until a
|
||||
-- button clears it, and then the menu is back exactly where it was. `pages`
|
||||
-- is a list because a `para` in the ASM is a screenful of its own -- the text
|
||||
-- box holds two rows and the fourth line of _PCMonHoldingMailText would
|
||||
-- otherwise be drawn off the bottom of the screen.
|
||||
function PcMenu:notice(pages)
|
||||
self.message = pages[1]
|
||||
self.messagePages = pages
|
||||
self.messagePage = 1
|
||||
self.messageCloses = false
|
||||
end
|
||||
|
||||
function PcMenu:close()
|
||||
if self.onClose then self.onClose(self.changedDecorations) end
|
||||
end
|
||||
|
||||
function PcMenu:choose()
|
||||
local entry = self.entries[self.index]
|
||||
if not entry then return end
|
||||
local game = self.game
|
||||
-- A hook- or monkey-patch-injected row carries label + onSelect and no id
|
||||
-- the ladder below knows, so without this arm it draws and does nothing.
|
||||
-- Same shape and same argument order as src/ui/gen2/PartyMenu.lua:433.
|
||||
if type(entry.onSelect) == "function" then
|
||||
entry.onSelect(self, game)
|
||||
return
|
||||
end
|
||||
if entry.id == "seeya" then
|
||||
self:close()
|
||||
return
|
||||
end
|
||||
if entry.id == "decoration" then
|
||||
if not (game and game.stack) then return end
|
||||
Screens.push(game, "Gen2DecorationMenu", {
|
||||
save = self.save,
|
||||
events = self.events,
|
||||
onDone = function(changed)
|
||||
self.changedDecorations = self.changedDecorations or changed or false
|
||||
game.stack:pop()
|
||||
end,
|
||||
})
|
||||
return
|
||||
end
|
||||
if entry.id == "mailbox" then
|
||||
if not (game and game.stack) then return end
|
||||
Screens.push(game, "Gen2MailboxMenu", {
|
||||
save = self.save,
|
||||
onClose = function() game.stack:pop() end,
|
||||
})
|
||||
return
|
||||
end
|
||||
if entry.id == "changebox" then
|
||||
self.picking = true
|
||||
self.pickIndex = self.save.currentBox or 1
|
||||
return
|
||||
end
|
||||
-- BillsPC_MovePKMNMenu asks IsAnyMonHoldingMail BEFORE it opens the list and
|
||||
-- refuses outright: MOVE POKéMON W/O MAIL is a whole-party operation, so one
|
||||
-- letter anywhere in the party stops it.
|
||||
if entry.id == "move" and Mail.anyMonHoldingMail(self.save) then
|
||||
self:notice({ Strings(MON_HOLDING_MAIL[1]), Strings(MON_HOLDING_MAIL[2]) })
|
||||
return
|
||||
end
|
||||
if not (game and game.stack) then return end
|
||||
Screens.push(game, "Gen2BoxMenu", {
|
||||
save = self.save,
|
||||
mode = entry.id, -- "withdraw" | "deposit" | "move"
|
||||
onClose = function() game.stack:pop() end,
|
||||
})
|
||||
end
|
||||
|
||||
function PcMenu:update(_dt)
|
||||
local input = self.game and self.game.input
|
||||
if not input then return end
|
||||
|
||||
if self.message then
|
||||
if input:wasPressed("a") or input:wasPressed("b") then
|
||||
local pages = self.messagePages
|
||||
if pages and self.messagePage < #pages then
|
||||
self.messagePage = self.messagePage + 1
|
||||
self.message = pages[self.messagePage]
|
||||
return
|
||||
end
|
||||
local closes = self.messageCloses
|
||||
self.message, self.messagePages, self.messagePage = nil, nil, nil
|
||||
self.messageCloses = true
|
||||
if closes then self:close() end
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if self.picking then
|
||||
local total = Boxes.NUM_BOXES
|
||||
if input:wasPressed("up") then
|
||||
self.pickIndex = self.pickIndex > 1 and self.pickIndex - 1 or total
|
||||
elseif input:wasPressed("down") then
|
||||
self.pickIndex = self.pickIndex < total and self.pickIndex + 1 or 1
|
||||
elseif input:wasPressed("a") then
|
||||
Boxes.setCurrent(self.save, self.pickIndex)
|
||||
self.picking = false
|
||||
elseif input:wasPressed("b") then
|
||||
self.picking = false
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
if input:wasPressed("up") then
|
||||
self.index = self.index > 1 and self.index - 1 or #self.entries
|
||||
elseif input:wasPressed("down") then
|
||||
self.index = self.index < #self.entries and self.index + 1 or 1
|
||||
elseif input:wasPressed("a") then
|
||||
self:choose()
|
||||
elseif input:wasPressed("b") then
|
||||
self:close()
|
||||
end
|
||||
end
|
||||
|
||||
function PcMenu:drawPanel()
|
||||
Chrome.clear()
|
||||
if self.message then
|
||||
Chrome.box(0, 12, 20, 6)
|
||||
-- A text box's two lines sit two tile rows apart, not one -- the same
|
||||
-- spacing src/render/TextBox.lua uses (line1 = ty+2, line2 = ty+4).
|
||||
local line = 14
|
||||
for part in (self.message .. "\n"):gmatch("(.-)\n") do
|
||||
Chrome.print(part, 1, line)
|
||||
line = line + 2
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
return
|
||||
end
|
||||
|
||||
if self.picking then
|
||||
-- CHANGE BOX: the 14 box names with how full each one is, six at a time.
|
||||
Chrome.box(0, 0, 20, 14)
|
||||
local rows = 6
|
||||
local scroll = math.max(0, math.min(self.pickIndex - rows,
|
||||
Boxes.NUM_BOXES - rows))
|
||||
for row = 1, rows do
|
||||
local i = row + scroll
|
||||
local ty = row * 2 - 1
|
||||
if i == self.pickIndex then Chrome.cursor(1, ty) end
|
||||
Chrome.print(Boxes.name(self.save, i), 2, ty)
|
||||
Chrome.printRight(
|
||||
("%d/%d"):format(Boxes.count(self.save, i), Boxes.MONS_PER_BOX),
|
||||
18, ty)
|
||||
end
|
||||
Chrome.box(0, 14, 20, 4)
|
||||
Chrome.print("Which BOX?", 1, 16)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
return
|
||||
end
|
||||
|
||||
-- .LogIn prints _PCWhatText into the (0,12) box, and that text is one word:
|
||||
-- "What?". PrintText starts at (1,14). The box goes down first because
|
||||
-- the menu window is drawn over it wherever a folded list runs past row 12,
|
||||
-- the way the cart's windows stack (src/ui/gen2/ItemPcMenu.lua does the
|
||||
-- same with the house's six-row item list).
|
||||
Chrome.box(0, 12, 20, 6)
|
||||
Chrome.print("What?", 1, 14)
|
||||
|
||||
-- ClearPCItemScreen: Textbox at (0,0) with a 10x18 interior, and a second
|
||||
-- at (0,12) with a 4x18 one. GetMenuTextStartCoord then puts the first
|
||||
-- label at (left+1+1, top+1+1) = (2,2) because STATICMENU_CURSOR is set and
|
||||
-- the menu does not ask for NO_TOP_SPACING; rows are two apart and the
|
||||
-- cursor sits one column left of the label. _BillsPC's own five rows end
|
||||
-- at row 10 and fit that box exactly; the folded MAIL BOX (and the
|
||||
-- bedroom's DECORATION) put rows below it, so the window is sized to the
|
||||
-- list rather than to the five the cart ships.
|
||||
Chrome.box(0, 0, 20, math.max(12, #self.entries * 2 + 2))
|
||||
for i, entry in ipairs(self.entries) do
|
||||
local ty = i * 2
|
||||
if i == self.index then Chrome.cursor(1, ty) end
|
||||
Chrome.print(entry.label, 2, ty)
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
function PcMenu:draw()
|
||||
self:drawPanel()
|
||||
end
|
||||
|
||||
function PcMenu:drawWidescreen(winW, winH)
|
||||
local G = love.graphics
|
||||
G.setColor(1, 1, 1, 1)
|
||||
G.rectangle("fill", 0, 0, winW, winH)
|
||||
local scale = Chrome.fitScale(winW, winH)
|
||||
G.push()
|
||||
G.translate(math.floor((winW - 160 * scale) / 2),
|
||||
math.floor((winH - 144 * scale) / 2))
|
||||
G.scale(scale, scale)
|
||||
self:drawPanel()
|
||||
G.pop()
|
||||
end
|
||||
|
||||
PcMenu.ENTRIES = ENTRIES
|
||||
|
||||
return PcMenu
|
||||
Reference in New Issue
Block a user