mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
af47e19e1a
The launcher spent ~9ms per frame building and drawing, and the Find Mods tab could hang the window for minutes. Both had the same root cause: a retained UI tree rebuilt every frame, and blocking curl calls made from the draw path. Replace the vendored FlexLove engine (28.5k lines) with src/ui/kit/ (Kit, Theme, Layout, Loader). The kit caches Text objects and all measurement, allocates nothing in the steady state, and draws flat. Build+draw is now under 1ms at every window size and on every tab (POKEPORT_LAUNCHER_PROF). Move every network call off the render thread onto a love.thread pool (src/net/Fetch.lua): mod index fetches, per-mod release checks, find-tab stats, thumbnails and mod installs. Mod indexes prewarm at boot so the Find Mods tab is populated before it is opened. Paginate every list -- mods, find, save slots, settings, release notes, versions -- with the page size derived from the real viewport height, so a 500-mod index costs what a 10-mod one does. Scrolling is gone. Anything that waits now raises a non-dismissable loader; per-row background work shows an inline spinner instead. The in-app updater moves to the top right beside the settings gear and pulses when an update is waiting. Theme is black with white outlines, no gradients or glows, and solid colour-coded embossed buttons with bold labels. The game tabs keep their cartridge colours. Everything is 1.3x larger. The save editor shares the theme, and adding an item there is now a searchable pop-up like adding a Pokemon. Also: - Reset rebinds, in Settings and under Touch Controls. Rebinds are additive (Input:applyBindings layers them over the defaults), so there was no in-game way to undo one. - Launch options: --game red [--slot N] / POKEPORT_GAME boots straight into a game for shortcuts and frontends, falling back to that game's tab when its ROM is not imported. Fixes found while porting: - Ellipsis and letterspacing truncated bytes, not codepoints, so a multi-byte mod name crashed the first frame on a Japanese index. Measurement no longer throws on malformed input either. - The new font set missed UiFont's kana fallback, rendering translated builds as tofu. - Fetch workers idle in Channel:demand() and LOVE waits for live threads at exit, so the process outlived the window; quitting mid-download also waited on curl's 300s ceiling. Shut the pool down in love.quit and bound its transfer timeouts. - In one column the save-slot card drew below the fold, over the footer, with no scrollbar left to reach it. The two FlexLove engine tests guarded a scroll manager and an auto-height propagation bug that no longer exist; replace them with a kit suite covering page bounds, viewport sizing and UTF-8 truncation, and retarget the NX test to assert the dependency is gone rather than that its perf guards are set.
291 lines
11 KiB
Lua
291 lines
11 KiB
Lua
-- Items panel: money, the shared item picker, badges, the configurable bag
|
|
-- (Bag.add/remove, ordered by Bag.order) and PC item storage (a plain
|
|
-- S.save.pcItems dict with no slot cap).
|
|
--
|
|
-- The picker is a searchable list rather than the old pair of arrows that
|
|
-- cycled one id at a time through ~250 items, which was the single worst
|
|
-- interaction in the editor. It scrolls under the mouse wheel too (#595):
|
|
-- typing used to be the only way to reach an id past the first screenful.
|
|
-- Badges sit in the wallet column as toggle chips because they are boolean
|
|
-- inventory flags, not stackable items, and must not look like quantity rows.
|
|
--
|
|
-- #715 reflow: side by side the wallet column plus the two quantity lists
|
|
-- need about 900 real px (a quantity row's -/+/x cluster alone is ~110px).
|
|
-- Below that the five cards stack in one full-width column that scrolls in
|
|
-- pixels (Kit.scrollPixels); the inner lists keep their own wheel/drag
|
|
-- regions, which claim the notch first when the pointer is over them.
|
|
|
|
local Bag = require("src.inventory.Bag")
|
|
local Theme = require("Theme")
|
|
local Ops = require("Ops")
|
|
local PAL = Theme.PAL
|
|
|
|
local M = {}
|
|
|
|
local MONEY_STEPS = { -1000, -100, 100, 1000 }
|
|
|
|
-- One quantity row shape, shared by the bag and the PC list: id, qty, then
|
|
-- the -/+/drop cluster. Returns true when the row body was clicked.
|
|
local function quantityRow(S, Kit, x, y, w, h, id, qty, selected, onMinus, onPlus, onDrop)
|
|
local s = Kit.scale
|
|
local clicked = Kit.row(x, y, w, h, selected, PAL.blue, 9 * s)
|
|
local btn = 24 * s
|
|
local bx = x + w - 10 * s - 3 * btn - 2 * (6 * s)
|
|
if Kit.stepper(bx, y + (h - btn) / 2, btn, btn, "-", { font = "small" }) then
|
|
onMinus()
|
|
end
|
|
if Kit.stepper(bx + btn + 6 * s, y + (h - btn) / 2, btn, btn, "+",
|
|
{ font = "small" }) then
|
|
onPlus()
|
|
end
|
|
if Kit.button(bx + 2 * (btn + 6 * s), y + (h - btn) / 2, btn, btn, "x",
|
|
{ kind = "danger", font = "tiny", radius = 6 * s }) then
|
|
onDrop()
|
|
end
|
|
local qtyText = ("x%d"):format(qty)
|
|
local qtyW = Kit.textWidth("monoRow", qtyText)
|
|
Kit.textRight("monoRow", qtyText, bx - 10 * s,
|
|
y + (h - Kit.textHeight("monoRow")) / 2, PAL.heading)
|
|
Kit.text("mono", Kit.ellipsize("mono", id, bx - qtyW - 30 * s - (x + 10 * s)),
|
|
x + 10 * s, y + (h - Kit.textHeight("mono")) / 2, PAL.text)
|
|
return clicked
|
|
end
|
|
|
|
-- ---------------------------------------------------------------- sections
|
|
-- Each card is a function of its own rect so the wide (three column) and the
|
|
-- stacked (#715) layouts are the same drawing code with different geometry.
|
|
|
|
local function moneyHeight(Kit, s, pad)
|
|
return pad * 2 + Kit.textHeight("caption") + 8 * s
|
|
+ Kit.textHeight("headline") + 10 * s + 30 * s
|
|
end
|
|
|
|
local function drawMoney(S, Kit, x, y, w, h)
|
|
local s = Kit.scale
|
|
local pad = 16 * s
|
|
Kit.card(x, y, w, h)
|
|
Kit.caption(x + pad, y + pad, "MONEY")
|
|
local maxW = 74 * s
|
|
if Kit.button(x + w - pad - maxW, y + pad - 4 * s, maxW, 26 * s, "Max out",
|
|
{ kind = "accent", font = "tiny", radius = 7 * s,
|
|
enabled = (S.save.money or 0) < Ops.MONEY_MAX }) then
|
|
Ops.maxMoney(S)
|
|
end
|
|
Kit.text("headline", ("$%d"):format(S.save.money or 0), x + pad,
|
|
y + pad + Kit.textHeight("caption") + 8 * s, PAL.yellow)
|
|
local mbY = y + h - pad - 30 * s
|
|
local mbW = (w - 2 * pad - 3 * 8 * s) / 4
|
|
for i, delta in ipairs(MONEY_STEPS) do
|
|
local label = (delta > 0 and "+" or "") .. tostring(delta)
|
|
if Kit.button(x + pad + (i - 1) * (mbW + 8 * s), mbY, mbW, 30 * s, label,
|
|
{ kind = "accent", font = "tiny", radius = 8 * s }) then
|
|
Ops.addMoney(S, delta)
|
|
end
|
|
end
|
|
end
|
|
|
|
local BADGE_COLS = 4
|
|
|
|
local function badgeHeight(S, Kit, s, pad)
|
|
local badgeRows = math.ceil(#Ops.badgeIds(S) / BADGE_COLS)
|
|
return pad * 2 + Kit.textHeight("caption") + 10 * s
|
|
+ badgeRows * (28 * s + 7 * s) - 7 * s
|
|
end
|
|
|
|
local function drawBadges(S, Kit, x, y, w, h)
|
|
local s = Kit.scale
|
|
local pad = 16 * s
|
|
local badgeIds = Ops.badgeIds(S)
|
|
Kit.card(x, y, w, h)
|
|
local earned = 0
|
|
for _, id in ipairs(badgeIds) do
|
|
-- #515: truthy check, not `== true` -- the in-game grant path stores a
|
|
-- number (see OverworldController.lua checkVictoryRewards), matching
|
|
-- src/inventory/Badges.lua's own truthy read.
|
|
if S.save.inventory[id] then earned = earned + 1 end
|
|
end
|
|
Kit.caption(x + pad, y + pad, "BADGES")
|
|
Kit.textRight("mono", ("%d/%d"):format(earned, #badgeIds), x + w - pad,
|
|
y + pad, PAL.caption)
|
|
local bTop = y + pad + Kit.textHeight("caption") + 10 * s
|
|
local bW = (w - 2 * pad - (BADGE_COLS - 1) * 7 * s) / BADGE_COLS
|
|
for i, id in ipairs(badgeIds) do
|
|
local bc = (i - 1) % BADGE_COLS
|
|
local br = math.floor((i - 1) / BADGE_COLS)
|
|
local on = S.save.inventory[id]
|
|
local short = id:gsub("BADGE$", "")
|
|
if Kit.chip(x + pad + bc * (bW + 7 * s), bTop + br * (28 * s + 7 * s),
|
|
bW, 28 * s, Kit.ellipsize("micro", short, bW - 8 * s), on,
|
|
PAL.green, PAL.steel) then
|
|
Ops.toggleBadge(S, id)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- The "add an item" card. It used to hold the whole catalog inline: a search
|
|
-- field plus a scrolling list, sharing this tab's height with the bag and PC
|
|
-- lists beside it, which on a phone left about three catalog rows visible.
|
|
-- Adding a Pokemon was already a full-screen modal; adding an item now opens
|
|
-- the same kind (panels/ItemPicker.lua), so this card is just the door.
|
|
local function drawPicker(S, Kit, x, y, w, h)
|
|
local s = Kit.scale
|
|
local pad = 16 * s
|
|
Kit.card(x, y, w, h)
|
|
Kit.caption(x + pad, y + pad, "ADD ITEM")
|
|
local cy = y + pad + Kit.textHeight("caption") + 10 * s
|
|
local inner = w - 2 * pad
|
|
|
|
Kit.text("mono", Kit.ellipsize("mono",
|
|
"Search the full item list and add to the bag or the PC.", inner),
|
|
x + pad, cy, PAL.muted)
|
|
cy = cy + Kit.textHeight("mono") + 12 * s
|
|
|
|
local btnH = math.max(34 * s, 34)
|
|
local half = (inner - 8 * s) / 2
|
|
if Kit.button(x + pad, cy, half, btnH, "+ Add to bag",
|
|
{ font = "small", kind = "primary" }) then
|
|
Ops.openItemPicker(S, Kit, "bag")
|
|
end
|
|
if Kit.button(x + pad + half + 8 * s, cy, half, btnH, "+ Add to PC",
|
|
{ font = "small", kind = "accent" }) then
|
|
Ops.openItemPicker(S, Kit, "pc")
|
|
end
|
|
end
|
|
|
|
-- The bag and PC cards share one shape: a caption line, an optional meter,
|
|
-- a quantity-row list with wheel/drag + pager.
|
|
local function drawQuantityCard(S, Kit, x, y, w, h, cfg)
|
|
local s = Kit.scale
|
|
local pad = 16 * s
|
|
Kit.card(x, y, w, h)
|
|
Kit.caption(x + pad, y + pad, cfg.title)
|
|
Kit.textRight("mono", cfg.counter, x + w - pad, y + pad, PAL.caption)
|
|
local rowsTop = y + pad + Kit.textHeight("caption") + 8 * s
|
|
if cfg.meterFrac then
|
|
Kit.meter(x + pad, rowsTop, w - 2 * pad, 5 * s, cfg.meterFrac * 100,
|
|
cfg.meterFrac >= 1 and PAL.yellow or PAL.blue)
|
|
rowsTop = rowsTop + 5 * s + 12 * s
|
|
else
|
|
rowsTop = rowsTop + 12 * s
|
|
end
|
|
|
|
local pagerH = 30 * s
|
|
local pagerY = y + h - pad - pagerH
|
|
local rowH = 36 * s
|
|
local rowGap = 6 * s
|
|
local listH = pagerY - 12 * s - rowsTop
|
|
local perPage = math.max(1, math.floor(listH / (rowH + rowGap)))
|
|
local order = cfg.order
|
|
local offset = Ops.clamp(cfg.offset or 0, 0, math.max(0, #order - perPage))
|
|
-- the wheel moves the same offset the pager below does (#595)
|
|
offset = Kit.scroll(x + pad, rowsTop, w - 2 * pad, listH, offset, #order, perPage)
|
|
|
|
if #order == 0 then
|
|
Kit.emptyBox(x + pad, rowsTop, w - 2 * pad, math.min(listH, 70 * s), cfg.empty)
|
|
end
|
|
Kit.pushClip(x + pad, rowsTop, w - 2 * pad, listH)
|
|
for i = 1, math.min(perPage, #order - offset) do
|
|
local id = order[offset + i]
|
|
local ry = rowsTop + (i - 1) * (rowH + rowGap)
|
|
if quantityRow(S, Kit, x + pad, ry, w - 2 * pad, rowH, id,
|
|
cfg.qty(id), id == cfg.selected(),
|
|
function() cfg.adjust(id, -1) end,
|
|
function() cfg.adjust(id, 1) end,
|
|
function() cfg.drop(id) end) then
|
|
cfg.select(id)
|
|
end
|
|
end
|
|
Kit.popClip()
|
|
Kit.scrollbar(x + pad, rowsTop, w - 2 * pad, listH, offset, #order, perPage)
|
|
return Kit.pager(x + pad, pagerY, w - 2 * pad, offset, #order, perPage)
|
|
end
|
|
|
|
local function drawBag(S, Kit, x, y, w, h)
|
|
local order = Bag.order(S.save)
|
|
local capacity = Bag.capacity(S.data)
|
|
S.bagOffset = drawQuantityCard(S, Kit, x, y, w, h, {
|
|
title = "BAG",
|
|
counter = ("%d/%d slots"):format(Bag.slots(S.save), capacity),
|
|
meterFrac = Bag.slots(S.save) / capacity,
|
|
order = order,
|
|
offset = S.bagOffset,
|
|
empty = "Bag is empty.",
|
|
qty = function(id) return S.save.inventory[id] or 0 end,
|
|
selected = function() return S.selectedBagId end,
|
|
select = function(id)
|
|
S.selectedBagId = id
|
|
Ops.say(S, ("Selected %s in the bag"):format(id))
|
|
end,
|
|
adjust = function(id, d) Ops.bagAdjust(S, id, d) end,
|
|
drop = function(id) Ops.bagDrop(S, id) end,
|
|
})
|
|
end
|
|
|
|
local function drawPc(S, Kit, x, y, w, h)
|
|
local pcOrder = Ops.pcOrder(S)
|
|
S.pcOffset = drawQuantityCard(S, Kit, x, y, w, h, {
|
|
title = "PC STORAGE",
|
|
counter = ("%d kinds"):format(#pcOrder),
|
|
order = pcOrder,
|
|
offset = S.pcOffset,
|
|
empty = "PC storage is empty. Items sent here have no slot cap.",
|
|
qty = function(id) return S.save.pcItems[id] or 0 end,
|
|
selected = function() return S.selectedPcId end,
|
|
select = function(id)
|
|
S.selectedPcId = id
|
|
Ops.say(S, ("Selected %s in PC storage"):format(id))
|
|
end,
|
|
adjust = function(id, d) Ops.pcAdjust(S, id, d) end,
|
|
drop = function(id) Ops.pcDrop(S, id) end,
|
|
})
|
|
end
|
|
|
|
function M.draw(S, Kit, x, y, w, h)
|
|
local s = Kit.scale
|
|
local gap = 20 * s
|
|
local pad = 16 * s
|
|
Ops.pcItems(S)
|
|
|
|
if w < 900 * s then
|
|
-- stacked (#715): one full-width column, scrolled in pixels. The offset
|
|
-- from LAST frame's scrollPixels call positions this frame, and the call
|
|
-- itself comes after the cards so their inner lists claim the wheel or a
|
|
-- drag over their own bodies first.
|
|
local off = Theme.clamp(S.itemsScroll or 0, 0,
|
|
math.max(0, (S._itemsContentH or 0) - h))
|
|
local moneyH = moneyHeight(Kit, s, pad)
|
|
local badgeH = badgeHeight(S, Kit, s, pad)
|
|
local pickH = 280 * s
|
|
local listH = 300 * s
|
|
Kit.pushClip(x, y, w, h)
|
|
local cy = y - off
|
|
drawMoney(S, Kit, x, cy, w, moneyH); cy = cy + moneyH + gap
|
|
drawPicker(S, Kit, x, cy, w, pickH); cy = cy + pickH + gap
|
|
drawBadges(S, Kit, x, cy, w, badgeH); cy = cy + badgeH + gap
|
|
drawBag(S, Kit, x, cy, w, listH); cy = cy + listH + gap
|
|
drawPc(S, Kit, x, cy, w, listH); cy = cy + listH
|
|
Kit.popClip()
|
|
S._itemsContentH = (cy + off) - y
|
|
S.itemsScroll = Kit.scrollPixels(x, y, w, h, off, S._itemsContentH)
|
|
return
|
|
end
|
|
|
|
local leftW = math.max(260 * s, math.min(320 * s, w * 0.26))
|
|
local listW = (w - leftW - 2 * gap) / 2
|
|
local bagX = x + leftW + gap
|
|
local pcX = bagX + listW + gap
|
|
|
|
-- Money and badges are fixed-height so the picker gets every pixel left
|
|
-- over: cycling through ~250 item ids in a two-row list was the thing that
|
|
-- made the old panel unusable.
|
|
local moneyH = moneyHeight(Kit, s, pad)
|
|
local badgeH = badgeHeight(S, Kit, s, pad)
|
|
drawMoney(S, Kit, x, y, leftW, moneyH)
|
|
drawPicker(S, Kit, x, y + moneyH + gap, leftW, h - moneyH - badgeH - 2 * gap)
|
|
drawBadges(S, Kit, x, y + h - badgeH, leftW, badgeH)
|
|
drawBag(S, Kit, bagX, y, listW, h)
|
|
drawPc(S, Kit, pcX, y, listW, h)
|
|
end
|
|
|
|
return M
|